Skip to content

Instantly share code, notes, and snippets.

@rytswd
Created February 29, 2024 21:04
Show Gist options
  • Save rytswd/f44ee9476b4eea2400199350cf1d0261 to your computer and use it in GitHub Desktop.
Save rytswd/f44ee9476b4eea2400199350cf1d0261 to your computer and use it in GitHub Desktop.
Emacs: Move text, similar to VSCode
(defun rytswd/move-text--internal (arg)
"Moves text down or up based on arg input.
There are 6 patterns in total:
1. (Region) Direction up, with region beginning is where the cursor is at
2. (Region) Direction up, with region end is where the cursor is at
3. (Region) Direction down, with region beginning is where the cursor is at
4. (Region) Direction down, with region end is where the cursor is at
5. Direction up without region
6. Direction down without region
Those without region is more straightforward. The ones with regions is quite
cumbersome as I'd need to delete the lines and recerate the region after
lines have moved to the new location."
(let ((cursor-col (current-column)))
;; In case of region in active:
(if (and mark-active transient-mark-mode)
(let* ((beg (region-beginning))
(end (region-end))
(start (point))
(cursor-line (line-number-at-pos))
(region-other-point (if (eq start beg) end beg))
;; Compute the number of characters highlighted in region to
;; recreate the region later on.
(region-length (- start region-other-point))
;; The region is only to get the full line input.
(line-start (save-mark-and-excursion
(goto-char beg)
(line-beginning-position)))
(line-end (save-mark-and-excursion
(goto-char end)
;; Handle the final line in the buffer gracefully.
(if (eq (point) (point-max))
;; Simply return the final point.
(line-end-position)
;; If not, i.e. there is another line, include the line break.
(1+ (line-end-position))
)))
(text (buffer-substring line-start line-end))
)
;; NOTE: Steps from here are complicated, but at least it does the job.
(delete-region line-start line-end)
;; When deleting the line, the cursor moves to the beginning of next
;; line. Because I want to insert the deleted text one line below or
;; above, I need to (forward-line) first.
(forward-line arg)
(insert text)
;; Once the text is added back, I get back to the original position
;; where the cursor was. From there, (forward-line) again to ensure I'm
;; on the right text input.
(goto-line cursor-line)
(forward-line arg)
(move-to-column cursor-col)
;; Once I'm essentially where the cursor was at (in terms of the
;; selected text), I set the region from where my cursor was, and
;; create the region by the original number of characters selected
;; by the region. Because I start the region from where my cursor was
;; in the end, I make sure to use (exchange-point-and-mark) so that I
;; get back to where I was exactly before the command was run. In
;; order for the regino to stay visible and active, I need to use a
;; trick of (setq deactivate-mark nil).
(push-mark (point) nil t)
(setq deactivate-mark nil)
(forward-char (- region-length))
(exchange-point-and-mark)
)
;; Non-marker based
(let ((start (line-beginning-position)))
(if (> arg 0)
;; Direction down, i.e. arg is positive
(progn
(forward-line arg)
(transpose-lines arg)
(forward-line (- arg))
(move-to-column cursor-col))
;; Direction up, i.e. arg is negative
(progn
(transpose-lines (- arg))
(forward-line (- arg 1))
(move-to-column cursor-col))
)))))
(defun rytswd/move-text-down (arg)
"Move region (transient-mark-mode active) or current line
arg lines down."
(interactive "*p")
(rytswd/move-text--internal arg))
(defun rytswd/move-text-up (arg)
"Move region (transient-mark-mode active) or current line
arg lines up."
(interactive "*p")
(rytswd/move-text--internal (- arg)))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment