dotfiles/.emacs.d/vim-mode/vim-commands.el
michener 5b6729933d Add emacs directory
git-svn-id: http://photonzero.com/dotfiles/trunk@54 23f722f6-122a-0410-8cef-c75bd312dd78
2010-08-12 01:19:45 +00:00

698 lines
25 KiB
EmacsLisp

;;; vim-commands.el - Implementation of VIM commands.
;; Copyright (C) 2009, 2010 Frank Fischer
;; Author: Frank Fischer <frank.fischer@mathematik.tu-chemnitz.de>,
;;
;; This file is not part of GNU Emacs.
;;; Commentary:
;; In general there are two types of commands: those operating on a
;; motion and those not taking a motion. Examples of the first one are
;; the vim-commands c, d, y, =, examples of the second one are dd, D,
;; p, x.
;;
;; Commands are defined using the `vim:defcmd' macro and have the
;; following form:
;;
;; (vim:defcmd name (count
;; motion[:optional]
;; argument[:{char,file,buffer}]
;; [nonrepeatable]
;; [keep-visual])
;; body ...)
;;
;; Each of the arguments is optional. The names of the arguments must
;; be exactly as in the definition above (but see 'Argument-renaming'
;; below).
;;
;; The COUNT argument (if given) takes the count of the command which
;; is usually the number how often the command should be repeated.
;; This argument may be nil if no count is given. If the command takes
;; a MOTION argument, no COUNT argument is allowed (will always be
;; nil).
;;
;; The MOTION argument defines the range where the command should work
;; on. It's always of type `vim:motion'. Usually, a command should
;; respect the of the motion, i.e. charwise, linewise or block, but
;; there are commands that behave indepently of the motion type (e.g.
;; `vim:cmd-shift-left' always works linewise). If the MOTION
;; parameter has the form motion:optional, the MOTION parameter may be
;; nil, which can only happen if the command is bound in ex-mode (e.g.
;; the command `vim:cmd-substitute' is bound to :s may be called
;; without a motion, in which case it works only on the current line).
;; If the command is bound in normal-mode, the MOTION argument will
;; usually be created by some motion-command bound in
;; operator-pending-mode.
;;
;; The ARGUMENT argument is an aditional text-argument to be given and
;; may be nil, too. If it is specified as ARGUMENT:CHAR, the argument
;; is a one-character argument (see `vim:cmd-replace-char' usually
;; bound to 'r' for an example). If it specified as ARGUMENT:FILE it
;; takes a file-name as argument, ARGUMENT:BUFFER takes a buffer-name
;; as argument and a single ARGUMENT takes a string as argument. Only
;; the type ARGUMENT:CHAR has an effect in normal-mode, the others are
;; only important if bound in ex-mode. In this case the type of the
;; argument determines how minibuffer-completion is done. The argument
;; may be nil in which case the command should have a default
;; behaviour (e.g. the command `vim:cmd-write' bound to :write takes
;; an ARGUMENT:FILE argument and saves the current buffer to the given
;; file or to the buffer's own file if ARGUMENT is nil).
;;
;; The pseudo-argument NONREPEATABLE means, the command will not be
;; recorded to the repeat command (usually bound to '.'). This is
;; useful for non-editing commands, e.g. all window and scrolling
;; commands have this behaviour.
;;
;; The pseudo-argument KEEP-VISUAL means the command should not exit
;; visual-mode and go back to normal-mode when called in visual-mode.
;; This is useful for scrolling-commands which stay in visual-mode but
;; are no regular motions (scrolling commands move the (point) but are
;; no real motions since they can't in operating-pending mode), or
;; some visual-mode specific command like
;; `vim:visual-exchange-point-and-mark', usually bound to 'o').
;;
;; If you do not like the default argument names, they may be renamed
;; by using (ARG NEWNAME) instead of ARG, e.g.
;;
;; (vim:defcmd vim:cmd-replace-char (count (argument:char arg))
;;
;; defines a simple command with a COUNT argument but renames the
;; character-argument to ARG.
;;
;; Each command should place (point) at the correct position after the
;; operation.
;;
;;; Code:
(defcustom vim:shift-width 8
"The number of columns for shifting commands like < or >."
:type 'integer
:group 'vim-mode)
(vim:defcmd vim:cmd-insert (count)
"Switches to insert-mode before point."
(vim:activate-insert-mode))
(vim:defcmd vim:cmd-append (count)
"Switches to insert-mode after point."
(unless (eolp) (forward-char))
(vim:activate-insert-mode))
(vim:defcmd vim:cmd-Insert (count)
"Moves the cursor to the beginning of the current line
and switches to insert-mode."
(vim:motion-first-non-blank)
(vim:cmd-insert :count count))
(vim:defcmd vim:cmd-Append (count)
"Moves the cursor to the end of the current line
and switches to insert-mode."
(end-of-line)
(vim:cmd-append :count count))
(vim:defcmd vim:cmd-insert-line-above (count)
"Inserts a new line above the current one and goes to insert mode."
(vim:motion-beginning-of-line)
(newline)
(forward-line -1)
(indent-according-to-mode)
(vim:cmd-Insert))
(vim:defcmd vim:cmd-insert-line-below (count)
"Inserts a new line below the current one and goes to insert mode."
(vim:motion-end-of-line)
(newline)
(indent-according-to-mode)
(vim:cmd-insert))
(vim:defcmd vim:cmd-replace (count)
"Goes to replace-mode."
(vim:activate-insert-mode)
(vim:insert-mode-toggle-replace))
(vim:defcmd vim:insert-mode-exit (nonrepeatable)
"Deactivates insert-mode, returning to normal-mode."
(vim:activate-normal-mode)
(goto-char (max (line-beginning-position) (1- (point)))))
(vim:defcmd vim:cmd-delete-line (count register)
"Deletes the next count lines."
(vim:cmd-yank-line :count count :register register)
(let ((beg (line-beginning-position))
(end (save-excursion
(forward-line (1- (or count 1)))
(line-end-position))))
(if (= beg (point-min))
(if (= end (point-max))
(erase-buffer)
(delete-region beg (save-excursion
(goto-char end)
(forward-line)
(line-beginning-position))))
(delete-region (save-excursion
(goto-char beg)
(forward-line -1)
(line-end-position))
end))
(goto-char beg)
(vim:motion-first-non-blank)))
(vim:defcmd vim:cmd-delete (motion register)
"Deletes the characters defined by motion."
(case (vim:motion-type motion)
('linewise
(goto-line (vim:motion-first-line motion))
(vim:cmd-delete-line :count (vim:motion-line-count motion)
:register register))
('block
(vim:cmd-yank :motion motion :register register)
(delete-rectangle (vim:motion-begin-pos motion)
(vim:motion-end-pos motion)))
(t
(vim:cmd-yank :motion motion :register register)
(delete-region (vim:motion-begin-pos motion) (vim:motion-end-pos motion))
(goto-char (vim:motion-begin-pos motion)))))
(vim:defcmd vim:cmd-delete-char (count register)
"Deletes the next count characters."
(vim:cmd-delete :motion (vim:motion-right :count (or count 1))
:register register))
(vim:defcmd vim:cmd-change (motion register)
"Deletes the characters defined by motion and goes to insert mode."
(case (vim:motion-type motion)
('linewise
(goto-line (vim:motion-first-line motion))
(vim:cmd-change-line :count (vim:motion-line-count motion)
:register register))
('block
(let ((insert-info (vim:make-visual-insert-info :first-line (vim:motion-first-line motion)
:last-line (vim:motion-last-line motion)
:column (vim:motion-first-col motion))))
(vim:cmd-delete :motion motion :register register)
(vim:visual-start-insert insert-info)))
(t
;; deal with cw and cW
(when (and vim:current-motion
(not (member (char-after) '(? ?\r ?\n ?\t))))
(cond
((eq vim:current-motion 'vim:motion-fwd-word)
(let* ((cnt (* (or vim:current-cmd-count 1)
(or vim:current-motion-count 1)))
(pos
(save-excursion
(dotimes (i cnt)
(while
(not
(or (and (looking-at (concat "[^ \t\r\n]"
"[ \t\r\n]")))
(and (looking-at (concat "[" vim:word "]"
"[^ \t\r\n" vim:word "]")))
(and (looking-at (concat "[^ \t\r\n" vim:word "]"
"[" vim:word "]")))))
(forward-char))
(when (< i (1- cnt))
(forward-char)))
(point))))
(setq motion (vim:make-motion :begin (point) :end pos :type 'inclusive))))
((eq vim:current-motion 'vim:motion-fwd-WORD)
(let* ((cnt (* (or vim:current-cmd-count 1)
(or vim:current-motion-count 1)))
(pos
(save-excursion
(dotimes (i cnt)
(while
(not (looking-at (concat "[^ \t\r\n]"
"[ \t\r\n]")))
(forward-char))
(when (< i (1- cnt))
(forward-char)))
(point))))
(setq motion (vim:make-motion :begin (point) :end pos :type 'inclusive))))))
(vim:cmd-delete :motion motion :register register)
(if (eolp)
(vim:cmd-append :count 1)
(vim:cmd-insert :count 1)))))
(vim:defcmd vim:cmd-change-line (count register)
"Deletes count lines and goes to insert mode."
(let ((pos (line-beginning-position)))
(vim:cmd-delete-line :count count :register register)
(if (< (point) pos)
(progn
(end-of-line)
(newline))
(progn
(beginning-of-line)
(newline)
(forward-line -1)))
(indent-according-to-mode)
(if (eolp)
(vim:cmd-append :count 1)
(vim:cmd-insert :count 1))))
(vim:defcmd vim:cmd-change-rest-of-line (register)
"Deletes the rest of the current line."
(vim:cmd-delete :motion (vim:make-motion :begin (point)
:end (1- (line-end-position))
:type 'inclusive)
:register register)
(vim:cmd-append :count 1))
(vim:defcmd vim:cmd-change-char (count register)
"Deletes the next count characters and goes to insert mode."
(let ((pos (point)))
(vim:cmd-delete-char :count count :register register)
(if (< (point) pos)
(vim:cmd-append)
(vim:cmd-insert))))
(vim:defcmd vim:cmd-replace-char (count (argument:char arg))
"Replaces the next count characters with arg."
(unless (vim:char-p arg)
(error "Expected a character."))
(when (< (- (line-end-position) (point))
(or count 1))
(error "Too few characters to end of line."))
(delete-region (point) (+ (point) (or count 1)))
(insert-char arg (or count 1))
(backward-char))
(vim:defcmd vim:cmd-replace-region (motion (argument:char arg))
"Replace the complete region with `arg'"
(case (vim:motion-type motion)
('block
;; replace in block
(let ((begrow (vim:motion-first-line motion))
(begcol (vim:motion-first-col motion))
(endrow (vim:motion-last-line motion))
(endcol (1+ (vim:motion-last-col motion))))
(goto-line begrow)
(dotimes (i (1+ (- endrow begrow)))
;; TODO does it work with \r\n at the end?
(let ((maxcol (save-excursion
(end-of-line)
(current-column))))
(when (> maxcol begcol)
(delete-region (save-excursion
(move-to-column begcol t)
(point))
(save-excursion
(move-to-column (min endcol maxcol) t)
(point)))
(move-to-column begcol t)
(insert-char arg (- (min endcol maxcol) begcol))))
(forward-line 1))
(goto-line begrow)
(move-to-column begcol)))
(t ;; replace in linewise and normal
(let ((begrow (vim:motion-first-line motion))
(endrow (vim:motion-last-line motion)))
(goto-line begrow)
(do ((r begrow (1+ r)))
((> r endrow))
(goto-line r)
(let ((begcol
(if (and (= r begrow)
(not (eq (vim:motion-type motion) 'linewise)))
(save-excursion
(goto-char (vim:motion-begin-pos motion))
(current-column))
0))
(endcol
(if (and (= r endrow)
(not (eq (vim:motion-type motion) 'linewise)))
(save-excursion
(goto-char (vim:motion-end-pos motion))
(current-column))
;; TODO does it work with \r\n at the end?
(save-excursion
(end-of-line)
(current-column)))))
(delete-region (save-excursion
(move-to-column begcol t)
(point))
(save-excursion
(move-to-column endcol t)
(point)))
(move-to-column begcol t)
(insert-char arg (- endcol begcol)))))
(goto-char (vim:motion-begin-pos motion)))))
(vim:defcmd vim:cmd-yank (motion register nonrepeatable)
"Saves the characters in motion into the kill-ring."
(case (vim:motion-type motion)
('block (vim:cmd-yank-rectangle :motion motion :register register))
('linewise (goto-line (vim:motion-first-line motion))
(vim:cmd-yank-line :count (vim:motion-line-count motion)
:register register))
(t
(let ((text (buffer-substring
(vim:motion-begin-pos motion)
(vim:motion-end-pos motion))))
(if register
(set-register register text)
(kill-new text))))))
(vim:defcmd vim:cmd-yank-line (count register nonrepeatable)
"Saves the next count lines into the kill-ring."
(let (lines
(linenr (line-number-at-pos (point))))
(setq count (or count 1))
(save-excursion
(while (> count 0)
(push (buffer-substring (line-beginning-position) (line-end-position)) lines)
(forward-line)
(decf count)
(incf linenr)
(when (> linenr (line-number-at-pos (point)))
(setq count 0))))
(if register
(let ((txt (make-string 1 ? )))
(put-text-property 0 1 'yank-handler
(list 'vim:yank-line-handler (reverse lines))
txt)
(set-register register txt))
(kill-new " " nil (list 'vim:yank-line-handler (reverse lines))))))
(vim:defcmd vim:cmd-yank-rectangle (motion register nonrepeatable)
"Stores the rectangle defined by motion into the kill-ring."
(unless (eq (vim:motion-type motion) 'block)
(error "Motion must be of type block"))
;; TODO: yanking should not insert spaces or expand tabs.
(let ((begrow (vim:motion-first-line motion))
(begcol (vim:motion-first-col motion))
(endrow (vim:motion-last-line motion))
(endcol (vim:motion-last-col motion))
(parts nil))
(goto-line endrow)
(dotimes (i (1+ (- endrow begrow)))
(let ((beg (save-excursion (move-to-column begcol) (point)))
(end (save-excursion (move-to-column (1+ endcol)) (point))))
(push (cons (save-excursion (goto-char beg)
(- (current-column) begcol))
(buffer-substring beg end))
parts)
(forward-line -1)))
(if register
(let ((txt (make-string 1 ? )))
(put-text-property 0 1
'yank-handler
(list 'vim:yank-block-handler
(cons (- endcol begcol -1) parts))
txt)
(set-register register txt))
(kill-new " " nil (list 'vim:yank-block-handler
(cons (- endcol begcol -1) parts))))
(goto-line begrow)
(move-to-column begcol)))
(defun vim:yank-line-handler (text)
"Inserts the current text linewise."
(beginning-of-line)
(dolist (line text)
(insert line)
(newline)))
(defun vim:yank-block-handler (text)
"Inserts the current text as block."
(let ((ncols (car text))
(parts (cdr text))
(col (current-column))
(current-line (line-number-at-pos (point))))
(dolist (part parts)
(let* ((offset (car part))
(txt (cdr part))
(len (length txt)))
;; maybe we have to insert a new line at eob
(when (< (line-number-at-pos (point))
current-line)
(goto-char (point-max))
(newline))
(incf current-line)
(unless (and (< (current-column) col) ; nothing in this line
(<= offset 0) (zerop len)) ; and nothing to insert
(move-to-column (+ col (max 0 offset)) t)
(insert txt)
(unless (eolp)
;; text follows, so we have to insert spaces
(insert (make-string (- ncols len) ? ))))
(forward-line 1)))))
(vim:defcmd vim:cmd-paste-before (count register)
"Pastes the latest yanked text before the cursor position."
(unless (or kill-ring-yank-pointer register)
(error "kill-ring empty"))
(dotimes (i (or count 1))
(save-excursion
(if register
(insert-for-yank (vim:get-register register))
(yank)))))
(vim:defcmd vim:cmd-paste-behind (count register)
"Pastes the latest yanked text behind point."
(unless (or kill-ring-yank-pointer register)
(error "kill-ring empty"))
(let* ((txt (if register
(vim:get-register register)
(car kill-ring-yank-pointer)))
(yhandler (get-text-property 0 'yank-handler txt)))
(case (car-safe yhandler)
(vim:yank-line-handler
(end-of-line)
(if (eobp)
(progn
(newline)
(save-excursion
(vim:cmd-paste-before :count count :register register)
(goto-char (point-max))
(delete-backward-char 1)))
(forward-line)
(vim:cmd-paste-before :count count :register register))
(vim:motion-first-non-blank))
(vim:yank-block-handler
(forward-char)
(vim:cmd-paste-before :count count :register register))
(t
(forward-char)
(dotimes (i (or count 1))
(if register
(insert-for-yank (vim:get-register register))
(yank)))
(backward-char)))))
(vim:defcmd vim:cmd-join-lines (count)
"Join `count' lines with a minimum of two lines."
(dotimes (i (max 1 (1- (or count 1))))
(when (re-search-forward "\\(\\s-*\\)\\(\n\\s-*\\)\\()?\\)")
(delete-region (match-beginning 2)
(match-end 2))
(when (and (= (match-beginning 1) (match-end 1))
(= (match-beginning 3) (match-end 3)))
(insert-char ? 1))
(backward-char))))
(vim:defcmd vim:cmd-join (motion)
"Join the lines covered by `motion'."
(goto-line (vim:motion-first-line motion))
(vim:cmd-join-lines :count (vim:motion-line-count motion)))
(vim:defcmd vim:cmd-indent (motion)
"Reindent the lines covered by `motion'."
(goto-line (vim:motion-first-line motion))
(indent-region (line-beginning-position)
(line-end-position (vim:motion-line-count motion))))
(vim:defcmd vim:cmd-shift-left (motion)
"Shift the lines covered by `motion' leftwards."
(goto-line (vim:motion-first-line motion))
(indent-rigidly (line-beginning-position)
(line-end-position (vim:motion-line-count motion))
(- vim:shift-width)))
(vim:defcmd vim:cmd-shift-right (motion)
"Shift the lines covered by `motion' rightwards."
(goto-line (vim:motion-first-line motion))
(indent-rigidly (line-beginning-position)
(line-end-position (vim:motion-line-count motion))
vim:shift-width))
(vim:defcmd vim:cmd-toggle-case (motion)
"Toggles the case of all characters defined by `motion'."
(vim:change-case motion
#'(lambda (beg end)
(save-excursion
(goto-char beg)
(while (< beg end)
(let ((c (following-char)))
(delete-char 1 nil)
(insert-char (if (eq c (upcase c)) (downcase c) (upcase c)) 1)
(setq beg (1+ beg))))))))
(vim:defcmd vim:cmd-make-upcase (motion)
"Upcases all characters defined by `motion'."
(vim:change-case motion #'upcase-region))
(vim:defcmd vim:cmd-make-downcase (motion)
"Downcases all characters defined by `motion'."
(vim:change-case motion #'downcase-region))
(defun vim:change-case (motion case-func)
(case (vim:motion-type motion)
('block
(do ((l (vim:motion-first-line motion) (1+ l)))
((> l (vim:motion-last-line motion)))
(funcall case-func
(save-excursion
(goto-line l)
(move-to-column (vim:motion-first-col motion))
(point))
(save-excursion
(goto-line l)
(move-to-column (vim:motion-last-col motion))
(1+ (point))))))
('linewise
(save-excursion
(funcall case-func (vim:motion-begin-pos motion) (vim:motion-end-pos motion))))
(t
(funcall case-func (vim:motion-begin-pos motion) (vim:motion-end-pos motion))
(goto-char (vim:motion-end-pos motion)))))
(vim:defcmd vim:cmd-repeat (nonrepeatable)
"Repeats the last command."
(unless vim:repeat-events
(error "Nothing to repeat"))
(vim:reset-key-state)
;;(dotimes (i (or count 1))
(let ((repeat-events vim:repeat-events)
(vim:repeat-events nil))
(execute-kbd-macro repeat-events)))
(vim:defcmd vim:cmd-emacs (nonrepeatable)
"Switches to Emacs for the next command."
(let (message-log-max) (message "Switch to Emacs for the next command."))
(vim:escape-to-emacs nil))
(vim:defcmd vim:cmd-write-and-close (nonrepeatable)
"Saves the current buffer and closes the window."
(save-buffer)
(condition-case nil
(delete-window)
(error (condition-case nil
(delete-frame)
(error (save-buffers-kill-emacs))))))
(vim:defcmd vim:cmd-set-mark ((argument:char mark-char) nonrepeatable)
"Sets the mark `mark-char' at point."
(vim:set-mark mark-char))
(vim:defcmd vim:cmd-show-marks (nonrepeatable (argument marks))
"Shows all currently defined marks."
(let ((all-marks (append vim:local-marks-alist vim:global-marks-alist)))
(when marks
(setq all-marks (remove-if-not #'(lambda (x) (find (car x) marks)) all-marks)))
(setq all-marks (sort all-marks #'(lambda (x y) (< (car x) (car y)))))
(setq all-marks (apply #'concat
(mapcar
#'(lambda (m)
(format "%3c %5d %3d %s\n"
(car m)
(line-number-at-pos (cdr m))
(save-excursion
(goto-char (cdr m))
(current-column))
(buffer-substring-no-properties
(save-excursion
(goto-char (cdr m))
(line-beginning-position))
(+ 20
(save-excursion
(goto-char (cdr m))
(line-beginning-position))))))
all-marks)))
(let (message-truncate-lines message-log-max)
(message "%4s %5s %3s %s\n%s" "Mark" "Line" "Col" "File/Text"
all-marks))))
(vim:deflocalvar vim:current-macro nil
"The name of the currently recorded macro.")
(vim:defcmd vim:cmd-toggle-macro-recording ((argument:char reg) nonrepeatable)
"Toggles recording of a keyboard macro."
(if reg
(progn
(put 'vim:cmd-toggle-macro-recording 'argument nil)
(vim:cmd-start-macro reg))
(progn
(put 'vim:cmd-toggle-macro-recording 'argument 'char)
(vim:cmd-stop-macro))))
(defun vim:cmd-start-macro (reg)
"Starts recording a macro in register `reg'."
(setq vim:current-macro reg)
(start-kbd-macro nil)
(let (message-log-max)
(message "Start recording keyboard macro in register '%c'" reg)))
(defun vim:cmd-stop-macro ()
"Stops recording of a macro."
(end-kbd-macro)
(let (message-log-max)
(message "Stop recording keyboard macro in register '%c'" vim:current-macro))
(set-register vim:current-macro last-kbd-macro))
(vim:defcmd vim:cmd-execute-macro (count nonrepeatable (argument:char reg))
"Executes the keyboard-macro in register `reg.'"
(vim:reset-key-state)
(execute-kbd-macro (vim:get-register reg) count))
(provide 'vim-commands)
;;; vim-commands.el ends here