Racket 编程入门

第 7 部分 · 走向生产:工程化与实战

结构化编辑——让 DrRacket 替你管括号

手调括号是 Lisp 新手最大的挫败感来源,也是老手从不担心的事——差别只在是否用 paredit 这类结构化编辑:括号永远成对,移动、增删、调整嵌套都以 S 表达式为单位进行。这篇带你装好 drracket-paredit,并把值得记的快捷键过一遍。

结构化编辑

在 DrRacket 里装上 drracket-paredit 插件,编辑器就从「文本编辑」升级成「结构编辑」:你的操作对象不再是字符,而是 S 表达式,括号平衡由插件保证,手滑再也删不坏代码结构。它主要给你五件事:

  • 保持括号平衡:在插入或删除括号时,自动保证括号是成对出现的,避免语法错误。例如,当你输入 (时插件会自动补全一个) 。
  • 结构化导航:提供快捷键,以 S 表达式为单位进行导航,而不是逐字符或逐词移动。例如,可以快速跳到下一个或上一个完整的表达式。
  • 结构化编辑:支持对代码块(S 表达式)进行剪切、复制、粘贴等操作,而不仅仅是文本操作。例如,调整表达式的嵌套层次、合并括号或分解括号。
  • 安全删除:删除操作会自动避免破坏代码结构。例如,删除一个括号时,会同时删除匹配的括号。
  • 高级括号操作:支持括号的 “ 吞吐 ”(slurping & barfing)操作,即调整括号范围,使表达式更容易读或更符合语义。

在使用快捷键之前,必须先进入 Edit → Preferences → Editing,取消勾选「Enable keybindings in menus (overrides Emacs keybinds)」,否则所有 paredit 快捷键都不会生效。修改后需要重启 DrRacket。

快捷键介绍

drracket-paredit 包含的快捷键如下:

Movement:

  • ("c:m:f") paredit-forward-sexp
  • ("c:m:b") paredit-backward-sexp
  • ("c:m:d") down-sexp ;rebind to "c:m:d"
  • ("m:right") forward-atom ;this is not paredit shortcuts, but alternative for forward-word
  • ("m:left") backward-atom ;ditto

Depth-Changing:

  • ("m:s") paredit-splice-sexp
  • ("m:(") paredit-wrap-round
  • ("m:up") paredit-splice-sexp-killing-backward
  • ("m:down") paredit-splice-sexp-killing-forward
  • ("m:r") paredit-raise-sexp
  • ("m:?") paredit-convolute-sexp

Slurpage & barfage

  • ("c:right" "c:)" "c:]") paredit-slurp-forward
  • ("c:m:left" "c:(" "c:[") paredit-slurp-backward
  • ("c:left" "c:}") paredit-barf-forward
  • ("c:m:right" "c:{") paredit-barf-backward

这些快捷键各自的使用场景,配着例子过一遍:

如果不了解快捷键的含义,可以参考 Keyboard Shortcuts

移动(Movement)

这些快捷键让你能够快速地在代码中以 S 表达式为单位导航,而不是逐字符或逐词移动。

  • c:m:f (paredit-forward-sexp)

场景:跳转到下一个 S 表达式的末尾

示例:

(define (square x) (* x |x))
; 光标在 | 处,按 c:m:f 跳到 x 后面
(define (square x) (* x x|))
  • c:m:b (paredit-backward-sexp)

场景:跳到当前或上一个 S 表达式的开头

示例:

(define (square x) (* x x|))
; 光标在 | 处,按 c:m:b 跳到当前 x 的开头
(define (square x) (* x |x))
  • c:m:d (down-sexp)

场景:进入当前 S 表达式的下一层

示例:

(define |(square x) (* x x))
; 光标在 | 处,按 c:m:d 进入子表达式
(define (|square x) (* x x))
  • m:right (forward-atom)

场景:跳到下一个符号或原子(类似 forward-word)

示例:

(define |square x)
; 从 | 处按 m:right 跳到下一个原子的末尾
(define square| x)
  • m:left (backward-atom)

