;;; plomvi.el --- poor man's vim emulation ;; Copyright (C) 2019 ;; Author: Christian Heller ;; This program is free software; you can redistribute it and/or modify ;; it under the terms of the GNU General Public License as published by ;; the Free Software Foundation, either version 3 of the License, or ;; (at your option) any later version. ;; This program is distributed in the hope that it will be useful, ;; but WITHOUT ANY WARRANTY; without even the implied warranty of ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ;; GNU General Public License for more details. ;; You should have received a copy of the GNU General Public License ;; along with this program. If not, see . ;;; Commentary: ;; A very imperfect simulation of a small subset of Vim behavior, far ;; from the sophistication of packages like evil; an intentionally ;; thin wrapper around native Emacs behavior instead. To the author, ;; it mostly serves to both avoid muscle memory context switches ;; between Emacs and most basic Vim usages, and at the same time avoid ;; the conceptual distances evil puts between the user and native ;; Emacs logic. It does not care though to keep default Emacs ;; keybindings intact, overwriting some common ones with those from ;; the Vim universe. ;;; Instructions: ;; 1. load this script at the end of your init file: (load ...) ;; 2. to start plomvi by default, put this right after: ;; (plomvi-global-mode 1) ;; 3. if you want to use a different keybinding than "C-c C-c C-c" ;; to simulate the jump back from Insert to Normal mode in Vim (de ;; facto just a plomvi mode activation), put a line such as this ;; before the (load ...) line for plomvi (with "C-c" the desired ;; combo): ;; (defvar plomvi-return-combo (kbd "C-c")) (defun plomvi-half-scroll() "Scroll down half a screen width." (interactive) (scroll-up (/ (window-height) 2))) (defun plomvi-goto-line (count) "Jump to line: on nil count, last one, else count." (interactive "P") (if (null count) (goto-char (point-max)) (goto-char (point-min)) (forward-line (1- count)))) (defun plomvi-prefix-zero-or-line-start (prev-prefix) "If no prefix so far, jump to start of line, else start new prefix with 0." (interactive "P") (if (null prev-prefix) (beginning-of-line) (setq prefix-arg 0))) (defun plomvi-prompt (prompt-input) "Provide super-basic : prompt that only accepts: q q! w w FILENAME wq vsplit split If search and replace syntax is detected, it recommends using `query-replace' instead. " (interactive "M:") (cond ((string= prompt-input "q!") (kill-emacs)) ((string= prompt-input "q") (save-buffers-kill-emacs)) ((string= prompt-input "w") (save-buffer)) ((string-match "^w [^ ]+" prompt-input) (let ((file-name (substring prompt-input 2))) (write-file file-name))) ((string-match "^%?s/" prompt-input) (message "NOT IMPLEMENTED, consider using query-replace(-regexp)")) ((string= prompt-input "wq") ((lambda () (save-some-buffers t) (kill-emacs)))) ((string= prompt-input "vsplit") (split-window-horizontally)) ((string= prompt-input "split") (split-window-vertically)) (t (message "NOT IMPLEMENTED")))) (defun plomvi-newline-above () "Open and jump into new line above current line, deactivate `plomvi-mode'." (interactive) (beginning-of-line) (insert "\n") (previous-line) (plomvi-deactivate)) (defun plomvi-newline-below () "Open and jump into new line below current line, deactivate `plomvi-mode'." (interactive) (end-of-line) (insert "\n") (plomvi-deactivate)) (defun plomvi-paste-backward () "Paste last kill leftwards in current line, or (if kill ends in \n) above it. Note that this ignores killed rectangles. " (interactive) (if (eq nil (string-match "\n$" (current-kill 0))) (yank) (beginning-of-line) (yank) (previous-line))) (defun plomvi-paste-forward () "Paste last kill rightwards in current line, or (if kill ends in \n) under it. Doesn't move rightwards before yanking if at end of buffer. Note that this ignores killed rectangles." (interactive) (if (eq nil (string-match "\n$" (current-kill 0))) (progn (if (< (point) (point-max)) (right-char)) (yank)) (end-of-line) (if (< (point) (point-max)) (right-char)) (yank) (previous-line))) (defun plomvi-affect-lines-of-region(f) "Call f on start of first line of region and end of last line of region." (let* ((start-start-pos (region-beginning)) (start-end-pos (region-end)) (region-start (progn (goto-char start-start-pos) (line-beginning-position))) (region-end (progn (goto-char start-end-pos) (+ 1 (line-end-position))))) (funcall f region-start region-end) (goto-char region-start))) (defun plomvi-kill-region-lines() "Kill lines of marked region." (interactive) (plomvi-affect-lines-of-region 'kill-region)) (defun plomvi-x() "If rectangle or region marked, kill those; else, kill char after point." (interactive) (cond ((and (boundp 'rectangle-mark-mode) (eq t rectangle-mark-mode)) (kill-rectangle (region-beginning) (region-end))) ((use-region-p) (kill-region (region-beginning) (region-end))) ((not (= (point) (line-end-position))) (delete-char 1)) ((not (= (line-beginning-position) (line-end-position))) (backward-char) (delete-char 1)))) (defun plomvi-rectangle-mark() "Start marked rectangle, move right one char so a single column is visible." (interactive) (push-mark (point) nil t) (rectangle-mark-mode) (right-char)) (defun plomvi-search-forward() "Find next occurence of search string last targeted by isearch." (interactive) (search-forward isearch-string)) (defun plomvi-search-backward() "Find previous occurence of search string last targeted by isearch." (interactive) (search-backward isearch-string)) (defun plomvi-Y() "Copy rectangle, or full line, or region in full lines." (interactive) (cond ((and (boundp 'rectangle-mark-mode) (eq t rectangle-mark-mode)) (copy-rectangle-as-kill (region-beginning) (region-end))) ((use-region-p) (plomvi-affect-lines-of-region 'copy-region-as-kill)) (t (copy-region-as-kill (line-beginning-position) (+ 1 (line-end-position)))))) (defun plomvi-copy-region() "Copy marked region." (interactive) (copy-region-as-kill (region-beginning) (region-end))) (defun plomvi-replace-char (c) "Replace char after point with c." (interactive "cplomvi-replace-char") (delete-char 1) (insert-char c) (left-char)) (defun plomvi-no-redo() "Tell user what to do, since implementing vim redo was too much of a hassle." (interactive) (message "Vim-style redo not available. Try M-x for Emacs' undo-undo.")) (defun plomvi-activate() "Activate `plomvi-mode'." (interactive) (plomvi-mode)) (defun plomvi-deactivate() "Deactivate `plomvi-mode'." (interactive) (plomvi-mode -1)) (defvar plomvi-mode-basic-map (make-sparse-keymap) "Keymap for `plomvi-mode' on read-only buffers. In contrast to the keymap `plomvi-editable-mode' for editable buffers, this excludes keybindings for editing text, which thus become available to be used for other purposes.") (suppress-keymap plomvi-mode-basic-map t) (define-key plomvi-mode-basic-map (kbd ":") 'plomvi-prompt) (define-key plomvi-mode-basic-map (kbd "C-w") 'other-window) (define-key plomvi-mode-basic-map (kbd "k") 'previous-line) (define-key plomvi-mode-basic-map (kbd "j") 'next-line) (define-key plomvi-mode-basic-map (kbd "h") 'left-char) (define-key plomvi-mode-basic-map (kbd "l") 'right-char) (define-key plomvi-mode-basic-map (kbd "w") 'forward-word) (define-key plomvi-mode-basic-map (kbd "b") 'backward-word) (define-key plomvi-mode-basic-map (kbd "/") 'isearch-forward) (define-key plomvi-mode-basic-map (kbd "N") 'plomvi-search-backward) (define-key plomvi-mode-basic-map (kbd "n") 'plomvi-search-forward) (define-key plomvi-mode-basic-map (kbd "v") 'set-mark-command) (define-key plomvi-mode-basic-map (kbd "C-v") 'plomvi-rectangle-mark) (define-prefix-command 'plomvi-g-map) (define-key plomvi-mode-basic-map (kbd "g") 'plomvi-g-map) (define-key plomvi-g-map (kbd "g") 'beginning-of-buffer) (define-key plomvi-mode-basic-map (kbd "G") 'plomvi-goto-line) (define-key plomvi-mode-basic-map (kbd "$") 'end-of-line) (define-key plomvi-mode-basic-map (kbd "0") 'plomvi-prefix-zero-or-line-start) (define-key plomvi-mode-basic-map (kbd "1") 'digit-argument) (define-key plomvi-mode-basic-map (kbd "2") 'digit-argument) (define-key plomvi-mode-basic-map (kbd "3") 'digit-argument) (define-key plomvi-mode-basic-map (kbd "4") 'digit-argument) (define-key plomvi-mode-basic-map (kbd "5") 'digit-argument) (define-key plomvi-mode-basic-map (kbd "6") 'digit-argument) (define-key plomvi-mode-basic-map (kbd "7") 'digit-argument) (define-key plomvi-mode-basic-map (kbd "8") 'digit-argument) (define-key plomvi-mode-basic-map (kbd "9") 'digit-argument) (define-key plomvi-mode-basic-map (kbd "C-b") 'scroll-down) (define-key plomvi-mode-basic-map (kbd "C-f") 'scroll-up) (define-key plomvi-mode-basic-map (kbd "C-d") 'plomvi-half-scroll) (defvar plomvi-mode-editable-map (make-sparse-keymap) "Keymap for `plomvi-mode' on editable buffers. Inherits from `plomvi-mode-basic-map', but adds keybindings for text editing.") (set-keymap-parent plomvi-mode-editable-map plomvi-mode-basic-map) (define-key plomvi-mode-editable-map (kbd "i") 'plomvi-deactivate) (define-key plomvi-mode-editable-map (kbd "x") 'plomvi-x) (define-key plomvi-mode-editable-map (kbd "o") 'plomvi-newline-below) (define-key plomvi-mode-editable-map (kbd "O") 'plomvi-newline-above) (define-key plomvi-mode-editable-map (kbd "r") 'plomvi-replace-char) (define-key plomvi-mode-editable-map (kbd "u") 'undo-only) (define-key plomvi-mode-editable-map (kbd "C-r") 'plomvi-no-redo) (define-key plomvi-mode-editable-map (kbd "I") 'string-insert-rectangle) (define-key plomvi-mode-editable-map (kbd "p") 'plomvi-paste-forward) (define-key plomvi-mode-editable-map (kbd "P") 'plomvi-paste-backward) (define-key plomvi-mode-editable-map (kbd "Y") 'plomvi-Y) (define-key plomvi-mode-editable-map (kbd "y") 'plomvi-copy-region) (define-key plomvi-mode-editable-map (kbd "D") 'plomvi-kill-region-lines) (define-prefix-command 'plomvi-d-map) (define-key plomvi-mode-editable-map (kbd "d") 'plomvi-d-map) (define-key plomvi-d-map (kbd "w") 'kill-word) (define-key plomvi-d-map (kbd "$") 'kill-line) (define-key plomvi-d-map (kbd "d") 'kill-whole-line) (defvar plomvi-mode-hook) (defvar plomvi-mode-basic-hook) (defvar plomvi-mode-editable-hook) (defvar plomvi-mode-disable-hook) (defvar plomvi-mode-basic-disable-hook) (defvar plomvi-mode-editable-disable-hook) (defvar-local plomvi-mode nil "mode variable for `plomvi-mode'") (defvar-local plomvi-mode-basic nil "toggles `plomvi-mode-basic-map' in `minor-mode-map-alist' for `plomvi-mode'") (defvar-local plomvi-mode-editable nil "toggles `plomvi-mode-editable-map' in `minor-mode-map-alist' for `plomvi-mode'") (defun plomvi-mode (&optional arg) "Imperfectly emulates a subset of Vim's Normal mode. Sets mode variable `plomvi-mode' and, on read-only buffers, `plomvi-mode-basic', or, on editable buffers, `plomvi-mode-editable'. The latter two's values in `minor-mode-map-alist' toggle either `plomvi-mode-basic-map' or `plomvi-mode-editable-map'." (interactive (list (or current-prefix-arg 'toggle))) (let ((enable (if (eq arg 'toggle) ; follow suggestions (not plomvi-mode) ; from (elisp)Minor (> (prefix-numeric-value arg) 0 )))) ; Mode Conventions (if enable (unless (minibufferp) (if buffer-read-only (setq plomvi-mode-basic t) (setq plomvi-mode-editable t)) (setq plomvi-mode t) (run-hooks 'plomvi-mode-hook) (if plomvi-mode-basic (run-hooks 'plomvi-mode-basic-hook) (run-hooks 'plomvi-mode-editable-hook))) (setq plomvi-mode-editable nil plomvi-mode-basic nil plomvi-mode nil) (run-hooks 'plomvi-mode-editable-disable-hook) (run-hooks 'plomvi-mode-basic-disable-hook) (run-hooks 'plomvi-mode-disable-hook)))) (define-globalized-minor-mode plomvi-global-mode plomvi-mode plomvi-activate) (add-to-list 'minor-mode-alist '(plomvi-mode " PV")) (add-to-list 'minor-mode-map-alist (cons 'plomvi-mode-basic plomvi-mode-basic-map)) (add-to-list 'minor-mode-map-alist (cons 'plomvi-mode-editable plomvi-mode-editable-map)) (defvar plomvi-callable-mode-map (let ((map (make-sparse-keymap)) (return-combo (if (boundp 'plomvi-return-combo) plomvi-return-combo (kbd "C-c C-c C-c")))) (define-key map return-combo 'plomvi-activate) map)) (define-minor-mode plomvi-callable-mode "" :init-value t :keymap "plomvi-callable")