├── move-text.gif ├── .github └── workflows │ └── test.yml ├── README.md ├── move-text-tests.el └── move-text.el /move-text.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emacsfodder/move-text/HEAD/move-text.gif -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Move-Text CI Tests 2 | 3 | on: 4 | push: 5 | pull_request: 6 | 7 | jobs: 8 | test: 9 | runs-on: ubuntu-latest 10 | strategy: 11 | matrix: 12 | emacs_version: 13 | - 25.1 14 | - 25.2 15 | - 25.3 16 | - 26.1 17 | - 26.2 18 | - 26.3 19 | - 27.1 20 | - 27.2 21 | - 28.1 22 | - 28.2 23 | - 29.1 24 | - snapshot 25 | fail-fast: false 26 | steps: 27 | - uses: actions/checkout@v3 28 | - name: Set up Emacs 29 | uses: purcell/setup-emacs@master 30 | with: 31 | version: ${{ matrix.emacs_version }} 32 | 33 | - name: Test 34 | run: | 35 | emacs --batch \ 36 | -eval "(setq load-prefer-newer t)" \ 37 | -eval "(add-to-list 'load-path \".\")" \ 38 | -l ert \ 39 | -l move-text.el \ 40 | -l move-text-tests.el \ 41 | -f ert-run-tests-batch-and-exit 42 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | > Hi all, Please consider donating to this or any of my many of opensource projects. 2 | > 3 | > Buy Me a Coffee at ko-fi.com 4 | 5 | [![Move-Text CI Tests](https://github.com/emacsfodder/move-text/actions/workflows/test.yml/badge.svg)](https://github.com/emacsfodder/move-text/actions/workflows/test.yml) 6 | [![MELPA](https://melpa.org/packages/move-text-badge.svg)](https://melpa.org/#/move-text) 7 | [![MELPA Stable](https://stable.melpa.org/packages/move-text-badge.svg)](https://stable.melpa.org/#/move-text) 8 | 9 | # Move Text 10 | 11 | MoveText 12 | allows you to move the current line using M-up / M-down (or any other bindings you choose) 13 | if a region is marked, it will move the region instead. 14 | 15 | Using the prefix arg (C-u *number* or META *number*) will predetermine how many lines to move. 16 | 17 | Install from MELPA (or MELPA stable) 18 | 19 | ``` 20 | M-x package-install move-text 21 | ``` 22 | 23 | If you want to use the default bindings, add the following to .emacs 24 | anywhere after `(package-initialize)`: 25 | 26 | ``` 27 | (move-text-default-bindings) 28 | ``` 29 | This sets the keyboard shortcuts: 30 | 31 | - Meta-up `move-text-up` (line or active region) 32 | - Meta-down `move-text-down` (line or active region) 33 | 34 | ## Demonstration 35 | 36 | ![](move-text.gif) 37 | 38 | ### Indent after moving... 39 | 40 | [@jbreeden](https://github.com/jbreeden) gave us this useful function advice to have Emacs re-indent the text in-and-around a text move. 41 | 42 | ```lisp 43 | (defun indent-region-advice (&rest ignored) 44 | (let ((deactivate deactivate-mark)) 45 | (if (region-active-p) 46 | (indent-region (region-beginning) (region-end)) 47 | (indent-region (line-beginning-position) (line-end-position))) 48 | (setq deactivate-mark deactivate))) 49 | 50 | (advice-add 'move-text-up :after 'indent-region-advice) 51 | (advice-add 'move-text-down :after 'indent-region-advice) 52 | ``` 53 | -------------------------------------------------------------------------------- /move-text-tests.el: -------------------------------------------------------------------------------- 1 | ;;; move-text-tests.el --- Tests for Move-Text -*- lexical-binding: t; -*- 2 | ;; 3 | ;; Created: September 07, 2022 4 | 5 | ;;; Code: 6 | 7 | (require 'move-text) 8 | (require 'ert) 9 | 10 | (defmacro should-on-temp-buffer (input-string expect-string &rest body) 11 | "INPUT-STRING processed by BODY forms in a temp buffer should equal EXPECT-STRING." 12 | (declare (indent 1) (debug t)) 13 | `(should (string= ,expect-string 14 | (with-temp-buffer 15 | (insert ,input-string) 16 | (goto-char (point-min)) 17 | ,@body 18 | (buffer-string))))) 19 | 20 | (let ((transient-mark-mode t)) 21 | 22 | (ert-deftest move-line-down-test () 23 | "Move text down by (1) one line, (2) by region." 24 | (should-on-temp-buffer 25 | "This is a test 26 | Line 2 27 | Line 3 28 | " 29 | "Line 2 30 | This is a test 31 | Line 3 32 | " 33 | (goto-char 0) 34 | (call-interactively #'move-text-down))) 35 | 36 | (ert-deftest move-region-down-test () 37 | (should-on-temp-buffer 38 | "This is a test 39 | Line 2 40 | Line 3 41 | Line 4 42 | Line 5 43 | Line 6 44 | " 45 | "This is a test 46 | Line 2 47 | Line 4 48 | Line 3 49 | Line 5 50 | Line 6 51 | " 52 | (forward-line 2) 53 | (push-mark) 54 | (activate-mark) 55 | (forward-line) 56 | (message "Mark at %d - Point at %d" (mark) (point)) 57 | (move-text-down (mark) (point) 1))) 58 | 59 | (ert-deftest move-line-up-test () 60 | "Move text up by (1) one line, (2) by region." 61 | (should-on-temp-buffer 62 | "This is a test 63 | Line 2 64 | Line 3 65 | " 66 | "This is a test 67 | Line 3 68 | Line 2 69 | " 70 | (forward-line 2) 71 | (call-interactively #'move-text-up))) 72 | 73 | (ert-deftest move-region-up-test () 74 | (should-on-temp-buffer 75 | "This is a test 76 | Line 2 77 | Line 3 78 | Line 4 79 | Line 5 80 | Line 6 81 | " 82 | "Line 2 83 | Line 3 84 | This is a test 85 | Line 4 86 | Line 5 87 | Line 6 88 | " 89 | (goto-char 0) 90 | (forward-line) 91 | (push-mark) 92 | (activate-mark) 93 | (forward-line 2) 94 | (move-text-up (mark) (point) 1)))) 95 | 96 | ;;; move-text-tests.el ends here 97 | -------------------------------------------------------------------------------- /move-text.el: -------------------------------------------------------------------------------- 1 | ;;; move-text.el --- Move current line or region with M-up or M-down. -*- lexical-binding: t; -*- 2 | 3 | ;; filename: move-text.el 4 | ;; Description: Move current line or region with M-up or M-down. 5 | ;; Author: Jason Milkins 6 | ;; Keywords: edit 7 | ;; Url: https://github.com/emacsfodder/move-text 8 | ;; Compatibility: GNU Emacs 25.1 9 | ;; Version: 2.0.10 10 | ;; 11 | ;;; This file is NOT part of GNU Emacs 12 | 13 | ;;; License 14 | ;; 15 | ;; This program is free software; you can redistribute it and/or modify 16 | ;; it under the terms of the GNU General Public License as published by 17 | ;; the Free Software Foundation; either version 3, or (at your option) 18 | ;; any later version. 19 | 20 | ;; This program is distributed in the hope that it will be useful, 21 | ;; but WITHOUT ANY WARRANTY; without even the implied warranty of 22 | ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 23 | ;; GNU General Public License for more details. 24 | 25 | ;; You should have received a copy of the GNU General Public License 26 | ;; along with this program; see the file COPYING. If not, write to 27 | ;; the Free Software Foundation, Inc., 51 Franklin Street, Fifth 28 | ;; Floor, Boston, MA 02110-1301, USA. 29 | 30 | ;;; Commentary: 31 | ;; 32 | ;; MoveText 2.0.0 is a re-write of the old move-text and compatible with >= Emacs 25.1 33 | ;; 34 | ;; It allows you to move the current line using M-up / M-down if a 35 | ;; region is marked, it will move the region instead. 36 | ;; 37 | ;; Using the prefix (C-u *number* or META *number*) you can predefine how 38 | ;; many lines move-text will travel. 39 | ;; 40 | 41 | ;;; Installation: 42 | ;; 43 | ;; Put move-text.el to your load-path. 44 | ;; The load-path is usually ~/elisp/. 45 | ;; It's set in your ~/.emacs like this: 46 | ;; (add-to-list 'load-path (expand-file-name "~/elisp")) 47 | ;; 48 | ;; And the following to your ~/.emacs startup file. 49 | ;; 50 | ;; (require 'move-text) 51 | ;; (move-text-default-bindings) 52 | 53 | ;;; Acknowledgements: 54 | ;; 55 | ;; Original v1.x was a Feature extracted from basic-edit-toolkit.el - by Andy Stewart (LazyCat) 56 | ;; 57 | 58 | ;;; Code: 59 | (require 'cl-lib) 60 | 61 | (defun move-text-get-region-and-prefix () 62 | "Get the region and prefix for the `interactive' macro, without aborting. 63 | 64 | Note: `region-beginning' and `region-end' are the reason why an 65 | `interactive' macro with \"r\" will blow up with the error: 66 | 67 | \"The mark is not set now, so there is no region\" 68 | 69 | We check with `use-region-p' to avoid calling 70 | them when there's no region or it is not appropriate 71 | to act on it. 72 | 73 | We use `prefix-numeric-value' to return a number. 74 | " 75 | (list 76 | (when (use-region-p) (region-beginning)) ;; otherwise nil 77 | (when (use-region-p) (region-end)) 78 | (prefix-numeric-value current-prefix-arg))) 79 | 80 | ;;;###autoload 81 | (defun move-text--total-lines () 82 | "Convenience function to get the total lines in the buffer / or narrowed buffer." 83 | (line-number-at-pos (point-max))) 84 | 85 | ;;;###autoload 86 | (defun move-text--at-first-line-p () 87 | "Predicate, is the point at the first line?" 88 | (= (line-number-at-pos) 1)) 89 | 90 | ;;;###autoload 91 | (defun move-text--at-penultimate-line-p () 92 | "Predicate, is the point at the penultimate line?" 93 | (= (line-number-at-pos) (1- (move-text--total-lines)))) 94 | 95 | ;; save-mark-and-excursion in Emacs 25 works like save-excursion did before 96 | (eval-when-compile 97 | (when (< emacs-major-version 25) 98 | (defmacro save-mark-and-excursion (&rest body) 99 | `(save-excursion ,@body)))) 100 | 101 | ;;;###autoload 102 | (defun move-text--last-line-is-just-newline () 103 | "Predicate, is last line just a newline?" 104 | (save-mark-and-excursion 105 | (goto-char (point-max)) 106 | (beginning-of-line) 107 | (= (point-max) (point)))) 108 | 109 | ;;;###autoload 110 | (defun move-text--at-last-line-p () 111 | "Predicate, is the point at the last line?" 112 | (= (line-number-at-pos) (move-text--total-lines))) 113 | 114 | ;;;###autoload 115 | (defun move-text-line-up () 116 | "Move the current line up." 117 | (interactive) 118 | (if (move-text--at-last-line-p) 119 | (let ((target-point)) 120 | (kill-whole-line) 121 | (forward-line -1) 122 | (beginning-of-line) 123 | (setq target-point (point)) 124 | (yank) 125 | (unless (looking-at "\n") 126 | (newline)) 127 | (goto-char target-point)) 128 | (let ((col (current-column))) 129 | (progn (transpose-lines 1) 130 | (forward-line -2) 131 | (move-to-column col))))) 132 | 133 | ;;;###autoload 134 | (defun move-text-line-down () 135 | "Move the current line down." 136 | (interactive) 137 | (unless (or 138 | (move-text--at-last-line-p) 139 | (and 140 | (move-text--last-line-is-just-newline) 141 | (move-text--at-penultimate-line-p))) 142 | (let ((col (current-column))) 143 | (forward-line 1) 144 | (transpose-lines 1) 145 | (forward-line -1) 146 | (move-to-column col)))) 147 | 148 | ;;;###autoload 149 | (defun move-text-region (start end n) 150 | "Move the current region (START END) up or down by N lines." 151 | (interactive (move-text-get-region-and-prefix)) 152 | (let ((line-text (delete-and-extract-region start end))) 153 | (forward-line n) 154 | (let ((start (point))) 155 | (insert line-text) 156 | (setq deactivate-mark nil) 157 | (set-mark start)))) 158 | 159 | ;;;###autoload 160 | (defun move-text-region-up (start end n) 161 | "Move the current region (START END) up by N lines." 162 | (interactive (move-text-get-region-and-prefix)) 163 | (move-text-region start end (- n))) 164 | 165 | ;;;###autoload 166 | (defun move-text-region-down (start end n) 167 | "Move the current region (START END) down by N lines." 168 | (interactive (move-text-get-region-and-prefix)) 169 | (move-text-region start end n)) 170 | 171 | ;;;###autoload 172 | (defun move-text-up (start end n) 173 | "Move the line or region (START END) up by N lines." 174 | (interactive (move-text-get-region-and-prefix)) 175 | (if (not (move-text--at-first-line-p)) 176 | (if (region-active-p) 177 | (move-text-region-up start end n) 178 | (cl-loop repeat n do (move-text-line-up))))) 179 | 180 | ;;;###autoload 181 | (defun move-text-down (start end n) 182 | "Move the line or region (START END) down by N lines." 183 | (interactive (move-text-get-region-and-prefix)) 184 | (if (region-active-p) 185 | (move-text-region-down start end n) 186 | (cl-loop repeat n do (move-text-line-down)))) 187 | 188 | ;;;###autoload 189 | (defun move-text-default-bindings () 190 | "Bind `move-text-up' and `move-text-down' to M-up & M-down." 191 | (interactive) 192 | (global-set-key [M-down] 'move-text-down) 193 | (global-set-key [M-up] 'move-text-up)) 194 | 195 | (provide 'move-text) 196 | 197 | ;;; move-text.el ends here 198 | --------------------------------------------------------------------------------