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