├── .gitignore ├── ChangeLog.rst ├── README.rst ├── TODO.org ├── lem-pareto-mode.lisp └── lem-pareto.asd /.gitignore: -------------------------------------------------------------------------------- 1 | /playground.lisp 2 | -------------------------------------------------------------------------------- /ChangeLog.rst: -------------------------------------------------------------------------------- 1 | ==================== 2 | Pareto's ChangeLog 3 | ==================== 4 | 5 | 0.1.0 (2020-01-12) 6 | ================== 7 | 8 | * Initial release. 9 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ============================================================= 2 | Pareto - LEM mode to make Lisp code editing more efficient! 3 | ============================================================= 4 | 5 | Pareto is an additional minor mode, supplement to the Paredit, 6 | built into the `LEM`_. 7 | 8 | The main idea was taken from `Lispy`_ mode for Emacs. Whereas Lispy 9 | is a separate mode, Pareto reuses some Paredit functionality, and 10 | both minor modes should be enabled. 11 | 12 | The idea 13 | ======== 14 | 15 | The idea, inherited from the Lispy is to use short one letter (vi style) 16 | binding to navigate and edit sexps. Most bindings manipulate with the 17 | "current sexp". Current sexp is a sexp right before the cursor or right after the 18 | cursor. 19 | 20 | Pareto implements only most commonly used functionality of the Lispy: 21 | 22 | * ``m`` - marks the current sexp. 23 | * ``c`` - copies current sexp. 24 | * ``Ctrl-k`` - kills current sexp and moves point to the next one. 25 | * ``r`` - raises current sexp. 26 | * ``R`` - raises current sexp and all following siblings. 27 | * ``<`` - moves right paren to the left (aka barf). 28 | * ``>`` - moves right paren to the right (aka slurp). 29 | * ``j`` - jumps to the next sibling sexp. 30 | * ``k`` - jumps to the previous sibling sexp. 31 | * ``Return`` - autoindents a new line (this is not from Lispy, but also nice to have feature). 32 | * ``(`` - does the same like Paredit, but additionally can surround selected region. 33 | 34 | Pareto tries to keep implementation simple and readable by reusing as many code 35 | as possible and providing excessive comments. 36 | 37 | Installation 38 | ============ 39 | 40 | 1. Clone the repository https://github.com/40ants/lem-pareto to some directory: 41 | 42 | .. code:: bash 43 | 44 | mkdir -p ~/projects/lisp/ 45 | cd ~/projects/lisp/ 46 | git clone https://github.com/40ants/lem-pareto 47 | 48 | 2. Add this initialization code to your ``~/.lem/init.lisp``: 49 | 50 | .. code:: lisp 51 | 52 | (in-package :lem-user) 53 | 54 | (push "~/projects/lisp/lem-pareto/" asdf:*central-registry*) 55 | (asdf:load-system :lem-pareto) 56 | ;; Enable Paredit and Pareto along with Lisp mode 57 | (add-hook *find-file-hook* 58 | (lambda (buffer) 59 | (when (eq (buffer-major-mode buffer) 60 | 'lem-lisp-mode:lisp-mode) 61 | (change-buffer-mode buffer 'lem-paredit-mode:paredit-mode t) 62 | (change-buffer-mode buffer 'lem-pareto-mode:pareto-mode t)))) 63 | 64 | .. _LEM: https://github.com/cxxxr/lem 65 | .. _Lispy: https://github.com/abo-abo/lispy 66 | -------------------------------------------------------------------------------- /TODO.org: -------------------------------------------------------------------------------- 1 | * DONE Moving up by Ctrl-Alt-u - работает из коробки 2 | - State "DONE" from [2020-01-13 Mon 01:53] 3 | * DONE The same for Ctrl-Alt-f,b,d 4 | - State "DONE" from [2020-01-13 Mon 01:53] 5 | * DONE Marking by "m" 6 | - State "DONE" from [2020-01-13 Mon 01:53] 7 | * DONE Copying sexp by "c" 8 | - State "DONE" from [2020-01-13 Mon 01:53] 9 | * DONE Killing the sexp by Ctrl-k (paredit-kill не годится, так как надо возвращать курсор на следующий sexp) 10 | - State "DONE" from [2020-01-13 Mon 01:53] 11 | * DONE Raising by "r" (paredit не годится так как неправильно райзит на левой скобке и не умеет райзить всё) 12 | - State "DONE" from [2020-01-13 Mon 01:53] 13 | * DONE Moving by Shift > and Shift < 14 | - State "DONE" from [2020-01-13 Mon 01:53] 15 | * DONE Moving between sexps by "j" "k" 16 | - State "DONE" from [2020-01-13 Mon 01:53] 17 | * DONE Raising by shift-r to raise all sexp from the same level. 18 | - State "DONE" from [2020-01-13 Mon 01:53] 19 | This should also keep all comments and reindent them. 20 | Original Lispy raises not all children, but only from the current one 21 | and further. 22 | * DONE Autoindent after the Enter. 23 | - State "DONE" from [2020-01-13 Mon 01:53] 24 | 25 | * DONE Insert paren when region is active, should surround region (paredit's insert does not) 26 | - State "DONE" from "STARTED" [2020-01-13 Mon 06:29] 27 | - State "STARTED" from "TODO" [2020-01-13 Mon 05:58] 28 | * DONE Select current symbol on Alt-m (right now it is bound to back-to-indentation-command) 29 | - State "DONE" from "STARTED" [2020-01-13 Mon 19:26] 30 | - State "STARTED" from "TODO" [2020-01-13 Mon 06:30] 31 | * TODO When selection is active "raise" should raise it 32 | * TODO Backspace should delete preceding sexp 33 | * TODO Replace C-w backward-delete-word may be on the base of the paredit-backward-delete (it deletes one character at a time 34 | 35 | * TODO May be implement moving of the left paren of the sexp 36 | Lem's paredit does not support slurping and barfing which moves the left paren 37 | of the sexp. But Emacs's does (if i remember correctly). Probably it should be implemented 38 | in the Paredit first. 39 | 40 | * BUGS 41 | 42 | ** TODO When raising not the last item, cursor remains on a wrong line 43 | but should be on the last paren of the raised sexp. 44 | 45 | There will be a problem if you raise a doooom: 46 | #+BEGIN_SRC lisp 47 | (progn 48 | (make-instanse 'dooooom) 49 | (make-instanse 'basdsad)) 50 | #+END_SRC 51 | 52 | -------------------------------------------------------------------------------- /lem-pareto-mode.lisp: -------------------------------------------------------------------------------- 1 | (defpackage :lem-pareto-mode 2 | (:use #:cl) 3 | (:export #:*pareto-mode-keymap* 4 | #:pareto-mode)) 5 | (in-package :lem-pareto-mode) 6 | 7 | 8 | (lem:define-minor-mode pareto-mode 9 | (:name "pareto" 10 | :description "Addon to Paredit for easier semantic code editing." 11 | :keymap *pareto-mode-keymap*)) 12 | 13 | (defun point () 14 | (lem:current-point)) 15 | 16 | (defun left-p (&key (at (point))) 17 | "Return t if before a opening paren." 18 | (lem:looking-at at 19 | "[[({]")) 20 | 21 | (defun right-p (&key (at (point))) 22 | "Return t if after a closing paren." 23 | ;; Here we need to look at the character before the cursor. 24 | ;; That is why we do character-offset -1 25 | (lem:with-point ((before-cursor at)) 26 | (lem:character-offset before-cursor -1) 27 | (lem:looking-at before-cursor 28 | "[])}]"))) 29 | 30 | (defun one-char-left (&key (from (point))) 31 | (lem:character-offset from -1)) 32 | 33 | (defun one-char-right (&key (from (point))) 34 | (lem:character-offset from 1)) 35 | 36 | (defun up () 37 | (lem:backward-up-list)) 38 | 39 | (eval-when (:compile-toplevel :load-toplevel :execute) 40 | (defun get-code (label body) 41 | "A helper for `when-to-sexp' to extract different parts of the code." 42 | (rest (assoc label body)))) 43 | 44 | (defmacro when-on-sexp ((&key char-to-insert) &body body) 45 | "Executes body if current point is before or after the paren. 46 | Before execution, point is moved to the position before opening paren. 47 | If point is not on the sexp, than char-to-insert is inserted." 48 | (let ((right-code (get-code :right body)) 49 | (left-code (get-code :left body))) 50 | (when (or (and right-code 51 | (null left-code)) 52 | (and left-code 53 | (null right-code))) 54 | (error "You should use :left and :right labels together.")) 55 | 56 | `(progn 57 | ,(unless right-code 58 | '(when (right-p) 59 | (pareto-different))) 60 | 61 | (cond 62 | ,@(when right-code 63 | `(((right-p) 64 | ,@right-code))) 65 | ((left-p) 66 | ,@(or left-code body)) 67 | (,char-to-insert 68 | (lem:insert-character (point) 69 | ,char-to-insert)))))) 70 | 71 | (defmacro when-on-the-right ((&key char-to-insert) &body body) 72 | "Executes body if current point is after the paren. 73 | If point is not on the sexp, than char-to-insert is inserted." 74 | `(cond 75 | ((right-p) 76 | ,@body) 77 | (,char-to-insert 78 | (lem:insert-character (point) 79 | ,char-to-insert)))) 80 | 81 | (defmacro when-on-the-left ((&key char-to-insert) &body body) 82 | "Executes body if current point is before of the paren. 83 | If point is not on the sexp, than char-to-insert is inserted." 84 | `(cond 85 | ((left-p) 86 | ,@body) 87 | (,char-to-insert 88 | (lem:insert-character (point) 89 | ,char-to-insert)))) 90 | 91 | (lem:define-command pareto-different () () 92 | "Switch to the different side of current sexp." 93 | (let ((buffer (lem:current-buffer))) 94 | (cond 95 | ;; During selection, "d" will move cursor from begining to the end 96 | ((and (lem:buffer-mark-p buffer) 97 | (not (lem:point= (lem:region-beginning) 98 | (lem:region-end)))) 99 | (lem:exchange-point-mark)) 100 | ;; Moving from the end of sexp to the beginning 101 | ((right-p) 102 | (lem:backward-list)) 103 | ;; Moving from the beginning of sexp to the end 104 | ((left-p) 105 | (lem:forward-list)) 106 | ;; Just insering the "d" 107 | (t (lem:insert-character (point) #\d))))) 108 | 109 | (lem:define-command pareto-mark-list () () 110 | "Mark list from special position." 111 | (when-on-sexp (:char-to-insert #\m) 112 | (lem:mark-set) 113 | (pareto-different))) 114 | 115 | (lem:define-command pareto-clone () () 116 | "Clone sexp and indent it. 117 | If it is a top level, then there will be a new line added." 118 | ;; If we are at the end of sexp, 119 | ;; we need to go to the beginning first, 120 | ;; because when we'll make a selection later, 121 | ;; the point will be moved to the end of the sexp, 122 | ;; and a copy will be inserted after it. 123 | (when-on-sexp (:char-to-insert #\c) 124 | ;; We will add an empty new line, if the sexp begins in the first column. 125 | (let ((add-extra-new-line (zerop (lem:point-charpos (point))))) 126 | ;; First, we need to select a sexp 127 | (pareto-mark-list) 128 | ;; and then to copy it as a region: 129 | (let ((text (lem:points-to-string (lem:region-beginning) 130 | (lem:region-end)))) 131 | (lem:newline) 132 | 133 | (when add-extra-new-line 134 | (lem:newline)) 135 | 136 | (lem:insert-string (point) 137 | text) 138 | ;; at the end, we are indenting it nicely. 139 | ;; To do indentation correctly, we need 140 | ;; to move to the beginning and to indent the first 141 | ;; line, to make it in peace with the outer code. 142 | (pareto-different) 143 | (lem:indent-line (point)) 144 | ;; Then move to the end and indent the whole new sexp. 145 | (pareto-different) 146 | (lem-lisp-mode:lisp-indent-sexp))))) 147 | 148 | (lem:define-command pareto-kill () () 149 | "Kills sexp and moves point to the next sexp." 150 | (lem-paredit-mode:paredit-kill) 151 | ;; We need to indent a line because 152 | ;; paredit kill leaves it with a bad indentation 153 | ;; and cursor does not point to the next sexp. 154 | (lem-lisp-mode:lisp-indent-sexp)) 155 | 156 | (lem:define-command pareto-raise () () 157 | "Replaces the parent with the current sexp." 158 | (when-on-sexp (:char-to-insert #\r) 159 | ;; Killing a current sexp and remembering it in the 160 | ;; *kill-ring* 161 | (pareto-kill) 162 | (lem:backward-up-list) 163 | ;; Now, removing outer sexp to replace it with the raised one. 164 | ;; Here we need to override a *kill-ring* to prevent 165 | ;; outer sexp to be pushed to it. 166 | (lem::with-disable-killring () 167 | (pareto-kill)) 168 | (lem:yank) 169 | (lem-lisp-mode:lisp-indent-sexp))) 170 | 171 | (defun search-last-sexp () 172 | "Returns a point pointing to the closing paren of the last sibling sexp." 173 | (lem:with-point ((prev-point (point))) 174 | (loop for point = prev-point then (lem:scan-lists prev-point 1 0 t) 175 | unless point 176 | do (return prev-point)))) 177 | 178 | (lem:define-command pareto-raise-some () () 179 | "Replaces the parent with a current sexp and all children sexps after it." 180 | (when-on-sexp (:char-to-insert #\R) 181 | ;; When we enter the `when-on-sexp', we are on the left side, 182 | ;; so, we need to remember the current point to go to the last sexp. 183 | (let ((beginning (point)) 184 | (end (search-last-sexp))) 185 | (lem:kill-region beginning end) 186 | 187 | (lem:backward-up-list) 188 | ;; Now, removing outer sexp to replace it with the raised one. 189 | ;; Here we need to override a *kill-ring* to prevent 190 | ;; outer sexp to be pushed to it. 191 | (lem::with-disable-killring () 192 | (pareto-kill)) 193 | ;; Now inserting children back to the buffer. 194 | (lem:yank) 195 | ;; And to indent whole parent sexp to make every line fit it place. 196 | ;; This trick does not work if sexps were extracted to the top level. 197 | (lem:save-excursion 198 | (lem:backward-up-list) 199 | (lem-lisp-mode:lisp-indent-sexp))))) 200 | 201 | (lem:define-command pareto-shift-right () () 202 | "Moves a right paren to the right." 203 | (when-on-sexp (:char-to-insert #\>) 204 | (one-char-right) 205 | (lem-paredit-mode:paredit-slurp) 206 | (up) 207 | (pareto-different))) 208 | 209 | (lem:define-command pareto-shift-left () () 210 | "Moves a right paren to the left." 211 | (when-on-sexp (:char-to-insert #\<) 212 | (one-char-right) 213 | (lem-paredit-mode:paredit-barf) 214 | (up) 215 | (pareto-different))) 216 | 217 | (lem:define-command pareto-next-sexp () () 218 | "Moves a the next sexp on the same level." 219 | (when-on-sexp (:char-to-insert #\j) 220 | (:left 221 | ;; In case of the left paren, we a jumping forward through 222 | ;; two parens (closing and opening) and return back to the 223 | ;; opening one. 224 | (lem:scan-lists (point) 2 0) 225 | (pareto-different)) 226 | (:right 227 | ;; From right parens just jumping to the next one 228 | (lem:scan-lists (point) 1 0)))) 229 | 230 | (lem:define-command pareto-prev-sexp () () 231 | "Moves a the next sexp on the same level." 232 | (when-on-sexp (:char-to-insert #\k) 233 | (:right 234 | ;; In case of the right paren, we a jumping backward through 235 | ;; two parens (opening and closing) and return back to the 236 | ;; closing one. 237 | (lem:scan-lists (point) -2 0) 238 | (pareto-different)) 239 | (:left 240 | ;; From left paren just jumping to the next one 241 | (lem:scan-lists (point) -1 0)))) 242 | 243 | (lem:define-command pareto-newline () () 244 | "Adds a new line and indents it." 245 | (lem:newline) 246 | (lem:indent-line (point))) 247 | 248 | (lem:define-command pareto-insert-paren () () 249 | "Inserts a new pair of parens. 250 | By default, it acts like similar function from the Paredit, 251 | but if there is an active selection, then it surrounds it 252 | with parens. In this case, new place of the cursor depends 253 | on the text surrounded by new parens. 254 | 255 | If selected text is a sexp, then cursor is placed before it 256 | and separated by space. 257 | Otherwise, cursor is placed behind it without any space, because 258 | most probably, you want to make a function call and it may be 259 | called without arguments." 260 | (let ((buffer (lem:current-buffer))) 261 | (cond 262 | ;; When selection is active, we want to surround it 263 | ;; with parens 264 | ((lem:buffer-mark-p buffer) 265 | (let ((on-sexp (left-p :at (lem:region-beginning)))) 266 | (when on-sexp 267 | (lem:insert-character (lem:region-beginning) #\Space)) 268 | (lem:insert-character (lem:region-beginning) #\() 269 | 270 | (lem:insert-character (lem:region-end) #\)) 271 | ;; Also we want to put the cursor right after the opening paren 272 | ;; or before the closing paren, depending on the code we are 273 | ;; surrounding. 274 | (lem:move-point (lem:current-point) 275 | (if on-sexp 276 | (one-char-right :from 277 | (lem:region-beginning)) 278 | (one-char-left :from 279 | (lem:region-end)))))) 280 | (t 281 | (lem-paredit-mode:paredit-insert-paren))))) 282 | 283 | (lem:define-command pareto-mark-symbol () () 284 | "Marks current symbol" 285 | ;; When cursor is on the left paren, we want to select 286 | ;; the first symbol in a sexp 287 | (do () 288 | ((not (left-p :at (point)))) 289 | (log:info "Skipping (") 290 | (lem:character-offset (point) 2)) 291 | 292 | (lem:skip-symbol-backward (point)) 293 | (lem:mark-set) 294 | (lem:skip-symbol-forward (point))) 295 | 296 | (lem:define-key *pareto-mode-keymap* "d" 'pareto-different) 297 | (lem:define-key *pareto-mode-keymap* "m" 'pareto-mark-list) 298 | (lem:define-key *pareto-mode-keymap* "M-m" 'pareto-mark-symbol) 299 | (lem:define-key *pareto-mode-keymap* "c" 'pareto-clone) 300 | (lem:define-key *pareto-mode-keymap* "r" 'pareto-raise) 301 | (lem:define-key *pareto-mode-keymap* "R" 'pareto-raise-some) 302 | (lem:define-key *pareto-mode-keymap* "C-k" 'pareto-kill) 303 | (lem:define-key *pareto-mode-keymap* ">" 'pareto-shift-right) 304 | (lem:define-key *pareto-mode-keymap* "<" 'pareto-shift-left) 305 | (lem:define-key *pareto-mode-keymap* "j" 'pareto-next-sexp) 306 | (lem:define-key *pareto-mode-keymap* "k" 'pareto-prev-sexp) 307 | (lem:define-key *pareto-mode-keymap* "Return" 'pareto-newline) 308 | (lem:define-key *pareto-mode-keymap* "(" 'pareto-insert-paren) 309 | 310 | ;; TODO: replace with custom implementation which will delete a word 311 | ;; probably it is a good idea to contribute it to the Paredit 312 | (lem:define-key *pareto-mode-keymap* "C-w" 'lem-paredit-mode:paredit-backward-delete) 313 | -------------------------------------------------------------------------------- /lem-pareto.asd: -------------------------------------------------------------------------------- 1 | (defsystem "lem-pareto" 2 | :description "A LEM mode to make Lisp code editing more efficient!" 3 | :author "Alexander Artemenko " 4 | :licence "BSD" 5 | :class :package-inferred-system 6 | :depends-on ("lem-pareto/lem-pareto-mode")) 7 | --------------------------------------------------------------------------------