notes from /dev/null

by Charles Choi 최민수


Moving Text Elegantly in Emacs

20 Jul 2023  Charles Choi

Suppose that you want to edit a list ["a", "b", "c", "d"] so its ordering is ["b", "c", "a", "d"]. How would you do it? A common approach is to manually select "a", cut and paste it into the third position, correct the commas, and call it a day. Doing this once doesn’t seem so bad; having to do this multiple times gets laborious though. Now extrapolate that to a life-time of manually editing around balanced delimiters. Surely there’s a better way?

Recently I learned Emacs can make short work of the above via the transpose family of functions. In particular, transpose-* can be used to build functions that move blocks of text forward or backward. Coupled with the repeat function, these functions provide a set of building blocks to edit text in an elegant fashion. For the above example, we could do the following:

  1. Move the pointer to the front of "a".
  2. Invoke cc/move-sexp-forward, a custom function using transpose-sexps. (I’ll explain sexp later)
  3. Either invoke cc/move-sexp-forward again or use the repeat command.

Here’s a video showing this at work:


In the above example, Emacs considers the string between quotes (example "a") to be a balanced expression (or in Lisp terminology a sexp). Balanced expressions have opening and closing delimiters and an Emacs mode will typically recognize different pairs of delimiters as a sexp. Some examples are "", '', {}, [], <>, and ().

The following code example describes two functions that can move a balanced expression either forward or backward in text. (Verified on Emacs 28.2)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
(defun cc/move-sexp-backward ()
  "Move balanced expression (sexp) to the right of point backward one sexp.
Point must be at the beginning of balanced expression (sexp)."
  (interactive)
  (transpose-sexps 1)
  (forward-sexp -2))

(defun cc/move-sexp-forward ()
  "Move balanced expression (sexp) to the right of point forward one sexp.
Point must be at the beginning of balanced expression (sexp)."
  (interactive)
  (forward-sexp 1)
  (transpose-sexps 1)
  (forward-sexp -1))

Source

Different block types of text can be moved, in symmetry to what is provided by the tranpose family of functions:

  • word
    • cc/move-word-backward
    • cc/move-word-forward
  • sentence
    • cc/move-sentence-backward
    • cc/move-sentence-forward

I leave out the block types char, line, region, and paragraph due to preference and brevity, but they are easily added given the code pattern shown above.

Usability

There are functions that you use occasionally but really don’t have the desire (or even capacity) to remember their names for, much less assign a keybinding to. I think such functions are ideal candidates for being put into a menu. So it is with my thinking about the transpose-* and move-* functions described above.

Detailed below shows how to configure submenus for transposing (cc/transpose-menu) and moving (cc/move-text-menu) text. These submenus can be invoked either via a context menu or via the top level Edit menu. First some screenshots of the menus themselves:

Now for the source:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
(easy-menu-define cc/transpose-menu nil
  "Keymap for Transpose submenu"
  '("Transpose"
    :visible (not buffer-read-only)
    ["Characters" transpose-chars
     :help "Interchange characters around point, moving forward one character."]
    ["Words" transpose-words
     :help "Interchange words around point, leaving point at end of them."]
    ["Lines" transpose-lines
     :help "Exchange current line and previous line, leaving point after both."]
    ["Sentences" transpose-sentences
     :help "Interchange the current sentence with the next one."]
    ["Paragraphs" transpose-paragraphs
     :help "Interchange the current paragraph with the next one."]
    ["Regions" transpose-regions
     :help "region STARTR1 to ENDR1 with STARTR2 to ENDR2."]
    ["Balanced Expressions (sexps)" transpose-sexps
     :help "Like C-t (‘transpose-chars’), but applies to balanced \
expressions (sexps)."]))

(easy-menu-define cc/move-text-menu nil
  "Keymap for Move Text submenu"
  '("Move Text"
    :visible (not buffer-read-only)
    ["Word Forward" cc/move-word-forward
     :help "Move word to the right of point forward one word."]
    ["Word Backward" cc/move-word-backward
     :help "Move word to the right of point backward one word."]
    ["Sentence Forward" cc/move-sentence-forward
     :help "Move sentence to the right of point forward one sentence."]
    ["Sentence Backward" cc/move-sentence-backward
     :help "Move sentence to the right of point backward one sentence."]
    ["Balanced Expression (sexp) Forward" cc/move-sexp-forward
     :help "Move balanced expression (sexp) to the right of point forward \
one sexp."]
    ["Balanced Expression (sexp) Backward" cc/move-sexp-backward
     :help "Move balanced expression (sexp) to the right of point backward \
one sexp."]))

Source

Instrumenting these submenus in the top-level Edit menu is as follows:

1
2
3
4
5
(easy-menu-add-item (lookup-key global-map [menu-bar edit]) nil
                    cc/transpose-menu "Fill")

(easy-menu-add-item (lookup-key global-map [menu-bar edit]) nil
                    cc/move-text-menu "Fill")

This link shows how I've instrumented my context-menu (among many other things) with the above transpose and move text submenus.

Summary

Moving text in Emacs can be elegant via the transpose family of functions, used directly or indirectly. Operations around balanced expressions are compellingly powerful. Coupled with Avy (an amazing navigation package which I just recently learned of thanks to Irreal) and the world is your oyster.

References

emacs

 

AboutMastodonInstagramGitHub

Feeds & TagsGet Captee for macOS

Powered by Pelican