Structural (form-aware) code folding for Lem.
Because Lisp is structurally uniform, folding is the same operation at every
depth: find a form's boundaries and collapse them. This extension gets those
boundaries straight from Lem's core s-expression engine
(backward-up-list / form-offset), so it folds a top-level defun and a
deeply nested child form with identical code.
| File | Role |
|---|---|
lem-code-folding.asd |
System definition. |
core-visible-lines.lisp |
The core changes folding needs — two generic visible-line hooks (stepping + counting). Ships as a self-installing shim; see below. |
code-folding.lisp |
The minor mode: fold model, boundary detection, the hook implementations, commands, keymap. |
Folding is not a rendering effect — it is a transform on the editor's vertical
line coordinate system. Three subsystems independently walk lines and must
agree: rendering (call-do-logical-line), scroll + arrow motion
(move-to-next-virtual-line), and the cursor's screen row
(window-cursor-y-not-wrapping, via count-lines). A display-only hack fixes
only the first, leaving scroll/wheel/trackpad jumping and the cursor vanishing
into hidden lines.
core-visible-lines.lisp adds two generic hooks — a visible-line step
(*visible-line-offset-function*) and a visible-line count
(*visible-line-count-function*) — and routes all three subsystems through
them. Core gains no concept of "folding"; it only learns that some lines may be
hidden. With no hook installed, behaviour is byte-for-byte the original.
As shipped this is a self-installing shim (redefines a few core functions at
load; emits harmless redefinition warnings). To upstream: fold the changes
into virtual-line.lisp and logical-line.lisp — the per-site diffs are noted
in the file — then delete the shim and drop it from the .asd.
Put this directory where ASDF/Quicklisp can find it (e.g. symlink into
~/common-lisp/ or your Lem extensions path), then in Lem:
(ql:quickload :lem-code-folding) ; or (asdf:load-system ...)Enable per buffer with M-x code-folding-mode. To turn it on automatically for
Lisp buffers, add a hook in your init.lisp:
(add-hook lem-lisp-mode:*lisp-mode-hook* 'lem-code-folding:code-folding-mode)| Key | Command | Action |
|---|---|---|
C-c f |
code-fold-toggle |
Toggle the fold at point; create one if none. |
C-c C-f |
code-fold |
Collapse the form at point. |
C-c C-u |
code-unfold |
Expand and discard the fold at point. |
C-c F |
code-fold-all |
Collapse every top-level form. |
C-c U |
code-unfold-all |
Remove all folds. |
Return |
code-folding-return |
On a closed fold, open it; otherwise a normal newline. |
Return opening a fold is a convenience for muscle memory (cursor parked on a
collapsed form, press Enter to reveal it). When the cursor is not on a closed
fold it delegates to the buffer's usual Return — newline-and-indent in lisp /
language modes, plain newline elsewhere — so ordinary editing is unaffected.
A collapsed form shows its first line plus a placeholder (*fold-placeholder*,
default " ..."; set to a glyph for SDL2/webview frontends).
- Pathological wrap + fold counting. Buffer-line crossings are fold-aware in
both wrap states, but the multi-line (
-n) wrapped steppers count a fold jump as a single virtual line; an extremely long wrapped header combined with a fold in one keypress could be off by a row. Normal use is unaffected. - Persistence. Folds are in-memory per buffer; not saved across sessions.
- One hook owner. The two hooks are global, single-owner; if another feature ever wants them, promote each to a list of predicates.
- Fold target.
code-foldfolds the form whose head is on the cursor's line ("fold the form I'm on"), falling back to the innermost multi-line form enclosing the cursor ("fold the block I'm in"). Comment-block and region folding aren't handled.
MIT.