;;; vim-motions.el - Implementation of VIM motions. ;; Copyright (C) 2009, 2010 Frank Fischer ;; Author: Frank Fischer , ;; ;; This file is not part of GNU Emacs. ;;; Commentary: ;; Motions describe functions moving the cursor or representing an ;; argument for an operator. There are three types of motions: ;; character-wise, line-wise and block-wise. Usually only the first ;; two types are represented by motion-commands while the last one is ;; implicitly used by visual-block-mode. ;; ;; A motion is defined using the macro 'vim:defmotion' which has the ;; following form: ;; ;; (vim:defmotion name (count ;; argument[:{char}] ;; {inclusive,exclusive,linewise,block}) ;; 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 motion which ;; is usually the number how often the motion should be repeated. This ;; argument may be nil if no count is given. ;; ;; 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:motion-find' usually bound to ;; 'f' for an example), otherwise it's a string-argument. Currently all ;; motions taking an argument take a character-argument. ;; ;; One if the pseudo-arguments INCLUSIVE, EXCLUSIVE, LINEWISE and BLOCK ;; must be given and specifies the type of the motion. See the Vim-manual ;; for an explanation of motion-types. ;; ;; If you do not like the default argument names, they may be renamed by using ;; (ARG NEWNAME) instead of ARG, e.g. ;; ;; (vim:defmotion vim:motion-find (inclusive count (argument:char arg)) ;; ;; defines an inclusive motion with a count-argument but renames the ;; character-argument to ARG. ;; ;; Each motion should return an object of type `vim:motion'. This may happen ;; in one of two ways: explicit or implicit. ;; ;; Explicit: The function creates an object of type `vim:motion' using ;; `vim:make-motion' specifing the begin position, the end position ;; and the type of the motion (overriding the motion-type specified in ;; the argument list). If the motion is a usual motion, the vim:motion ;; parameter :has-begin should be nil, if it's a text-objects it ;; should be t. The difference is that text-objects actively define a ;; range from the begin-position to the end-position, while ;; conventional motions define only the end-position placing begin at ;; (point). The motion should also change (point) usually to the ;; end-position of the returned motion. ;; ;; Implicit: Creating an explicit `vim:motion' object is overkill for ;; most simple motions. If the motion does not return a `vim:motion' ;; object, its created implicitly with the following rules: ;; - the begin-position is set to (point) before the execution of motion's ;; body ;; - the end-position is set to (point) after the execution of motion's ;; body ;; - :has-begin is nil ;; - the type is the type defined in the motion's argument list ;; Almost all motions defined in this file are implicit. ;; ;; Note that, independently on whether the motion is defined ;; implicitly or explicitly, calling a motion always returns a ;; `vim:motion' object, i.e. (vim:motion-p (vim:motion-left)) would ;; return t. ;; ;; Motions can be bound to some key-sequence as any other interactive ;; Emacs function, but they work only in vim-mode. Ususally motions ;; are bound to the operator-pending-mode keymap using `vim:omap'. ;;; Code: (vim:deflocalvar vim:this-column nil "The resulting column of the current motion.") (vim:deflocalvar vim:last-column nil "The resulting column of the previous motion.") (vim:deflocalvar vim:last-find nil "The previous find command (command . arg).") (defcustom vim:word "[:word:]" "Regexp-set matching a word." :type 'string :group 'vim-mode) (defcustom vim:whitespace " \t\r\n" "Regexp-set matching a whitespace." :type 'string :group 'vim-mode) (defun vim:adjust-point () "Adjust the pointer after a command." ;; TODO: should we check modes directly? (when (and (not (vim:insert-mode-p)) ) ;(not vim:replace-mode)) (when vim:this-column (move-to-column vim:this-column)) ;; always stop at the last character (not the newline) (when (and (not (vim:visual-mode-p)) (eolp) (not (bolp))) (backward-char))) (setq vim:last-column (or vim:this-column (current-column))) (setq vim:this-column nil)) (defun vim:use-last-column () "This function should by called by a motion not changing the column." (setq vim:this-column vim:last-column)) ;; This structure is passed to operators taking a motion. ;; It should *not* be returned by motions. (defstruct (vim:motion (:constructor vim:make-motion-struct)) has-begin ; t iff the motion defined an explicit begin begin ; first point in this motion end ; last point in this motion type ; 'inclusive, 'exclusive, 'linewise ) (defun* vim:make-motion (&key has-begin (begin (point)) (end (point)) type) "Creates a new motion with `begin' and `end' always positions within (point-min) and (point-max) and not at (line-end-position) (if possible)." (unless type (setq type (if (<= begin end) 'inclusive 'exclusive))) (labels ((shrink-to (pos lower upper) (max lower (min upper pos))) (normalize-pos (pos) (let ((pos (shrink-to pos (point-min) (point-max)))) (shrink-to pos (save-excursion (goto-char pos) (line-beginning-position)) (save-excursion (goto-char pos) (- (line-end-position) (if (eq type 'inclusive) 1 0))))))) (vim:make-motion-struct :has-begin has-begin :begin (normalize-pos begin) :end (normalize-pos end) :type type))) (defun vim:motion-line-count (motion) "Returns the number of lines the `motion' covers." (1+ (- (vim:motion-last-line motion) (vim:motion-first-line motion)))) (defun vim:motion-first-line (motion) "Returns the first line covered by `motion'." (min (line-number-at-pos (vim:motion-begin motion)) (line-number-at-pos (vim:motion-end motion)))) (defun vim:motion-last-line (motion) "Returns the last line covered by `motion'." (max (line-number-at-pos (vim:motion-begin motion)) (line-number-at-pos (vim:motion-end motion)))) (defun vim:motion-first-col (motion) "Returns the first column covered by `motion'." (min (save-excursion (goto-char (vim:motion-begin motion)) (current-column)) (save-excursion (goto-char (vim:motion-end motion)) (current-column)))) (defun vim:motion-last-col (motion) "Returns the last column covered by `motion'." (max (save-excursion (goto-char (vim:motion-begin motion)) (current-column)) (save-excursion (goto-char (vim:motion-end motion)) (current-column)))) (defun vim:motion-begin-pos (motion) "Returns the smaller position covered by `motion'. The result is modified depending on the motion type to return the correct start-position of emacs-ranges, i.e. - if motion is inclusive or exclusive, nothing is changed - if motion is line-wise, is always bol of the first line in the motion, - if motion is block 1 is added if and only if the begin column is larget than the end column." (case (vim:motion-type motion) (linewise (save-excursion (goto-line (vim:motion-first-line motion)) (line-beginning-position))) ('block (let ((b (min (vim:motion-begin motion) (vim:motion-end motion))) (e (max (vim:motion-begin motion) (vim:motion-end motion)))) (if (> (save-excursion (goto-char b) (current-column)) (save-excursion (goto-char e) (current-column))) (1+ b) b))) (t (min (vim:motion-begin motion) (vim:motion-end motion))))) (defun vim:motion-end-pos (motion) "Returns the larger position covered by `motion'. The result is modified depending on the motion type to return the correct end-position of emacs-ranges, i.e. - if motion is inclusive, 1 is added, - if motion is exclusive, nothing is change, - if motion is line-wise, is always eol of the last line in the motion, - if motion is block 1 is added if and only if the end column is larger than or equal to the begin column." (case (vim:motion-type motion) (linewise (save-excursion (goto-line (vim:motion-last-line motion)) (line-end-position))) ('block (let ((b (min (vim:motion-begin motion) (vim:motion-end motion))) (e (max (vim:motion-begin motion) (vim:motion-end motion)))) (if (>= (save-excursion (goto-char e) (current-column)) (save-excursion (goto-char b) (current-column))) (1+ e) e))) (inclusive (1+ (max (vim:motion-begin motion) (vim:motion-end motion)))) (t (max (vim:motion-begin motion) (vim:motion-end motion))))) (defmacro vim:do-motion (type expression) "Executes a motion body, ensuring the return of a valid vim:motion object." (let ((current-pos (gensym)) (motion (gensym))) `(let* ((,current-pos (point)) (,motion ,expression)) (if (vim:motion-p ,motion) ,motion (vim:make-motion :has-begin nil :begin ,current-pos :end (point) :type ,type))))) (font-lock-add-keywords 'emacs-lisp-mode '("vim:do-motion")) (vim:deflocalvar vim:local-marks-alist nil "Local marks for this buffer.") (defvar vim:global-marks-alist nil "Global marks.") (defun vim:local-mark-p (mark-char) "Returns t if `mark-char' is a local mark." (or (and (>= mark-char ?a) (<= mark-char ?z)) (member mark-char '(?^ ?. ?< ?>)))) (defun vim:global-mark-p (mark-char) "Returns t if `mark-char' is a global mark." (and (>= mark-char ?A) (<= mark-char ?z))) (defun vim:set-mark (mark-char &optional pos) "Sets the mark `mark-char' to `pos' or (point)." (let (m) (cond ((vim:local-mark-p mark-char) (setq m (or (cdr-safe (assoc mark-char vim:local-marks-alist)))) (unless m (setq m (make-marker)) (push (cons mark-char m) vim:local-marks-alist))) ((vim:global-mark-p mark-char) (setq m (or (cdr-safe (assoc mark-char vim:global-marks-alist)))) (unless m (setq m (make-marker)) (push (cons mark-char m) vim:global-marks-alist))) (t (error "Unknown mark '%c'" mark-char))) (set-marker m (or pos (point))))) (defun vim:get-local-mark (mark-char) "Returns the marker of `mark-char' if it's in the current buffer." (cond ((vim:local-mark-p mark-char) (let ((m (cdr-safe (assoc mark-char vim:local-marks-alist)))) (if m m (error "No mark '%c' defined." mark-char)))) ((vim:global-mark-p mark-char) (let ((m (cdr-safe (assoc mark-char vim:global-marks-alist)))) (if m (if (eq (marker-buffer m) (current-buffer)) m (error "Global mark '%c' not in current buffer." mark-char)) (error "No mark '%c' defined." mark-char)))) (t (error "Unknown mark: '%c'" mark-char)))) (add-hook 'before-change-functions 'vim:set-change-mark) (defun vim:set-change-mark (beg end) "Sets the change mark . to `beg'." (vim:set-mark ?. beg)) (defun vim:adjust-end-of-line-position (pos) "If pos is an end-of-line returns pos - 1 and pos otherwise." (save-excursion (goto-char pos) (max (line-beginning-position) (min (1- (line-end-position)) pos)))) (vim:defmotion vim:motion-left (exclusive count) "Move the cursor count characters left." (goto-char (max (line-beginning-position) (- (point) (or count 1))))) (vim:defmotion vim:motion-right (exclusive count) "Move the cursor count characters right." (goto-char (min (line-end-position) (+ (point) (or count 1))))) (vim:defmotion vim:motion-up (linewise count) "Move the cursor count lines up." (vim:use-last-column) (forward-line (- (or count 1)))) (vim:defmotion vim:motion-down (linewise count) "Move the cursor count lines down." (vim:use-last-column) (forward-line (or count 1))) (vim:defmotion vim:motion-lines (linewise count) "Moves count - 1 lines down." (vim:use-last-column) (forward-line (1- (or count 1)))) (defun vim:motion-beginning-of-line-or-digit-argument () "Feeds a 0 count or moves the cursor to the beginning of the line." (interactive) (if (and current-prefix-arg (not (zerop (prefix-numeric-value current-prefix-arg)))) (call-interactively 'digit-argument) (call-interactively 'vim:motion-beginning-of-line))) (vim:defmotion vim:motion-beginning-of-line (exclusive) "Move the cursor to the beginning of the current line." (beginning-of-line)) (vim:defmotion vim:motion-first-non-blank (exclusive) "Move the cursor to the first non-blank character of the current line." (back-to-indentation)) (vim:defmotion vim:motion-end-of-line (inclusive count) "Move the cursor to the end of the current line." (end-of-line count)) (vim:defmotion vim:motion-last-non-blank (inclusive count) "Move the cursor to the last non-blank charactor of the current line." (goto-char (save-excursion (beginning-of-line count) (re-search-forward "[ \t]*$") (max (line-beginning-position) (1- (match-beginning 0)))))) (vim:defmotion vim:motion-go-to-first-non-blank-beg (linewise count) "Moves the cursor to the first non-blank charactor of line count." (if count (goto-line count) (goto-char (point-min))) (vim:motion-first-non-blank)) (vim:defmotion vim:motion-go-to-first-non-blank-end (linewise count) "Moves the cursor to the first non-blank charactor of line count." (if count (goto-line count) (goto-char (point-max))) (vim:motion-first-non-blank)) (vim:defmotion vim:motion-fwd-word (exclusive count) "Moves the cursor beginning of the next word." (let ((word (concat "[" vim:word "]")) (noword (concat "[^" vim:word "]")) (nonword (concat "[^" vim:whitespace vim:word "]")) (nononword (concat "[" vim:whitespace vim:word "]"))) (dotimes (i (or count 1)) (forward-char) (while (not (or (and (looking-back noword) (looking-at word)) (and (looking-back nononword) (looking-at nonword)) (and (bolp) (eolp)) (eobp))) (forward-char)))) ;; in operator-pending mode, if we reached the beginning of a new ;; line, go back to the end of the previous line (when (and (vim:operator-pending-mode-p) (vim:looking-back "^[ \t]*") (not (save-excursion (forward-line -1) (and (bolp) (eolp))))) (forward-line -1) (end-of-line))) (vim:defmotion vim:motion-bwd-word (exclusive count) "Moves the cursor beginning of the previous word." (let ((word (concat "[" vim:word "]")) (noword (concat "[^" vim:word "]")) (nonword (concat "[^" vim:whitespace vim:word "]")) (nononword (concat "[" vim:whitespace vim:word "]"))) (dotimes (i (or count 1)) (backward-char) (while (not (or (and (looking-back noword) (looking-at word)) (and (looking-back nononword) (looking-at nonword)) (and (bolp) (eolp)) (eobp))) (backward-char))))) (vim:defmotion vim:motion-fwd-word-end (inclusive count) "Moves the cursor to the end of the next word." (let ((wordend (concat "[" vim:word "][^" vim:word "]")) (nowordend (concat "[^" vim:whitespace vim:word "][" vim:whitespace vim:word "]"))) (forward-char) (re-search-forward (concat wordend "\\|" nowordend "\\|\\'") nil nil (or count 1)) (goto-char (match-beginning 0)))) (vim:defmotion vim:motion-bwd-word-end (inclusive count) "Moves the cursor to the end of the previous word." (let ((wordend (concat "[" vim:word "][^" vim:word "]")) (nowordend (concat "[^" vim:whitespace vim:word "][" vim:whitespace vim:word "]"))) (unless (eobp) (forward-char)) (re-search-backward (concat wordend "\\|" nowordend "\\|\\`") nil nil (or count 1)) (goto-char (match-beginning 0)))) (vim:defmotion vim:motion-fwd-WORD (exclusive count) "Moves the cursor to beginning of the next WORD." (let ((WORD (concat "[^" vim:whitespace "]")) (noWORD (concat "[" vim:whitespace "]"))) (dotimes (i (or count 1)) (forward-char) (while (not (or (and (looking-back noWORD) (looking-at WORD)) (and (bolp) (eolp)) (eobp))) (forward-char)))) ;; in operator-pending mode, if we reached the beginning of a new ;; line, go back to the end of the previous line (when (and (vim:operator-pending-mode-p) (vim:looking-back "^[ \t]*") (not (save-excursion (forward-line -1) (and (bolp) (eolp))))) (forward-line -1) (end-of-line))) (vim:defmotion vim:motion-bwd-WORD (exclusive count) "Moves the cursor to beginning of the previous WORD." (let ((WORD (concat "[^" vim:whitespace "]")) (noWORD (concat "[" vim:whitespace "]"))) (dotimes (i (or count 1)) (backward-char) (while (not (or (and (looking-back noWORD) (looking-at WORD)) (and (bolp) (eolp)) (eobp))) (backward-char))))) (vim:defmotion vim:motion-fwd-WORD-end (inclusive count) "Moves the cursor to the end of the next WORD." (let ((WORDend (concat "[^" vim:whitespace "][" vim:whitespace "]"))) (forward-char) (re-search-forward (concat WORDend "\\|\\'") nil nil (or count 1)) (goto-char (match-beginning 0)))) (vim:defmotion vim:motion-bwd-WORD-end (inclusive count) "Moves the cursor to the end of the next WORD." (let ((WORDend (concat "[^" vim:whitespace "][" vim:whitespace "]"))) (unless (eobp) (forward-char)) (re-search-backward (concat WORDend "\\|\\'") nil nil (or count 1)) (goto-char (match-beginning 0)))) (vim:defmotion vim:motion-fwd-sentence (exclusive count) "Move the cursor `count' sentences forward." (dotimes (i (or count 1)) (let ((par-end (save-excursion (forward-paragraph) (point)))) (unless (re-search-forward (sentence-end) par-end t) (goto-char par-end))))) (vim:defmotion vim:motion-bwd-sentence (exclusive count) "Move the cursor `count' sentences backward." (dotimes (i (or count 1)) (let ((par-beg (save-excursion (backward-paragraph) (point)))) (if (re-search-backward (concat (sentence-end) "[^ \t\n]") par-beg t) (goto-char (1- (match-end 0))) (goto-char par-beg))))) (vim:defmotion vim:motion-fwd-paragraph (exclusive count) "Move the cursor `count' paragraphs forward." (forward-paragraph (or count 1))) (vim:defmotion vim:motion-bwd-paragraph (exclusive count) "Move the cursor `count' paragraphs backward." (backward-paragraph (or count 1))) (vim:defmotion vim:motion-find (inclusive count (argument:char arg)) "Move the cursor to the next count'th occurrence of arg." (forward-char) (let ((case-fold-search nil)) (unless (search-forward (char-to-string arg) nil t (or count 1)) (backward-char) (error (format "Can't find %c" arg))) (setq vim:last-find (cons 'vim:motion-find arg)) (backward-char))) (vim:defmotion vim:motion-find-back (exclusive count (argument:char arg)) "Move the cursor to the previous count'th occurrence of arg." (let ((case-fold-search nil)) (unless (search-backward (char-to-string arg) nil t (or count 1)) (error (format "Can't find %c" arg))) (setq vim:last-find (cons 'vim:motion-find-back arg)))) (vim:defmotion vim:motion-find-to (inclusive count (argument:char arg)) "Move the cursor to the character before the next count'th\ occurence of arg." (vim:motion-find :count count :argument arg) (backward-char) (setq vim:last-find (cons 'vim:motion-find-to arg))) (vim:defmotion vim:motion-find-back-to (exclusive count (argument:char arg)) "Move the cursor to the character after the previous count'th\ occurence of arg." (vim:motion-find-back :count count :argument arg) (forward-char) (setq vim:last-find (cons 'vim:motion-find-to arg))) (vim:defmotion vim:motion-repeat-last-find (inclusive count) "Repeats the last find command." (unless vim:last-find (error "No previous find command.")) (funcall (car vim:last-find) :count count :argument (cdr vim:last-find))) (vim:defmotion vim:motion-repeat-last-find-opposite (inclusive count) "Repeats the last find command." (unless vim:last-find (error "No previous find command.")) (let ((func (case (car vim:last-find) ('vim:motion-find 'vim:motion-find-back) ('vim:motion-find-back 'vim:motion-find) ('vim:motion-find-to 'vim:motion-find-back-to) ('vim:motion-find-back-to 'vim:motion-find-to) (t (error (format "Unexpected find command %s" (car vim:last-find)))))) (arg (cdr vim:last-find))) (let ((vim:last-find nil)) (funcall func :count count :argument arg)))) (vim:defmotion vim:motion-jump-item (inclusive) "Find the next item in this line after or under the cursor and jumps to the corresponding one." (let ((next-open (condition-case err (1- (scan-lists (point) 1 -1)) (error (point-max)))) (next-close (condition-case nil (1- (scan-lists (point) 1 +1)) (error (point-max))))) (let ((pos (min next-open next-close))) (when (>= pos (line-end-position)) (error "No matching item found on the current line.")) (if (= pos next-open) (progn (goto-char pos) (forward-list) (backward-char)) (progn (goto-char (1+ pos)) (backward-list)))))) (vim:defmotion vim:motion-inner-word (inclusive count) "Select `count' words." (let ((beg (save-excursion (forward-char) (vim:motion-bwd-word) (point))) (end (save-excursion (backward-char) (vim:motion-fwd-word-end :count count) (point)))) (goto-char end) (vim:make-motion :has-begin t :begin beg :end end :type 'inclusive))) (vim:defmotion vim:motion-mark (exclusive (argument:char mark-char)) "Moves to the position of `mark-char'." (goto-char (vim:get-local-mark mark-char))) (vim:defmotion vim:motion-mark-line (linewise (argument:char mark-char)) "Moves to the first non-blank char in the line of `mark-char'." (goto-char (vim:get-local-mark mark-char)) (vim:motion-first-non-blank) t) (provide 'vim-motions) ;;; vim-motions.el ends here