dotfiles/.emacs.d/vim-mode/vim-motions.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

666 lines
23 KiB
EmacsLisp

;;; vim-motions.el - Implementation of VIM motions.
;; Copyright (C) 2009, 2010 Frank Fischer
;; Author: Frank Fischer <frank.fischer@mathematik.tu-chemnitz.de>,
;;
;; 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