home · contact · privacy
8dd5117f6bd1907c7e7eb09c51ad11e3aa621a53
[plomvi.el] / plomvi.el
1 ;;; plomvi.el --- poor man's vim emulation
2
3 ;; Copyright (C) 2019
4
5 ;; Author: Christian Heller <plom+plomvi@plomlompom.com>
6
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.
11
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.
16
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/>.
19
20 ;;; Commentary:
21
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
30 ;; the Vim universe.
31
32 ;;; Instructions:
33
34 ;; 1. load this script into your init file: (load ...)
35
36 ;; 2. to start plomvi by default, put this into your init file:
37 ;; (plomvi-global-mode 1)
38
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)
43
44
45
46 (defun plomvi-half-scroll()
47   "Scroll down half a screen width."
48   (interactive)
49   (scroll-up (/ (window-height) 2)))
50
51 (defun plomvi-goto-line (count)
52   "Jump to line: on nil count, last one, else count."
53   (interactive "P")
54   (if (null count)
55       (goto-char (point-max))
56     (goto-char (point-min))
57     (forward-line (1- count))))
58
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."
61   (interactive "P")
62   (if (null prev-prefix)
63       (beginning-of-line)
64     (setq prefix-arg 0)))
65
66 (defun plomvi-prompt (prompt-input)
67   "Provide super-basic : prompt that only accepts:
68 q
69 q!
70 w
71 w FILENAME
72 wq
73 vsplit
74 split
75
76 If search and replace syntax is detected, it recommends using `query-replace'
77 instead.
78 "
79   (interactive "M:")
80   (cond
81    ((string= prompt-input "q!")
82     (kill-emacs))
83    ((string= prompt-input "q")
84     (save-buffers-kill-emacs))
85    ((string= prompt-input "w")
86     (save-buffer))
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"))))
98
99 (defun plomvi-newline-above ()
100   "Open and jump into new line above current line, deactivate `plomvi-mode'."
101   (interactive)
102   (beginning-of-line)
103   (insert "\n")
104   (previous-line)
105   (plomvi-deactivate))
106
107 (defun plomvi-newline-below ()
108   "Open and jump into new line below current line, deactivate `plomvi-mode'."
109   (interactive)
110   (end-of-line)
111   (insert "\n")
112   (plomvi-deactivate))
113
114 (defun plomvi-paste-backward ()
115   "Paste last kill leftwards in current line, or (if kill ends in \n) above it.
116
117 Note that this ignores killed rectangles.
118 "
119   (interactive)
120   (if (eq nil (string-match "\n$" (current-kill 0)))
121       (yank)
122     (beginning-of-line)
123     (yank)
124     (previous-line)))
125
126 (defun plomvi-paste-forward ()
127   "Paste last kill rightwards in current line, or (if kill ends in \n) under it.
128
129 Note that this ignores killed rectangles."
130   (interactive)
131   (if (eq nil (string-match "\n$" (current-kill 0)))
132       (progn
133         (right-char)
134         (yank))
135     (end-of-line)
136     (right-char)
137     (yank)
138     (previous-line)))
139
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))
144          (region-start (progn
145                          (goto-char start-start-pos)
146                          (line-beginning-position)))
147          (region-end (progn
148                        (goto-char start-end-pos)
149                        (+ 1 (line-end-position)))))
150     (funcall f region-start region-end)
151     (goto-char region-start)))
152
153 (defun plomvi-kill-region-lines()
154   "Kill lines of marked region."
155   (interactive)
156   (plomvi-affect-lines-of-region 'kill-region))
157
158 (defun plomvi-x()
159   "If rectangle or region marked, kill those; else, kill char after point."
160   (interactive)
161   (cond
162    ((and (boundp 'rectangle-mark-mode) (eq t rectangle-mark-mode))
163     (kill-rectangle (region-beginning) (region-end)))
164    ((use-region-p)
165     (kill-region (region-beginning) (region-end)))
166    ((not (= (line-beginning-position) (line-end-position)))
167     (delete-char 1)
168     (if (not (= (point) (line-beginning-position))) (backward-char)))))
169
170 (defun plomvi-rectangle-mark()
171   "Start marked rectangle, move right one char so a single column is visible."
172   (interactive)
173   (push-mark (point) nil t)
174   (rectangle-mark-mode)
175   (right-char))
176
177 (defun plomvi-search-forward()
178   "Find next occurence of search string last targeted by isearch."
179   (interactive)
180   (search-forward isearch-string))
181
182 (defun plomvi-search-backward()
183   "Find previous  occurence of search string last targeted by isearch."
184   (interactive)
185   (search-backward isearch-string))
186
187 (defun plomvi-Y()
188   "Copy rectangle, or full line, or region in full lines."
189   (interactive)
190   (cond
191    ((and (boundp 'rectangle-mark-mode) (eq t rectangle-mark-mode))
192     (copy-rectangle-as-kill (region-beginning) (region-end)))
193    ((use-region-p)
194     (plomvi-affect-lines-of-region 'copy-region-as-kill))
195    (t
196     (copy-region-as-kill (line-beginning-position) (+ 1 (line-end-position))))))
197
198 (defun plomvi-copy-region()
199   "Copy marked region."
200   (interactive)
201   (copy-region-as-kill (region-beginning) (region-end)))
202
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))
207
208 (defun plomvi-no-redo()
209   "Tell user what to do, since implementing vim redo was too much of a hassle."
210   (interactive)
211   (message "Vim-style redo not available. Try M-x for Emacs' undo-undo."))
212
213 (defun plomvi-activate()
214   "Activate `plomvi-mode'."
215   (interactive)
216   (plomvi-mode))
217
218 (defun plomvi-deactivate()
219   "Deactivate `plomvi-mode'."
220   (interactive)
221   (plomvi-mode -1))
222
223 (defun plomvi-end-of-line()
224   "Move to end of line exclusive line break char."
225   (interactive)
226   (end-of-line)
227   (if (not (= (point) (line-beginning-position))) (backward-char)))
228
229 (defvar plomvi-mode-basic-map (make-sparse-keymap)
230   "Keymap for `plomvi-mode' on read-only buffers.
231
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)
267
268 (defvar plomvi-mode-editable-map (make-sparse-keymap)
269   "Keymap for `plomvi-mode' on editable buffers.
270
271 Inherits from `plomvi-mode-basic-map', but adds keybindings for
272 text editing.")
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'")
303
304 (defun plomvi-mode (&optional arg)
305   "Imperfectly emulates a subset of Vim's Normal mode.
306
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
315     (if enable
316         (unless (minibufferp)
317           (if buffer-read-only
318               (setq plomvi-mode-basic t)
319             (setq plomvi-mode-editable t))
320           (setq plomvi-mode 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
327             plomvi-mode nil)
328       (run-hooks 'plomvi-mode-editable-disable-hook)
329       (run-hooks 'plomvi-mode-basic-disable-hook)
330       (run-hooks 'plomvi-mode-disable-hook))))
331
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))