场景:与 m:right 相反,跳到上一个符号或原子

示例:

(define square |x)
; 从 | 处按 m:left 跳到前一个原子
(define |square x)

深度变化(Depth-Changing)

这些快捷键用于修改表达式的结构(如合并、提取或包装)。

  • m:s (paredit-splice-sexp)

场景:移除当前表达式的外围括号,保持内部结构

示例:

(foo (bar |(+ 1 2)) baz)
; 光标在 | 处,按 m:s 移除 (bar ...) 的括号
(foo bar (+ 1 2) baz)
  • m:( (paredit-wrap-round)

场景:用一对括号包裹当前表达式

示例:

(foo |bar baz)
; 光标在 | 处,按 m:( 包裹 bar
(foo (bar) baz)
  • m:up (paredit-splice-sexp-killing-backward)

场景:移除外围括号,并删除光标前面的所有内容

示例:

(foo (bar |(+ 1 2)) baz)
; 光标在 | 处,按 m:up,删除光标前面的内容
(foo (+ 1 2) baz)
  • m:down (paredit-splice-sexp-killing-forward)

场景:移除外围括号,并删除光标位置及其后面的所有内容

示例:

(foo (|bar (+ 1 2)) baz)
; 光标在 | 处,按 m:down,删除光标及其后面的内容
(foo baz)
  • m:r (paredit-raise-sexp)

场景:将光标所在的 S 表达式提升,替换其父表达式

示例:

(* 10 (+ |(- 5 2) 3))
; 光标在 | 处,按 m:r,(- 5 2) 替换整个 (+ ...) 表达式
(* 10 (- 5 2))
  • m:? (paredit-convolute-sexp)

场景:重新排列嵌套结构,将光标所在表达式与外层表达式互换位置

示例:

(let ([x 3]) (+ |(* 2 x) 1))
; 光标在 | 处,按 m:?,(* 2 x) 提升与 + 互换
(+ (let ([x 3]) (* 2 x)) 1)

吞吐与吐出(Slurpage & Barfage)

这些快捷键是操作括号范围的神器,可以让括号快速 “ 吞 ” 或 “ 吐 ” 邻近的表达式。

  • c:right / c:) / c:] (paredit-slurp-forward)

场景:将光标所在括号 右边紧邻 的表达式 “ 吞 ” 进括号内(扩大括号范围)

示例:

(foo (bar |baz) qux)
; 光标在括号内任意位置,按 c:right
(foo (bar baz qux))
; qux 被吞入括号内,右括号向右移动
  • c:m:left / c:( / c:[ (paredit-slurp-backward)

场景:将光标所在括号 左边紧邻 的表达式 “ 吞 ” 进括号内(扩大括号范围)

示例:

(foo bar (|baz qux))
; 光标在括号内任意位置,按 c:m:left
(foo (bar baz qux))
; bar 被吞入括号内,左括号向左移动
  • c:left / c:} (paredit-barf-forward)

场景:将光标所在括号内 最右侧 的表达式 “ 吐 ” 出括号外(缩小括号范围)

示例:

(foo (bar baz |qux))
; 光标在括号内任意位置,按 c:left
(foo (bar baz) qux)
; qux 被吐出括号外,右括号向左移动
  • c:m:right / c:{ (paredit-barf-backward)

场景:将光标所在括号内 最左侧 的表达式 “ 吐 ” 出括号外(缩小括号范围)

示例:

(foo (bar |baz qux))
; 光标在括号内任意位置,按 c:m:right
(foo bar (baz qux))
; bar 被吐出括号外,左括号向右移动

总结

  • 移动快捷键:快速定位到需要操作的 S 表达式。
  • 深度操作快捷键:修改 S 表达式的嵌套结构。
  • 括号操作快捷键:高效调整括号范围

三组快捷键配合起来,你就再也不是在「改文本」,而是在「改结构」——这正是 Lisp 说代码即数据时,编辑器理应配套的体验。装好它,前面各章的示例代码怎么调整括号都不慌。