;;; vim-commands.el - Implementation of VIM commands. ;; Copyright (C) 2009, 2010 Frank Fischer ;; Author: Frank Fischer , ;; ;; 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