1 ;;; plomvi.el --- poor man's vim emulation
5 ;; Author: Christian Heller <plom+plomvi@plomlompom.com>
7 ;; This program is free software; you can redistribute it and/or modify
8 ;; it under the terms of the GNU General Public License as published by
9 ;; the Free Software Foundation, either version 3 of the License, or
10 ;; (at your option) any later version.
12 ;; This program is distributed in the hope that it will be useful,
13 ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
14 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 ;; GNU General Public License for more details.
17 ;; You should have received a copy of the GNU General Public License
18 ;; along with this program. If not, see <http://www.gnu.org/licenses/>.
22 ;; A very imperfect simulation of a small subset of Vim behavior, far
23 ;; from the sophistication of packages like evil; an intentionally
24 ;; thin wrapper around native Emacs behavior instead. To the author,
25 ;; it mostly serves to both avoid muscle memory context switches
26 ;; between Emacs and most basic Vim usages, and at the same time avoid
27 ;; the conceptual distances evil puts between the user and native
28 ;; Emacs logic. It does not care though to keep default Emacs
29 ;; keybindings intact, overwriting some common ones with those from
34 ;; 1. load this script into your init file: (load ...)
36 ;; 2. to start plomvi by default, put this into your init file:
37 ;; (plomvi-global-mode 1)
39 ;; 3. define some otherwise unused keybinding to simulate what would
40 ;; be a jump back from Insert mode to Normal mode in Vim (but is de
41 ;; facto just a plomvi mode activation), such as this:
42 ;; (global-set-key (kbd "<f1>") 'plomvi-activate)
46 (defun plomvi-half-scroll()
47 "Scroll down half a screen width."
49 (scroll-up (/ (window-height) 2)))
51 (defun plomvi-goto-line (count)
52 "Jump to line: on nil count, last one, else count."
55 (goto-char (point-max))
56 (goto-char (point-min))
57 (forward-line (1- count))))
59 (defun plomvi-prefix-zero-or-line-start (prev-prefix)
60 "If no prefix so far, jump to start of line, else start new prefix with 0."
62 (if (null prev-prefix)
66 (defun plomvi-prompt (prompt-input)
67 "Provide super-basic : prompt that only accepts:
76 If search and replace syntax is detected, it recommends using `query-replace'
81 ((string= prompt-input "q!")
83 ((string= prompt-input "q")
84 (save-buffers-kill-emacs))
85 ((string= prompt-input "w")
87 ((string-match "^w [^ ]+" prompt-input)
88 (let ((file-name (substring prompt-input 2))) (write-file file-name)))
89 ((string-match "^%?s/" prompt-input)
90 (message "NOT IMPLEMENTED, consider using query-replace(-regexp)"))
91 ((string= prompt-input "wq")
92 ((lambda () (save-some-buffers t) (kill-emacs))))
93 ((string= prompt-input "vsplit")
94 (split-window-horizontally))
95 ((string= prompt-input "split")
96 (split-window-vertically))
97 (t (message "NOT IMPLEMENTED"))))
99 (defun plomvi-newline-above ()
100 "Open and jump into new line above current line, deactivate `plomvi-mode'."
107 (defun plomvi-newline-below ()
108 "Open and jump into new line below current line, deactivate `plomvi-mode'."
114 (defun plomvi-paste-backward ()
115 "Paste last kill leftwards in current line, or (if kill ends in \n) above it.
117 Note that this ignores killed rectangles.
120 (if (eq nil (string-match "\n$" (current-kill 0)))
126 (defun plomvi-paste-forward ()
127 "Paste last kill rightwards in current line, or (if kill ends in \n) under it.
129 Note that this ignores killed rectangles."
131 (if (eq nil (string-match "\n$" (current-kill 0)))
140 (defun plomvi-affect-lines-of-region(f)
141 "Call f on start of first line of region and end of last line of region."
142 (let* ((start-start-pos (region-beginning))
143 (start-end-pos (region-end))
145 (goto-char start-start-pos)
146 (line-beginning-position)))
148 (goto-char start-end-pos)
149 (+ 1 (line-end-position)))))
150 (funcall f region-start region-end)
151 (goto-char region-start)))
153 (defun plomvi-kill-region-lines()
154 "Kill lines of marked region."
156 (plomvi-affect-lines-of-region 'kill-region))
159 "If rectangle or region marked, kill those; else, kill char after point."
162 ((and (boundp 'rectangle-mark-mode) (eq t rectangle-mark-mode))
163 (kill-rectangle (region-beginning) (region-end)))
165 (kill-region (region-beginning) (region-end)))
166 ((not (= (line-beginning-position) (line-end-position)))
168 (if (not (= (point) (line-beginning-position))) (backward-char)))))
170 (defun plomvi-rectangle-mark()
171 "Start marked rectangle, move right one char so a single column is visible."
173 (push-mark (point) nil t)
174 (rectangle-mark-mode)
177 (defun plomvi-search-forward()
178 "Find next occurence of search string last targeted by isearch."
180 (search-forward isearch-string))
182 (defun plomvi-search-backward()
183 "Find previous occurence of search string last targeted by isearch."
185 (search-backward isearch-string))
188 "Copy rectangle, or full line, or region in full lines."
191 ((and (boundp 'rectangle-mark-mode) (eq t rectangle-mark-mode))
192 (copy-rectangle-as-kill (region-beginning) (region-end)))
194 (plomvi-affect-lines-of-region 'copy-region-as-kill))
196 (copy-region-as-kill (line-beginning-position) (+ 1 (line-end-position))))))
198 (defun plomvi-copy-region()
199 "Copy marked region."
201 (copy-region-as-kill (region-beginning) (region-end)))
203 (defun plomvi-replace-char (c)
204 "Replace char after point with c."
205 (interactive "cplomvi-replace-char")
206 (delete-char 1) (insert-char c) (left-char))
208 (defun plomvi-no-redo()
209 "Tell user what to do, since implementing vim redo was too much of a hassle."
211 (message "Vim-style redo not available. Try M-x for Emacs' undo-undo."))
213 (defun plomvi-activate()
214 "Activate `plomvi-mode'."
218 (defun plomvi-deactivate()
219 "Deactivate `plomvi-mode'."
223 (defun plomvi-end-of-line()
224 "Move to end of line exclusive line break char."
227 (if (not (= (point) (line-beginning-position))) (backward-char)))
229 (defvar plomvi-mode-basic-map (make-sparse-keymap)
230 "Keymap for `plomvi-mode' on read-only buffers.
232 In contrast to the keymap `plomvi-editable-mode' for editable
233 buffers, this excludes keybindings for editing text, which thus
234 become available to be used for other purposes.")
235 (suppress-keymap plomvi-mode-basic-map t)
236 (define-key plomvi-mode-basic-map (kbd ":") 'plomvi-prompt)
237 (define-key plomvi-mode-basic-map (kbd "C-w") 'other-window)
238 (define-key plomvi-mode-basic-map (kbd "k") 'previous-line)
239 (define-key plomvi-mode-basic-map (kbd "j") 'next-line)
240 (define-key plomvi-mode-basic-map (kbd "h") 'left-char)
241 (define-key plomvi-mode-basic-map (kbd "l") 'right-char)
242 (define-key plomvi-mode-basic-map (kbd "w") 'forward-word)
243 (define-key plomvi-mode-basic-map (kbd "b") 'backward-word)
244 (define-key plomvi-mode-basic-map (kbd "/") 'isearch-forward)
245 (define-key plomvi-mode-basic-map (kbd "N") 'plomvi-search-backward)
246 (define-key plomvi-mode-basic-map (kbd "n") 'plomvi-search-forward)
247 (define-key plomvi-mode-basic-map (kbd "v") 'set-mark-command)
248 (define-key plomvi-mode-basic-map (kbd "C-v") 'plomvi-rectangle-mark)
249 (define-prefix-command 'plomvi-g-map)
250 (define-key plomvi-mode-basic-map (kbd "g") 'plomvi-g-map)
251 (define-key plomvi-g-map (kbd "g") 'beginning-of-buffer)
252 (define-key plomvi-mode-basic-map (kbd "G") 'plomvi-goto-line)
253 (define-key plomvi-mode-basic-map (kbd "$") 'plomvi-end-of-line)
254 (define-key plomvi-mode-basic-map (kbd "0") 'plomvi-prefix-zero-or-line-start)
255 (define-key plomvi-mode-basic-map (kbd "1") 'digit-argument)
256 (define-key plomvi-mode-basic-map (kbd "2") 'digit-argument)
257 (define-key plomvi-mode-basic-map (kbd "3") 'digit-argument)
258 (define-key plomvi-mode-basic-map (kbd "4") 'digit-argument)
259 (define-key plomvi-mode-basic-map (kbd "5") 'digit-argument)
260 (define-key plomvi-mode-basic-map (kbd "6") 'digit-argument)
261 (define-key plomvi-mode-basic-map (kbd "7") 'digit-argument)
262 (define-key plomvi-mode-basic-map (kbd "8") 'digit-argument)
263 (define-key plomvi-mode-basic-map (kbd "9") 'digit-argument)
264 (define-key plomvi-mode-basic-map (kbd "C-b") 'scroll-down)
265 (define-key plomvi-mode-basic-map (kbd "C-f") 'scroll-up)
266 (define-key plomvi-mode-basic-map (kbd "C-d") 'plomvi-half-scroll)
268 (defvar plomvi-mode-editable-map (make-sparse-keymap)
269 "Keymap for `plomvi-mode' on editable buffers.
271 Inherits from `plomvi-mode-basic-map', but adds keybindings for
273 (set-keymap-parent plomvi-mode-editable-map plomvi-mode-basic-map)
274 (define-key plomvi-mode-editable-map (kbd "i") 'plomvi-deactivate)
275 (define-key plomvi-mode-editable-map (kbd "x") 'plomvi-x)
276 (define-key plomvi-mode-editable-map (kbd "o") 'plomvi-newline-below)
277 (define-key plomvi-mode-editable-map (kbd "O") 'plomvi-newline-above)
278 (define-key plomvi-mode-editable-map (kbd "r") 'plomvi-replace-char)
279 (define-key plomvi-mode-editable-map (kbd "u") 'undo-only)
280 (define-key plomvi-mode-editable-map (kbd "C-r") 'plomvi-no-redo)
281 (define-key plomvi-mode-editable-map (kbd "I") 'string-insert-rectangle)
282 (define-key plomvi-mode-editable-map (kbd "p") 'plomvi-paste-forward)
283 (define-key plomvi-mode-editable-map (kbd "P") 'plomvi-paste-backward)
284 (define-key plomvi-mode-editable-map (kbd "Y") 'plomvi-Y)
285 (define-key plomvi-mode-editable-map (kbd "y") 'plomvi-copy-region)
286 (define-key plomvi-mode-editable-map (kbd "D") 'plomvi-kill-region-lines)
287 (define-prefix-command 'plomvi-d-map)
288 (define-key plomvi-mode-editable-map (kbd "d") 'plomvi-d-map)
289 (define-key plomvi-d-map (kbd "w") 'kill-word)
290 (define-key plomvi-d-map (kbd "$") 'kill-line)
291 (define-key plomvi-d-map (kbd "d") 'kill-whole-line)
292 (defvar plomvi-mode-hook)
293 (defvar plomvi-mode-basic-hook)
294 (defvar plomvi-mode-editable-hook)
295 (defvar plomvi-mode-disable-hook)
296 (defvar plomvi-mode-basic-disable-hook)
297 (defvar plomvi-mode-editable-disable-hook)
298 (defvar-local plomvi-mode nil "mode variable for `plomvi-mode'")
299 (defvar-local plomvi-mode-basic nil
300 "toggles `plomvi-mode-basic-map' in `minor-mode-map-alist' for `plomvi-mode'")
301 (defvar-local plomvi-mode-editable nil
302 "toggles `plomvi-mode-editable-map' in `minor-mode-map-alist' for `plomvi-mode'")
304 (defun plomvi-mode (&optional arg)
305 "Imperfectly emulates a subset of Vim's Normal mode.
307 Sets mode variable `plomvi-mode' and, on read-only buffers, `plomvi-mode-basic',
308 or, on editable buffers, `plomvi-mode-editable'. The latter two's values in
309 `minor-mode-map-alist' toggle either `plomvi-mode-basic-map' or
310 `plomvi-mode-editable-map'."
311 (interactive (list (or current-prefix-arg 'toggle)))
312 (let ((enable (if (eq arg 'toggle) ; follow suggestions
313 (not plomvi-mode) ; from (elisp)Minor
314 (> (prefix-numeric-value arg) 0 )))) ; Mode Conventions
316 (unless (minibufferp)
318 (setq plomvi-mode-basic t)
319 (setq plomvi-mode-editable t))
321 (run-hooks 'plomvi-mode-hook)
322 (if plomvi-mode-basic
323 (run-hooks 'plomvi-mode-basic-hook)
324 (run-hooks 'plomvi-mode-editable-hook)))
325 (setq plomvi-mode-editable nil
326 plomvi-mode-basic nil
328 (run-hooks 'plomvi-mode-editable-disable-hook)
329 (run-hooks 'plomvi-mode-basic-disable-hook)
330 (run-hooks 'plomvi-mode-disable-hook))))
332 (define-globalized-minor-mode plomvi-global-mode plomvi-mode plomvi-activate)
333 (add-to-list 'minor-mode-alist '(plomvi-mode " PV"))
334 (add-to-list 'minor-mode-map-alist (cons 'plomvi-mode-basic
335 plomvi-mode-basic-map))
336 (add-to-list 'minor-mode-map-alist (cons 'plomvi-mode-editable
337 plomvi-mode-editable-map))