dot-emacs/local-lisp/move-text.el

198 lines
6 KiB
EmacsLisp

;;; move-text.el --- Move current line or region with M-up or M-down. -*- lexical-binding: t; -*-
;; filename: move-text.el
;; Description: Move current line or region with M-up or M-down.
;; Author: Jason Milkins <jasonm23@gmail.com>
;; Keywords: edit
;; Url: https://github.com/emacsfodder/move-text
;; Compatibility: GNU Emacs 25.1
;; Version: 2.0.10
;;
;;; This file is NOT part of GNU Emacs
;;; License
;;
;; This program is free software; you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation; either version 3, or (at your option)
;; any later version.
;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
;; GNU General Public License for more details.
;; You should have received a copy of the GNU General Public License
;; along with this program; see the file COPYING. If not, write to
;; the Free Software Foundation, Inc., 51 Franklin Street, Fifth
;; Floor, Boston, MA 02110-1301, USA.
;;; Commentary:
;;
;; MoveText 2.0.0 is a re-write of the old move-text and compatible with >= Emacs 25.1
;;
;; It allows you to move the current line using M-up / M-down if a
;; region is marked, it will move the region instead.
;;
;; Using the prefix (C-u *number* or META *number*) you can predefine how
;; many lines move-text will travel.
;;
;;; Installation:
;;
;; Put move-text.el to your load-path.
;; The load-path is usually ~/elisp/.
;; It's set in your ~/.emacs like this:
;; (add-to-list 'load-path (expand-file-name "~/elisp"))
;;
;; And the following to your ~/.emacs startup file.
;;
;; (require 'move-text)
;; (move-text-default-bindings)
;;; Acknowledgements:
;;
;; Original v1.x was a Feature extracted from basic-edit-toolkit.el - by Andy Stewart (LazyCat)
;;
;;; Code:
(require 'cl-lib)
(defun move-text-get-region-and-prefix ()
"Get the region and prefix for the `interactive' macro, without aborting.
Note: `region-beginning' and `region-end' are the reason why an
`interactive' macro with \"r\" will blow up with the error:
\"The mark is not set now, so there is no region\"
We check with `use-region-p' to avoid calling
them when there's no region or it is not appropriate
to act on it.
We use `prefix-numeric-value' to return a number.
"
(list
(when (use-region-p) (region-beginning)) ;; otherwise nil
(when (use-region-p) (region-end))
(prefix-numeric-value current-prefix-arg)))
;;;###autoload
(defun move-text--total-lines ()
"Convenience function to get the total lines in the buffer / or narrowed buffer."
(line-number-at-pos (point-max)))
;;;###autoload
(defun move-text--at-first-line-p ()
"Predicate, is the point at the first line?"
(= (line-number-at-pos) 1))
;;;###autoload
(defun move-text--at-penultimate-line-p ()
"Predicate, is the point at the penultimate line?"
(= (line-number-at-pos) (1- (move-text--total-lines))))
;; save-mark-and-excursion in Emacs 25 works like save-excursion did before
(eval-when-compile
(when (< emacs-major-version 25)
(defmacro save-mark-and-excursion (&rest body)
`(save-excursion ,@body))))
;;;###autoload
(defun move-text--last-line-is-just-newline ()
"Predicate, is last line just a newline?"
(save-mark-and-excursion
(goto-char (point-max))
(beginning-of-line)
(= (point-max) (point))))
;;;###autoload
(defun move-text--at-last-line-p ()
"Predicate, is the point at the last line?"
(= (line-number-at-pos) (move-text--total-lines)))
;;;###autoload
(defun move-text-line-up ()
"Move the current line up."
(interactive)
(if (move-text--at-last-line-p)
(let ((target-point))
(kill-whole-line)
(forward-line -1)
(beginning-of-line)
(setq target-point (point))
(yank)
(unless (looking-at "\n")
(newline))
(goto-char target-point))
(let ((col (current-column)))
(progn (transpose-lines 1)
(forward-line -2)
(move-to-column col)))))
;;;###autoload
(defun move-text-line-down ()
"Move the current line down."
(interactive)
(unless (or
(move-text--at-last-line-p)
(and
(move-text--last-line-is-just-newline)
(move-text--at-penultimate-line-p)))
(let ((col (current-column)))
(forward-line 1)
(transpose-lines 1)
(forward-line -1)
(move-to-column col))))
;;;###autoload
(defun move-text-region (start end n)
"Move the current region (START END) up or down by N lines."
(interactive (move-text-get-region-and-prefix))
(let ((line-text (delete-and-extract-region start end)))
(forward-line n)
(let ((start (point)))
(insert line-text)
(setq deactivate-mark nil)
(set-mark start))))
;;;###autoload
(defun move-text-region-up (start end n)
"Move the current region (START END) up by N lines."
(interactive (move-text-get-region-and-prefix))
(move-text-region start end (- n)))
;;;###autoload
(defun move-text-region-down (start end n)
"Move the current region (START END) down by N lines."
(interactive (move-text-get-region-and-prefix))
(move-text-region start end n))
;;;###autoload
(defun move-text-up (start end n)
"Move the line or region (START END) up by N lines."
(interactive (move-text-get-region-and-prefix))
(if (not (move-text--at-first-line-p))
(if (region-active-p)
(move-text-region-up start end n)
(cl-loop repeat n do (move-text-line-up)))))
;;;###autoload
(defun move-text-down (start end n)
"Move the line or region (START END) down by N lines."
(interactive (move-text-get-region-and-prefix))
(if (region-active-p)
(move-text-region-down start end n)
(cl-loop repeat n do (move-text-line-down))))
;;;###autoload
(defun move-text-default-bindings ()
"Bind `move-text-up' and `move-text-down' to M-up & M-down."
(interactive)
(global-set-key [M-down] 'move-text-down)
(global-set-key [M-up] 'move-text-up))
(provide 'move-text)
;;; move-text.el ends here