Paxedit - Peaceful LISP Editing
Paxedit is an Emacs extension which eliminates the work, tedium, and mistakes involved with manual editing and refactoring LISP code. Paxedit allows the quick refactoring of symbols, symbolic expressions (explicit and implicit), and comments. Normally a unique command or set of commands would allow a user to delete, copy, or transpose symbols, symbolic expressions, or comments. Additionally, after executing some delete or general refactoring commands the user must clean up any extraneous whitespace, correct indentation, and make sure all their expressions are balanced.
Paxedit takes a departure from the above manual state of code editing through automation. Paxedit does away with multiple different commands. Paxedit knows when it’s in a symbol or a comment. Paxedit does the right thing in the right context. For example, Paxedit has one delete command which can be used to delete comments and symbolic expressions explicit and implicit. That is just one of many Paxedit’s context aware commands. Additionally, all Paxedit commands by default cleanup whitespace, fix indentation issues caused by refactoring, and expressions stay balanced.
Table of Contents
Demo
- Watch a demo of Paxedit on Youtube.
- Practice using Paxedit in
practice.el
andpractice.clj
.
Introduction
What is Paxedit?
Paxedit allows the quick refactoring of symbolic expressions from deleting, swapping with neighbor symbolic expressions, and enclosing specified expressions in a comment expression.
- Conceptually consistent
- Parenthesis, brackets, quotes, etc. stay balanced
- Removes the need to select different commands for different contexts
- No need for manual whitespace cleanup
- Code stays correctly indented
- Understands implicit expressions
- Customizable
- Can be extended to work for other languages
- Can even be used for general text editing
Consider the following block of code. In the code example below, the characters ‘-!-’ are a stand in for the cursor.
(if x
(message-!- "It's true!")
(message "It's false!"))
;;; To delete the "(message "It's true!")" symbolic
;;; expression from the containing if expression
;;; consider the number of steps required.
;;; 1. Move the cursor to the outside of the expression
(if x
-!-(message "It's true!")
(message "It's false!"))
;;; 2. Delete the expression with a command that deletes the expression
(if x
-!-
(message "It's false!"))
;;; 3. Clean up the remaining whitespace
(if x-!-
(message "It's false!"))
;;; Paxedit can accomplish the above in one command.
;;; Place the cursor inside the expression you want
;;; to delete and run the command:
;;; paxedit-delete
Paxedit understands functions with implicit associations among its arguments (think Clojure maps or setf). Often times it is convenient to manipulate expressions that are not grouped in parenthesis, brackets, or quotes as a group. Essentially Paxedit allows structures that are not enclosed by parenthesis, brackets, or quotes to be manipulated as if they were. In the code example below, the characters ‘-!-’ are a stand in for the cursor.
;;; Implicit expressions are features of most LISP languages,
;;; below are examples from Emacs LISP & Clojure.
;;; Emacs LISP
(setf a 10
b 20)
;;; Clojure's Map
{:name "Jake"
:age 25}
;;; Clojure let
(let [x 1
y 2])
;;; The variable a is associated with the value 10 in setf.
;;; It would be convenient if we could manipulate the
;;; variables and their values as a unit for refactoring
;;; purposes. For example, we might want to delete the
;;; pair 'b 20' in one command. Paxedit allows exactly that.
;;; Deleting implicit expression
(setf a 10
b-!- 20)
;;; -> (paxedit-delete)
(setf a 10-!-)
;;; Swapping backwards
(setf b 20
a-!- 10)
;;; -> (paxedit-transpose-backward)
(setf a-!- 10
b 20)
;;; What Paxedit sees
(setf (a 10)
(b 10))
Rationale
Paxedit eliminates the work, tedium, and mistakes involved with manual editing and refactoring LISP code.
Tools—software, hardware, or process—should be levers for the mind. Paredit for Emacs is a great example. It lifts the need for the user to think about the keeping parenthesis balanced, since it enforces balanced parenthesis as a default and forces one to use the built in mechanisms to manipulate symbolic expressions which are guaranteed to stay balanced. This removes a large source of errors. Personally, I can’t remember a time when unbalanced parenthesis caused issues in my own code. Useful tools allow the user to stop focusing on the incidental, the mechanics, and simply do the task at hand. Paxedit allows the user to forget about the mechanics and incidental details of refactoring SEXPs, strings, symbols, and comments.
Installation & Usage
Requirements
- Emacs 24.1 or greater (may work on lower versions of Emacs, but untested)
- Paredit Version 23 (on MELPA Stable) - Paredit mode does not need to be enabled for Paxedit to work. In fact, one could use SmartParens and Paxedit at the same time as long as Paredit is installed.
- XTest Library (Optional, for those interested in modifying Paxedit. Highly recommend downloading this library to leverage existing unit tests written for Paxedit).
MELPA
Available to install via Melpa.
Manual Install
- Install the dependencies of Paxedit
- Paredit
- Install Paxedit by including
paxedit.el
in your path or building the file into a package.
Setup
- Add this to your init.el, .emacs.d, or other configuration file.
;;; Load paxedit functionality (require 'paxedit)
- For Paxedit to work, the user must start the minor mode for Paxedit. Run
paxedit-mode
interactive function to start minor mode. If you want Paxedit to start automatically with a major mode add the respective hooks (see below).(add-hook 'emacs-lisp-mode-hook 'paxedit-mode) (add-hook 'clojure-mode-hook 'paxedit-mode)
- Suggested default key bindings.
(eval-after-load "paxedit" '(progn (define-key paxedit-mode-map (kbd "M-<right>") 'paxedit-transpose-forward) (define-key paxedit-mode-map (kbd "M-<left>") 'paxedit-transpose-backward) (define-key paxedit-mode-map (kbd "M-<up>") 'paxedit-backward-up) (define-key paxedit-mode-map (kbd "M-<down>") 'paxedit-backward-end) (define-key paxedit-mode-map (kbd "M-b") 'paxedit-previous-symbol) (define-key paxedit-mode-map (kbd "M-f") 'paxedit-next-symbol) (define-key paxedit-mode-map (kbd "C-%") 'paxedit-copy) (define-key paxedit-mode-map (kbd "C-&") 'paxedit-kill) (define-key paxedit-mode-map (kbd "C-*") 'paxedit-delete) (define-key paxedit-mode-map (kbd "C-^") 'paxedit-sexp-raise) ;; Symbol backward/forward kill (define-key paxedit-mode-map (kbd "C-w") 'paxedit-backward-kill) (define-key paxedit-mode-map (kbd "M-w") 'paxedit-forward-kill) ;; Symbol manipulation (define-key paxedit-mode-map (kbd "M-u") 'paxedit-symbol-change-case) (define-key paxedit-mode-map (kbd "C-@") 'paxedit-symbol-copy) (define-key paxedit-mode-map (kbd "C-#") 'paxedit-symbol-kill)))
Customization
- Prevent whitespace and alignment cleanup. By default Paxedit fixes whitespace and alignment issues left over from refactoring. This functionality by default is enabled, but can be disabled.
(setf paxedit-alignment-cleanup nil)
- Adding new implicit functions. If new implicit functions are added, paxedit-mode must be disabled and re-enabled for the changes to take effect.
;;; Elisp function with implicit structure of two starting ;;; at first argument. (some-function first 1 second 2) ;;; Elisp function with implicit structure of three starting ;;; at the second argument. (some-function2 ignored-symbol 'first 1 "one" 'second 2 "two") (eval-after-load "paxedit" '(progn (add-to-list 'paxedit-implicit-functions-elisp '(some-function . (1 2))) (add-to-list 'paxedit-implicit-functions-elisp '(some-function2 . (2 3))))) ;;; Similarly if we wanted to add these functions to ;;; Clojure we can do: (eval-after-load "paxedit" '(progn (add-to-list 'paxedit-implicit-functions-clojure '(some-function . (1 2))) (add-to-list 'paxedit-implicit-functions-clojure '(some-function2 . (2 3)))))
- Language specific customization. One can use the logic below to add more languages.
;;; Refer to code in Paxedit which defines implicit functions & structures ;;; Defining custom implicit functions/macros for ELISP & Clojure shown ;;; below, and can be used to define new languages. (defcustom paxedit-implicit-functions-elisp '((setq . (1 2)) (setf . (1 2)) (setq-default . (1 2)) (defcustom . (4 2)) (paxedit-new . (1 2)) (paxedit-cnew . (1 2)) (paxedit-cond . (1 2)) (paxedit-put . (2 2)))) ;;; Here is how the association for Elisp & Clojure is setup up ;;; internally. A major mode is associated with Paxedit, which ;;; when loaded will setup the buffer local implicit functions ;;; and structures. (defvar paxedit-assoc '((emacs-lisp-mode . (paxedit-implicit-functions-elisp ;; Elisp does not have any ;; implicit strucutrues nil)) (clojure-mode . (paxedit-implicit-functions-clojure paxedit-implicit-structures-clojure))) "Associate major mode with implicit functions and structure.") ;;; Adding a new language (add-to-list paxedit-assoc '(haskell-mode . (paxedit-implicit-functions-haskell paxedit-implicit-structures-haskell))) ;;; User must define paxedit-implicit-structures-haskell & paxedit-implicit-functions-haskell ;;; using the format for paxedit-implicit-functions-elisp
Functionality
Context Sensitive Navigation
paxedit-backward-up
- Move to the start of the explicit expression, implicit expression or comment.;;; Explicit expression (+ 1 2 (+ 3 -!-4)) -> (+ 1 2 -!-(+ 3 4)) ;;; Implicit expression ;;; Implicit structures, Clojure maps {:one 1 :two -!-2 :three 3} ;;; -> {:one 1 -!-:two 2 :three 3} ;;; In the context of a comment, the cursor will jump to the start of the comment (message "hello world") ; While in some comment -!-editing ;;; -> (message "hello world") -!-; While in some comment editing
paxedit-backward-end
- Move to the end of the explicit expression, implicit expression or comment.;;; Explicit expression (+ 1 2 (+ 3 -!-4)) -> (+ 1 2 (+ 3 4)-!-) ;;; Implicit expression ;;; Implicit structures, Clojure maps {:one 1 :two -!-2 :three 3} ;;; -> {:one 1 :two 2-!- :three 3} ;;; In the context of a comment, the cursor will jump to the start of the comment (message "hello world") ; While in some comment -!-editing ;;; -> (message "hello world") ; While in some comment editing-!-
Context Sensitive Refactoring
paxedit-transpose-forward
- Swap the current explicit expression, implicit expression, symbol, or comment forward depending on what the cursor is on and what is available to swap with. This command is very versatile and will do the “right” thing in each context. See below for the different uses.;;; Swapping symbols, place the cursor within the symbol and run the ;;; shortcut for paxedit-transpose-forward to swap places with the ;;; next symbol or expression while preserving cursor and correctly ;;; re-indenting. (+ tw-!-o one three) -> (+ one tw-!-o three) (+ 1-!-0 (+ 2 3)) -> (+ (+ 2 3) 1-!-0) ;;; Swapping expressions, place the cursor anywhere not within a ;;; symbol and the containing expression can be swapped with the next ;;; expression. (concat "-!-world!" "Hello ") -> (concat "Hello " "-!-world!") (- (+ -!-3 4) (+ 100 200)) -> (- (+ 100 200) (+ -!-3 4)) ;;; Swapped expressions are properly indented (if some-condition (-!-message "It's false") (message "It's true")) ;;; -> (if some-condition (message "It's true") (-!-message "It's false")) ;;; Swapping expressions implicit structures e.g. Clojure maps {:two-!- 2 :one 1 :three 3} ;;; -> {:one 1 :two-!- 2 :three 3} ;;; Swapping comments ;;; should be-!- last ;;; should be first ;;; -> ;;; should be first ;;; should be-!- last
paxedit-transpose-backward
- Swaps the current explicit, implicit expression, symbol, or comment backward depending on what the cursor is on and what is available to swap with. Swaps in the opposite direction ofpaxedit-transpose-forward
, see forward documentation for examples.paxedit-delete
- Delete current explicit expression, implicit expression, or comment. Also cleans up the left-over whitespace from deletion and corrects indentation.;;; Deleting expressions (when some-truth (message "It's true!") (message-!- "It's false!")) ;;; -> (when some-truth (message "It's true!")) ;;; Deleting implicit expressions (setf x 1 y -!-2 g 3) ;;; -> (setf x 1 g 3) ;;; Deleting comments ;;; Some unnecessary -!-comment ;;; Needed comment ;;; -> ;;; Needed comment
paxedit-kill
- Kill current explicit expression, implicit expression, or comment. Also cleans up left-over whitespace from kill and corrects indentation.paxedit-copy
- Copy current explicit expression, implicit expression, or comment.paxedit-sexp-raise
- Raises the expression the cursor is in while preserving the cursor location.(when t (message -!-"hello world")) ;;; -> (message -!-"hello world") ;;; When located in a symbol (when t (mess-!-age "hello world")) ;;; -> (when t mess-!-age)
paxedit-insert-semicolon
- Insert comment or semicolon depending on the location (or context) of the cursor. If the cursor is in a string, comment, or creating a character (?; in elisp or Clojure’s ‘;’) insert semicolon else executeparedit-comment-dwim
to insert comment.;;; Typing semicolon into a lisp buffer -!- ;;; -> ;;; -!- ;;; Results in inserting of comment (message "hello -!-") ;;; -> (message "hello ;") ;;; Results in insertion of semicolon
paxedit-wrap-comment
- Wrap a comment macro around the current expression. If the current expression is already wrapped by a comment, then the wrapping comment is removed.;;; Comment or uncomment the expression. (message -!-"hello world") -> (comment (message -!-"hello world")) ;;; Executing the paxedit-wrap-comment function on a commented ;;; expression causes the comment to be removed. (comment (message -!-"hello world")) -> (message -!-"hello world")
Symbolic Expression Refactoring
paxedit-compress
- Remove all the extraneous whitespace (e.g. newlines, tabs, spaces) to condense expression and contained sub-expressions onto one line.(if -!-(> x 10) (+ x 100) x) ;;; -> paxedit-compress (if -!-(> x 10) (+ x 100) x)
paxedit-dissolve
- Remove enclosing parenthesis, square brackets, curly brackets, or string quotes. In the case of strings, the user is prompted and asked if they would like to dissolve the enclosing quotes since doing so could unbalance the code through introduction of rogue parenthesis, brackets, and so on.(+ (1 -!-2 3)) -> (+ 1 -!-2 3) ["one" -!-"two"] -> "one" -!-"two"
paxedit-format-1
- Expression will be formatted to have one expression per line after the first two expression. Currently, this command does not preserve the cursor position.(+ 1 2 -!-3 4) ;;; -> paxedit-format-1 (+ -!-1 2 3 4)
Symbol Refactoring
paxedit-symbol-change-case
- Change the symbol to all uppercase if any of the symbol characters are lowercase, else lowercase the whole symbol.hell-!-o -> HELL-!-O HELL-!-O -> hell-!-o
paxedit-symbol-kill
- Kill the symbol the text cursor is next to or in and cleans up the left-over whitespace from kill.;;; Kill the current symbol and add it to kill ring and cleans up left ;;; over whitespace. (+ some-other-num some-nu-!-m) -> (+ some-other-num-!-) (+ some-other-num some-num-!-) -> (+ some-other-num-!-)
paxedit-symbol-delete
- Delete the symbol the text cursor is next to or in and cleans up the left-over whitespace from delete.(+ some-other-num some-nu-!-m) -> (+ some-other-num-!-) (+ some-other-num some-num-!-) -> (+ some-other-num-!-)
paxedit-backward-kill
- Kill symbol before the cursor—if it exists—deleting any whitespace in between the cursor and the symbol, while keeping the containing expression balanced. The universal argument can be used to repeat the command N number of times. This context specific command takes a conservative approach by preventing unbalancing of comments or expressions.paxedit-backward-kill
will not kill beyond the containing expression. Additionally, when the cursor is inside a comment, the kill command will kill no further than the start or end of the comment to prevent accidental commenting of other expressions or un-commenting.hello -!-world -> -!-world (+ x1-!- y1 g1) -> (+ -!- y1 g1) (concat one-vari-!-able two) -> (concat -!-able two) ;;; Backward kill will not kill symbols beyond the expression or ;;; comment that currently contains the cursor. Note how the next ;;; examples do nothing but emit a message there is no symbol found ;;; to kill. (-!-concat one-variable two) -> (-!-concat one-variable two) ;;;-!- hello world -> ;;;-!- hello world ;;; Using the universal argument, C-U 2, to kill two symbols (+ x1 y1 -!-g1) -> (+ -!-g1)
paxedit-forward-kill
- Kill symbol after the cursor—if it exists—deleting any whitespace in between the cursor and the symbol, while keeping the containingexpression balanced. The universal argument can be used to repeat the command N number of times. This context specific command takes a conservative approach by preventing unbalancing of comments or expressions.paxedit-forward-kill
will not kill beyond the containing expression. Additionally, when the cursor is inside a comment, the kill command will kill no further than the start or end of the comment to prevent accidental commenting of other expressions or un-commenting.hello -!-world -> hello -!- (+ x1-!- y1 g1) -> (+ x1 -!- g1) (concat one-vari-!-able two) -> (concat one-vari-!- two) ;;; Forward kill will not kill symbols beyond the expression or ;;; comment that currently contains the cursor. Note how the next ;;; examples do nothing but emit a message there is no symbol found ;;; to kill. (concat one-variable two-!-) -> (concat one-variable two-!-) ;;; hello world-!- -> ;;; hello world-!- ;;; Using the universal argument, C-U 2, to kill two symbols (+ x1-!- y1 g1) -> (+ x1-!-)"
Open Pair
paxedit-open-round
- Context specific open round. When the cursor is located within a symbol, the symbol is wrapped in parentheses (see scenario 1). If the cursor is outside of any symbol a pair of parentheses are inserted, and a space is inserted to seperate the newly created parentheses from any neighboring symbols (see scenario 2). If the cursor is located within a string a single, open parenthesis will be inserted without a matching close parenthesis (see scenario 3).;;; Scenario 1. Located in symbol (a b-!-a) -> (a (ba-!-)) ;;; Scenario 2. Located outside symbol (a -!-b c d) -> (a (-!-) b c d) ;;; Scenario 3. Located inside quotes (a \"some -!-string\") -> (a \"some (-!-string\") ;;; Scenario 4. Region has mark set (a b %c d%) -> (a b (c d)-!-)
paxedit-open-quoted-round
- Context specific single-quoted, open round. When the cursor is located within a symbol, the symbol is wrapped in single-quoted parentheses (see scenario 1). If the cursor is outside of any symbol a pair of single-quoted parentheses are inserted, and a space is inserted to seperate the newly created single-quoted parentheses from any neighboring symbols (see scenario 2). If the cursor is located within a string a single, single-quoted, open parenthesis will be inserted without a matching close parenthesis (see scenario 3).;;; Scenario 1. Located in symbol (a b-!-a) -> (a '(ba-!-)) ;;; Scenario 2. Located outside symbol (a -!-b c d) -> (a '(-!-) b c d) ;;; Scenario 3. Located inside quotes (a \"some -!-string\") -> (a \"some '(-!-string\") ;;; Scenario 4. Region has mark set (a b %c d%) -> (a b '(c d)-!-)
paxedit-open-bracket
- Context specific open bracket. When the cursor is located within a symbol, the symbol is wrapped in brackets (see scenario 1). If the cursor is outside of any symbol a pair of brackets are inserted, and a space is inserted to seperate the newly created brackets from any neighboring symbols (see scenario 2). If the cursor is located within a string a single, open bracket will be inserted without a matching close bracket (see scenario 3).;;; Scenario 1. Located in symbol [a b-!-a] -> [a [ba-!-]] ;;; Scenario 2. Located outside symbol [a -!-b c d] -> [a [-!-] b c d] ;;; Scenario 3. Located inside quotes [a \"some -!-string\"] -> [a \"some [-!-string\"] ;;; Scenario 4. Region has mark set [a b %c d%] -> [a b [c d]-!-]
paxedit-open-curly-bracket
- Context specific open curly bracket. When the cursor is located within a symbol, the symbol is wrapped in curly brackets (see scenario 1). If the cursor is outside of any symbol a pair of curly brackets are inserted, and a space is inserted to seperate the newly created curly brackets from any neighboring symbols (see scenario 2). If the cursor is located within a string a single, open curly bracket will be inserted without a matching close curly bracket (see scenario 3).;;; Scenario 1. Located in symbol {a b-!-a} -> {a {ba-!-}} ;;; Scenario 2. Located outside symbol {a -!-b c d} -> {a {-!-} b c d} ;;; Scenario 3. Located inside quotes {a \"some -!-string\"} -> {a \"some {-!-string\"} ;;; Scenario 4. Region has mark set {a b %c d%} -> {a b {c d}-!-}
Debugging
paxedit-macro-expand-replace
- Expand the current expression in its place if it is macro.;;; Example of expanding the anaphoric, "awhen" macro in place. (awhen some-value-!- (message "It's true!")) ;;; -> (let ((it some-value)) (if it (progn (message "It's true!")) nil))-!-
Whitespace & Indentation
paxedit-cleanup
- Indent buffer as defined by mode, remove tabs, and delete trialing whitespace.paxedit-delete-whitespace
- Delete all whitespace (e.g. space, tab, newlines) to the left and right of the cursor.;;; Collapses the whitespace and newlines on both sides of the cursor This is -!- too much whitespace. ;;; -> This is-!-too much whitespace. ;;; Newlines and whitespace (+ 1 -!-2) ;;; -> (+ 1-!-2)
Other Notes & Performance
- Context dependent actions have certain limits and trade-offs. There had to be decisions made on whether to go for precisely similar behavior across commands or some variation due to pragmatic considerations.
- This code was written with a focus on clarity rather than efficiency.
- Implicit SEXPs of large size (the number of symbols or expressions in the SEXP) may be slow during deletion and refactoring. On a 2.3GHz, quad-core I7 processor, implicit expressions of size greater than 150 expressions became noticeably slow to refactor.
Inspiration
- Paredit - http://www.emacswiki.org/emacs/ParEdit
- Brett Victor’s, Inventing on Principle Talk
- Editing Lisp Code - http://c2.com/cgi/wiki?EditingLispCode
License
Copyright © 2014 Mustafa Shameem
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 http://www.gnu.org/licenses/.