home · contact · privacy
4d1b9e3cfa5db15a21b6abffd7ea06a165e8529e
[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-nothing()
47   "Do nothing. Used to shadow self-insert bindings in `plomvi-mode-editable-map'."
48   (interactive))
49
50 (defun plomvi-half-scroll()
51   "Scroll down half a screen width."
52   (interactive)
53   (scroll-up (/ (window-height) 2)))
54
55 (defun plomvi-goto-line (count)
56   "Jump to line: on nil count, last one, else count."
57   (interactive "P")
58   (if (null count)
59       (goto-char (point-max))
60     (goto-char (point-min))
61     (forward-line (1- count))))
62
63 (defun plomvi-prefix-zero-or-line-start (prev-prefix)
64   "If no prefix so far, jump to start of line, else start new prefix with 0."
65   (interactive "P")
66   (if (null prev-prefix)
67       (beginning-of-line)
68     (setq prefix-arg 0)))
69
70 (defun plomvi-prompt (prompt-input)
71   "Provide super-basic : prompt that only accepts:
72 q
73 q!
74 w
75 w FILENAME
76 wq
77 vsplit
78 split
79
80 If search and replace syntax is detected, it recommends using `query-replace'
81 instead.
82 "
83   (interactive "M:")
84   (cond
85    ((string= prompt-input "q!")
86     (kill-emacs))
87    ((string= prompt-input "q")
88     (save-buffers-kill-emacs))
89    ((string= prompt-input "w")
90     (save-buffer))
91    ((string-match "^w [^ ]+" prompt-input)
92     (let ((file-name (substring prompt-input 2))) (write-file file-name)))
93    ((string-match "^%?s/" prompt-input)
94     (message "NOT IMPLEMENTED, consider using query-replace(-regexp)"))
95    ((string= prompt-input "wq")
96     ((lambda () (save-some-buffers t) (kill-emacs))))
97    ((string= prompt-input "vsplit")
98     (split-window-horizontally))
99    ((string= prompt-input "split")
100     (split-window-vertically))
101    (t (message "NOT IMPLEMENTED"))))
102
103 (defun plomvi-newline-above ()
104   "Open and jump into new line above current line, deactivate `plomvi-mode'."
105   (interactive)
106   (beginning-of-line)
107   (insert "\n")
108   (previous-line)
109   (plomvi-deactivate))
110
111 (defun plomvi-newline-below ()
112   "Open and jump into new line below current line, deactivate `plomvi-mode'."
113   (interactive)
114   (end-of-line)
115   (insert "\n")
116   (plomvi-deactivate))
117
118 (defun plomvi-paste-backward ()
119   "Paste last kill leftwards in current line, or (if kill ends in \n) above it.
120
121 Note that this ignores killed rectangles.
122 "
123   (interactive)
124   (if (eq nil (string-match "\n$" (current-kill 0)))
125       (yank)
126     (beginning-of-line)
127     (yank)
128     (previous-line)))
129
130 (defun plomvi-paste-forward ()
131   "Paste last kill rightwards in current line, or (if kill ends in \n) under it.
132
133 Note that this ignores killed rectangles."
134   (interactive)
135   (if (eq nil (string-match "\n$" (current-kill 0)))
136       (progn
137         (right-char)
138         (yank))
139     (end-of-line)
140     (right-char)
141     (yank)
142     (previous-line)))
143
144 (defun plomvi-region-kill()
145   "Kill marked region."
146   (interactive)
147   (kill-region (region-beginning) (region-end)))
148
149 (defun plomvi-x()
150   "If rectangle or region marked, kill those; else, kill char after point."
151   (interactive)
152   (cond
153    ((and (boundp 'rectangle-mark-mode) (eq t rectangle-mark-mode))
154     (kill-rectangle (region-beginning) (region-end)))
155    ((use-region-p)
156     (plomvi-region-kill))
157    (t
158     (delete-char 1))))
159
160 (defun plomvi-rectangle-mark()
161   "Start marked rectangle, move right one char so a single column is visible."
162   (interactive)
163   (push-mark (point) nil t)
164   (rectangle-mark-mode)
165   (right-char))
166
167 (defun plomvi-search-forward()
168   "Find next occurence of search string last targeted by isearch."
169   (interactive)
170   (search-forward isearch-string))
171
172 (defun plomvi-search-backward()
173   "Find previous  occurence of search string last targeted by isearch."
174   (interactive)
175   (search-backward isearch-string))
176
177
178 (defun plomvi-copy-line()
179   "Copy current line into kill buffer."
180   (interactive)
181   (let ((keep_pos (point)))  ; We sort of cheat: We kill the line, then we
182     (kill-whole-line)        ; paste it back, and return point to its
183     (plomvi-paste-backward)  ; original position.
184     (goto-char keep_pos)))   ;
185
186 (defun plomvi-copy-region()
187   "Copy marked region."
188   (interactive)
189   (copy-region-as-kill (region-beginning) (region-end)))
190
191 (defun plomvi-replace-char (c)
192   "Replace char after point with c."
193   (interactive "cplomvi-replace-char")
194   (delete-char 1) (insert-char c) (left-char))
195
196 ;;; some attempt at a redo feature, not very successful, documented here for
197 ;;; research purposes
198 ;
199 ;(setq plomvi-in-redo nil)  ; should be made buffer-local
200 ;(setq plomvi-undo-count 0) ; should be made buffer-local
201 ;(defun plomvi-undo()
202 ;  (interactive)
203 ;  (undo-only)
204 ;  (setq plomvi-in-redo nil)
205 ;  (setq plomvi-undo-count (+ plomvi-undo-count 1)))
206 ;(defun plomvi-redo()
207 ;  (interactive)
208 ;  (if (> plomvi-undo-count 0)
209 ;      (progn
210 ;        (if (null plomvi-in-redo)
211 ;            (progn
212 ;              (insert-char ?\s 1)
213 ;              (undo)
214 ;              (setq plomvi-in-redo t)))
215 ;        (progn
216 ;          (undo)
217 ;          (setq plomvi-undo-count (- plomvi-undo-count 1))))))
218
219 (defun plomvi-no-redo()
220   "Tell user what to do, since implementing vim redo was too much of a hassle."
221   (interactive)
222   (message "Vim-style redo not available. Try M-x for Emacs' undo-undo."))
223
224 (defun plomvi-activate()
225   "Activate `plomvi-mode'."
226   (interactive)
227   (plomvi-mode))
228
229 (defun plomvi-deactivate()
230   "Deactivate `plomvi-mode'."
231   (interactive)
232   (plomvi-mode -1))
233
234 (defvar plomvi-mode-basic-map (make-sparse-keymap)
235   "Keymap for `plomvi-mode' on read-only buffers.
236
237 In contrast to the keymap `plomvi-editable-mode' for editable buffers,
238 this not only excludes keybindings for editing text, but also does not
239 shadow keybindings that are bound to `self-insert-command'.
240
241 Thus, it on the whole shadows much fewer keybindings of other keymaps
242 that can therefore be used for other purposes.")
243 (define-key plomvi-mode-basic-map (kbd ":") 'plomvi-prompt)
244 (define-key plomvi-mode-basic-map (kbd "C-w") 'other-window)
245 (define-key plomvi-mode-basic-map (kbd "k") 'previous-line)
246 (define-key plomvi-mode-basic-map (kbd "j") 'next-line)
247 (define-key plomvi-mode-basic-map (kbd "h") 'left-char)
248 (define-key plomvi-mode-basic-map (kbd "l") 'right-char)
249 (define-key plomvi-mode-basic-map (kbd "w") 'forward-word)
250 (define-key plomvi-mode-basic-map (kbd "b") 'backward-word)
251 (define-key plomvi-mode-basic-map (kbd "/") 'isearch-forward)
252 (define-key plomvi-mode-basic-map (kbd "N") 'plomvi-search-backward)
253 (define-key plomvi-mode-basic-map (kbd "n") 'plomvi-search-forward)
254 (define-key plomvi-mode-basic-map (kbd "v") 'set-mark-command)
255 (define-key plomvi-mode-basic-map (kbd "C-v") 'plomvi-rectangle-mark)
256 (define-prefix-command 'plomvi-g-map)
257 (define-key plomvi-mode-basic-map (kbd "g") 'plomvi-g-map)
258 (define-key plomvi-g-map (kbd "g") 'beginning-of-buffer)
259 (define-key plomvi-mode-basic-map (kbd "G") 'plomvi-goto-line)
260 (define-key plomvi-mode-basic-map (kbd "$") 'end-of-line)
261 (define-key plomvi-mode-basic-map (kbd "0") 'plomvi-prefix-zero-or-line-start)
262 (define-key plomvi-mode-basic-map (kbd "1") 'digit-argument)
263 (define-key plomvi-mode-basic-map (kbd "2") 'digit-argument)
264 (define-key plomvi-mode-basic-map (kbd "3") 'digit-argument)
265 (define-key plomvi-mode-basic-map (kbd "4") 'digit-argument)
266 (define-key plomvi-mode-basic-map (kbd "5") 'digit-argument)
267 (define-key plomvi-mode-basic-map (kbd "6") 'digit-argument)
268 (define-key plomvi-mode-basic-map (kbd "7") 'digit-argument)
269 (define-key plomvi-mode-basic-map (kbd "8") 'digit-argument)
270 (define-key plomvi-mode-basic-map (kbd "9") 'digit-argument)
271 (define-key plomvi-mode-basic-map (kbd "C-b") 'scroll-down)
272 (define-key plomvi-mode-basic-map (kbd "C-f") 'scroll-up)
273 (define-key plomvi-mode-basic-map (kbd "C-d") 'plomvi-half-scroll)
274
275 (defvar plomvi-mode-editable-map (make-sparse-keymap)
276   "Keymap for `plomvi-mode' on editable buffers.
277
278 Inherits from `plomvi-mode-basic-map', but adds keybindings for text editing
279 and shadows keybindings bound to `self-insert-command' to avoid accidentally
280 typing text outside of what would be Vim's Insert mode.")
281 (set-keymap-parent plomvi-mode-editable-map plomvi-mode-basic-map)
282 (define-key plomvi-mode-editable-map [remap self-insert-command] 'plomvi-nothing)
283 (define-key plomvi-mode-editable-map (kbd "i") 'plomvi-deactivate)
284 (define-key plomvi-mode-editable-map (kbd "x") 'plomvi-x)
285 (define-key plomvi-mode-editable-map (kbd "o") 'plomvi-newline-below)
286 (define-key plomvi-mode-editable-map (kbd "O") 'plomvi-newline-above)
287 (define-key plomvi-mode-editable-map (kbd "r") 'plomvi-replace-char)
288 (define-key plomvi-mode-editable-map (kbd "u") 'undo-only)
289 (define-key plomvi-mode-editable-map (kbd "C-r") 'plomvi-no-redo)
290 ;(define-key plomvi-mode-editable-map (kbd "u") 'plomvi-undo)
291 ;(define-key plomvi-mode-editable-map (kbd "C-r") 'plomvi-redo)
292 (define-key plomvi-mode-editable-map (kbd "I") 'string-insert-rectangle)
293 (define-key plomvi-mode-editable-map (kbd "p") 'plomvi-paste-forward)
294 (define-key plomvi-mode-editable-map (kbd "P") 'plomvi-paste-backward)
295 (define-key plomvi-mode-editable-map (kbd "Y") 'plomvi-copy-line)
296 (define-key plomvi-mode-editable-map (kbd "y") 'plomvi-copy-region)
297 (define-key plomvi-mode-editable-map (kbd "D") 'plomvi-region-kill)
298 (define-prefix-command 'plomvi-d-map)
299 (define-key plomvi-mode-editable-map (kbd "d") 'plomvi-d-map)
300 (define-key plomvi-d-map (kbd "w") 'kill-word)
301 (define-key plomvi-d-map (kbd "$") 'kill-line)
302 (define-key plomvi-d-map (kbd "d") 'kill-whole-line)
303 (defvar plomvi-mode-hook)
304 (defvar plomvi-mode-basic-hook)
305 (defvar plomvi-mode-editable-hook)
306 (defvar plomvi-mode-disable-hook)
307 (defvar plomvi-mode-basic-disable-hook)
308 (defvar plomvi-mode-editable-disable-hook)
309 (defvar-local plomvi-mode nil "mode variable for `plomvi-mode'")
310 (defvar-local plomvi-mode-basic nil
311   "toggles `plomvi-mode-basic-map' in `minor-mode-map-alist' for `plomvi-mode'")
312 (defvar-local plomvi-mode-editable nil
313   "toggles `plomvi-mode-editable-map' in `minor-mode-map-alist' for `plomvi-mode'")
314
315 (defun plomvi-mode (&optional arg)
316   "Imperfectly emulates a subset of Vim's Normal mode.
317
318 Sets mode variable `plomvi-mode' and, on read-only buffers, `plomvi-mode-basic',
319 or, on editable buffers, `plomvi-mode-editable'. The latter two's values in
320 `minor-mode-map-alist' toggle either `plomvi-mode-basic-map' or
321 `plomvi-mode-editable-map'."
322   (interactive (list (or current-prefix-arg 'toggle)))
323   (let ((enable (if (eq arg 'toggle)
324                     (not plomvi-mode)
325                   (> (prefix-numeric-value arg) 0 ))))
326     (if enable
327         (unless (minibufferp)
328           (if buffer-read-only
329               (setq plomvi-mode-basic t)
330             (setq plomvi-mode-editable t))
331           (setq plomvi-mode t)
332           (run-hooks 'plomvi-mode-hook)
333           (if plomvi-mode-basic
334               (run-hooks 'plomvi-mode-basic-hook)
335             (run-hooks 'plomvi-mode-editable-hook)))
336       (setq plomvi-mode-editable nil
337             plomvi-mode-basic nil
338             plomvi-mode nil)
339       (run-hooks 'plomvi-mode-editable-disable-hook)
340       (run-hooks 'plomvi-mode-basic-disable-hook)
341       (run-hooks 'plomvi-mode-disable-hook))))
342
343 (define-globalized-minor-mode plomvi-global-mode plomvi-mode plomvi-activate)
344 (add-to-list 'minor-mode-alist '(plomvi-mode " PV"))
345 (add-to-list 'minor-mode-map-alist (cons 'plomvi-mode-basic
346                                          plomvi-mode-basic-map))
347 (add-to-list 'minor-mode-map-alist (cons 'plomvi-mode-editable
348                                          plomvi-mode-editable-map))