├── .gitignore ├── README.md └── evil-lispy.el /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled 2 | *.elc 3 | 4 | # Packaging 5 | .cask 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | evil-lispy 2 | ========== 3 | 4 | A minor mode and Evil state for editing Lisp code with Lispy. 5 | 6 | More documentation to come. 7 | 8 | 9 | Credits 10 | ------- 11 | 12 | This started as a fork of [evil-lispy](https://github.com/nyobe/evil-lispy), so 13 | a lot of credit goes to that author as these packages share similar goals. 14 | -------------------------------------------------------------------------------- /evil-lispy.el: -------------------------------------------------------------------------------- 1 | ;;; evil-lispy.el --- precision Lisp editing with Evil and Lispy 2 | 3 | ;; Copyright (C) 2015 Brandon Carrell 4 | 5 | ;; Author: Brandon Carrell , Mika Vilpas 6 | ;; URL: https://github.com/sp3ctum/evil-lispy 7 | ;; Version: 0.0.1 8 | ;; Keywords: lisp 9 | 10 | ;; This file is not part of GNU Emacs 11 | 12 | ;; This file is free software; you can redistribute it and/or modify 13 | ;; it under the terms of the GNU General Public License as published by 14 | ;; the Free Software Foundation; either version 3, or (at your option) 15 | ;; any later version. 16 | 17 | ;; This program is distributed in the hope that it will be useful, 18 | ;; but WITHOUT ANY WARRANTY; without even the implied warranty of 19 | ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 20 | ;; GNU General Public License for more details. 21 | 22 | ;; For a full copy of the GNU General Public License 23 | ;; see . 24 | 25 | ;;; Commentary: 26 | ;; 27 | ;; evil-lispy defines a minor mode and an additional Evil state for editing 28 | ;; Lisp code. The goal is to encourage a workflow where you can hop between 29 | ;; Lispy State for making structured edits using Lispy bindings and the rest 30 | ;; of the standard Evil states for general editing. Where it makes sense, 31 | ;; this package redefines a few Lispy bindings, which can be turned on or off 32 | ;; by a variable. 33 | 34 | ;; In addition to providing the Lispy state, this package will do its best to 35 | ;; keep your buffer balanced while writing Lisp code. To that end, it will 36 | ;; advise common Evil operations and block them if they will break your buffer 37 | ;; or modify them slightly in order to keep everything balanced. 38 | ;; 39 | ;; It also provides several useful keybindings for editing Lisp in Normal mode. 40 | ;; 41 | ;; There are several ways to activate Lispy state, denoted as in your 42 | ;; modeline, all from Normal mode: 43 | ;; 44 | ;; ) will hop to the right paren and activate Lispy state 45 | ;; ( will hop to the left paren and activate Lispy state 46 | ;; >i will hop to the right paren and activate Lispy state 47 | ;; " 78 | :message "Entering Lispy state!" 79 | :cursor ("red" box) 80 | :suppress-keymap t 81 | :entry-hook (evil-lispy-state-entry) 82 | :exit-hook (evil-lispy-state-exit) 83 | nil) 84 | 85 | (defun evil-lispy-state-entry () 86 | (remove-hook 'activate-mark-hook #'evil-visual-activate-hook t) 87 | (lispy-mode 1)) 88 | 89 | (defun evil-lispy-state-exit () 90 | (when (region-active-p) (deactivate-mark)) 91 | (add-hook 'activate-mark-hook #'evil-visual-activate-hook nil t) 92 | (lispy-mode -1)) 93 | 94 | (defun evil-lispy-enter-state (direction extra-direction) 95 | "Return a lambda which enters Lispy state at the DIRECTION side of 96 | the current form. DIRECTION must be either 'left or 'right." 97 | (let ((f (intern (concat "lispy-" (symbol-name direction)))) 98 | (g (intern (concat "lispy-" (symbol-name extra-direction))))) 99 | `(lambda () 100 | (interactive) 101 | (when (looking-at lispy-left) (forward-char)) 102 | (let ((pos (point))) 103 | (,f 1) 104 | (when (eq (point) pos) (,g 1))) 105 | (evil-lispy-state)))) 106 | 107 | (fset 'evil-lispy-enter-state-left (evil-lispy-enter-state 'left 'backward)) 108 | (fset 'evil-lispy-enter-state-right (evil-lispy-enter-state 'right 'forward)) 109 | 110 | (defun evil-lispy-enter-marked-state () 111 | "Enters `lispy-state' with the current symbol under point marked." 112 | (interactive) 113 | (evil-lispy-state) 114 | (lispy-mark-symbol)) 115 | 116 | (defun evil-lispy-enter-visual-state () 117 | "If we're in visual state, enter `lispy-state' with the current region 118 | selected." 119 | (interactive) 120 | (let ((start (region-beginning)) 121 | (end (region-end)) 122 | (pos (point))) 123 | (evil-lispy-state) 124 | (set-mark (if (eq pos start) end start)))) 125 | 126 | (defun evil-lispy-enter-insert-state (direction extra-direction) 127 | "Return a lambda which enters Insert state at the DIRECTION side of 128 | the current form. DIRECTION must be either 'left or 'right." 129 | `(lambda () 130 | (interactive) 131 | (funcall (evil-lispy-enter-state ',direction ',extra-direction)) 132 | (evil-insert-state) 133 | (cond 134 | ((eq ',direction 'left) 135 | (forward-char) 136 | (unless (looking-at "\s") 137 | (insert ?\s) 138 | (backward-char))) 139 | ((eq ',direction 'right) 140 | (backward-char) 141 | (unless (looking-back "\s") 142 | (insert ?\s)))))) 143 | 144 | (fset 'evil-lispy-enter-insert-state-left 145 | (evil-lispy-enter-insert-state 'left 'backward)) 146 | (fset 'evil-lispy-enter-insert-state-right 147 | (evil-lispy-enter-insert-state 'right 'forward)) 148 | 149 | ;; ——— Mode ———————————————————————————————————————————————————————————————————— 150 | 151 | (defvar evil-lispy-mode-map (make-sparse-keymap)) 152 | 153 | (define-minor-mode evil-lispy-mode 154 | "A minor mode for integrating Evil and Lispy." 155 | :lighter " evil-lispy" 156 | :keymap evil-lispy-mode-map 157 | :after-hook (evil-normal-state)) 158 | 159 | ;; ——— Text objects ———————————————————————————————————————————————————————————— 160 | 161 | (evil-define-text-object evil-lispy--outer-form-object (&optional count beg end type) 162 | (let ((bounds (lispy--bounds-list))) 163 | (when bounds 164 | (evil-range (car bounds) (cdr bounds))))) 165 | 166 | (evil-define-text-object evil-lispy--inner-form-object (&optional count beg end type) 167 | (let ((bounds (lispy--bounds-list))) 168 | (evil-range (1+ (car bounds)) (1- (cdr bounds))))) 169 | 170 | (define-key evil-inner-text-objects-map "f" 'evil-lispy--inner-form-object) 171 | (define-key evil-outer-text-objects-map "f" 'evil-lispy--outer-form-object) 172 | 173 | ;; ——— Operations —————————————————————————————————————————————————————————————— 174 | 175 | (defun evil-lispy-describe () 176 | (interactive) 177 | (save-excursion 178 | (lispy-mark-symbol) 179 | (lispy-describe-inline))) 180 | 181 | ;; ——— Operators ——————————————————————————————————————————————————————————————— 182 | 183 | (defun evil-lispy--current-line-string () 184 | (buffer-substring-no-properties (line-beginning-position) 185 | (line-end-position))) 186 | 187 | (defun evil-lispy--balanced-p (start end) 188 | "Predicate to check if a range contains balanced boundaries. 189 | Useful for checking if a delete will break balance. Returns t if start -> end 190 | is balanced." 191 | (condition-case condition 192 | (let ((s (buffer-substring-no-properties start end))) 193 | (with-temp-buffer 194 | (insert s) 195 | (check-parens)) 196 | t) 197 | (error nil))) 198 | 199 | (defun evil-lispy--line-deletion-bounds () 200 | "Return a pair of positions representing the bounds of a line deletion." 201 | (let* ((line (evil-lispy--current-line-string)) 202 | (line-length (length line)) 203 | (curpos (current-column)) 204 | (chars-to-delete (with-temp-buffer 205 | (insert line) 206 | (move-to-column curpos) 207 | (lispy-kill) 208 | (- line-length 209 | (length (evil-lispy--current-line-string)))))) 210 | (cons (point) (+ (point) chars-to-delete)))) 211 | 212 | (defun evil-lispy--reconcile-bounds (req-bnds bnds) 213 | (let ((req-beg (car req-bnds)) 214 | (req-end (cdr req-bnds)) 215 | (bnds-beg (1+ (car bnds))) 216 | (bnds-end (1- (cdr bnds)))) 217 | (cons (if (< req-beg bnds-beg) 218 | bnds-beg 219 | req-beg) 220 | (if (> req-end bnds-end) 221 | bnds-end 222 | req-end)))) 223 | 224 | (defun evil-lispy--safe-operator (op orig beg end type reg yank-handler) 225 | "Wrapper for creating safe variants of Evil operations." 226 | (let* ((bnd-op (if (lispy--in-string-p) 227 | #'lispy--bounds-string 228 | #'lispy--bounds-list)) 229 | (cur-bnds (save-excursion 230 | (goto-char orig) 231 | (funcall bnd-op)))) 232 | (cond 233 | ;; We're not changing anything or the operation is balanced 234 | ((or (= end beg) (evil-lispy--balanced-p beg end)) 235 | (funcall op beg end type reg yank-handler)) 236 | 237 | ;; We're not in a form and it's unbalanced 238 | ((null cur-bnds) 239 | (error "Couldn't reconcile bounds of unbalanced operation")) 240 | 241 | ;; Destination is in another form or string 242 | ;; We could refuse the operation, but let's try massaging it 243 | ;; and accomplish something, even if it's not the entire 244 | ;; request 245 | (t 246 | (let* ((revised-beg (save-excursion 247 | (goto-char beg) 248 | (cl-loop until 249 | (equal cur-bnds (funcall bnd-op)) 250 | do 251 | (forward-char) 252 | finally return 253 | (if (eq type 'line) 254 | (point) 255 | (1+ (point)))))) 256 | (revised-end (save-excursion 257 | (goto-char end) 258 | (cl-loop until 259 | (equal cur-bnds (funcall bnd-op)) 260 | do 261 | (backward-char) 262 | finally return 263 | (if (> end orig) 264 | (1+ (point)) 265 | (point)))))) 266 | (funcall op 267 | revised-beg 268 | revised-end 269 | type 270 | reg 271 | yank-handler) 272 | (goto-char revised-beg)))))) 273 | 274 | (evil-define-operator evil-lispy-delete (beg end type reg yank-handler orig) 275 | "A balanced version of `evil-delete'." 276 | (interactive "" (list (point))) 277 | (evil-lispy--safe-operator #'evil-delete orig beg end type reg yank-handler)) 278 | 279 | (evil-define-operator evil-lispy-yank (beg end type reg yank-handler orig) 280 | "A balanced version of `evil-yank'." 281 | (interactive "" (list (point))) 282 | (evil-lispy--safe-operator #'evil-yank orig beg end type reg yank-handler)) 283 | 284 | (evil-define-operator evil-lispy-change (beg end type reg yank-handler orig) 285 | "A balanced version of `evil-change'." 286 | (interactive "" (list (point))) 287 | (evil-lispy--safe-operator #'evil-change orig beg end type reg yank-handler)) 288 | 289 | (evil-define-operator evil-lispy-change-line (beg end type reg yank-handler) 290 | "Delete to end of line using `lispy-kill' and change to `evil-insert-state'." 291 | :motion nil 292 | (interactive "") 293 | (let ((bnds (evil-lispy--line-deletion-bounds))) 294 | (evil-change (car bnds) 295 | (cdr bnds) 296 | type 297 | reg 298 | yank-handler))) 299 | 300 | (evil-define-operator evil-lispy-delete-line (beg end type reg yank-handler) 301 | "Delete to end of line using `lispy-kill'." 302 | :motion nil 303 | (interactive "") 304 | (let ((bnds (evil-lispy--line-deletion-bounds))) 305 | (evil-delete (car bnds) 306 | (cdr bnds) 307 | type 308 | reg 309 | yank-handler))) 310 | 311 | (evil-define-operator evil-lispy-yank-line (beg end type reg yank-handler) 312 | "Safe yank to end of line." 313 | :motion nil 314 | :move-point nil 315 | (interactive "") 316 | (let ((bnds (evil-lispy--line-deletion-bounds))) 317 | (evil-yank (car bnds) 318 | (cdr bnds) 319 | type 320 | reg 321 | yank-handler))) 322 | 323 | 324 | ;; ——— Keys ———————————————————————————————————————————————————————————————————— 325 | 326 | (define-key evil-lispy-state-map [escape] 'evil-normal-state) 327 | 328 | ;; ——— Entering state —————————————————— 329 | (evil-define-key 'normal evil-lispy-mode-map 330 | "(" #'evil-lispy-enter-state-left 331 | ")" #'evil-lispy-enter-state-right 332 | "gv" #'evil-lispy-enter-marked-state 333 | "i" #'evil-lispy-enter-insert-state-right 336 | ">I" #'evil-lispy-enter-insert-state-right) 337 | 338 | (evil-define-key 'visual evil-lispy-mode-map 339 | (kbd "RET") #'evil-lispy-enter-visual-state) 340 | 341 | ;; ——— Editing operations —————————————— 342 | (when evil-lispy-modified-operators 343 | (evil-define-key 'normal evil-lispy-mode-map 344 | "D" #'evil-lispy-delete-line 345 | "C" #'evil-lispy-change-line 346 | "Y" #'evil-lispy-yank-line 347 | "d" #'evil-lispy-delete 348 | "c" #'evil-lispy-change 349 | "y" #'evil-lispy-yank)) 350 | 351 | (evil-define-key 'normal evil-lispy-mode-map 352 | "K" #'evil-lispy-describe 353 | (kbd "M-k") #'lispy-kill-sentence 354 | (kbd "C-1") #'evil-lispy-describe 355 | (kbd "C-2") #'lispy-arglist-inline) 356 | 357 | ;; ——— Insert operations ——————————————— 358 | (evil-define-key 'insert evil-lispy-mode-map 359 | "(" #'lispy-parens 360 | "[" #'lispy-brackets 361 | "{" #'lispy-braces 362 | ")" #'lispy-right-nostring 363 | "\"" #'lispy-quotes 364 | (kbd "DEL") #'lispy-delete-backward 365 | (kbd "M-k") #'lispy-kill-sentence 366 | (kbd "C-1") #'lispy-describe-inline 367 | (kbd "C-2") #'lispy-arglist-inline) 368 | 369 | (provide 'evil-lispy) 370 | 371 | ;;; evil-lispy.el ends here 372 | --------------------------------------------------------------------------------