· 6 min read ·

The Editing Model That Predated Multi-Cursor by Three Decades

Source: lobsters

The premise behind Daniel Moch’s exploration of Acme — that it is a “text-based GUI” rather than a terminal application — is accurate and worth taking seriously. But the most intellectually significant part of the Plan 9 editor lineage is not the window system or the filesystem interface. It is the text manipulation model that Rob Pike developed for Sam in 1987 and carried forward into Acme. Structural regular expressions and selection-first editing predate multi-cursor by more than three decades, and modern editors are still reconstructing pieces of what Sam made possible.

The Verb-Object Problem

Most text editors, including Vim, operate on a verb-object model. You choose an action (delete, change, yank) and then specify what it acts on: a word, a line, a paragraph, a pattern match. This works well for single targeted operations. The model breaks down when you want to apply the same operation to many non-contiguous pieces of text based on their structure, not their position.

Vim’s :g/pattern/cmd gets partway there. It executes a command for each line matching a pattern across the file. But it operates on lines, not arbitrary text spans, and it cannot compose: you cannot say “in each match of this outer pattern, apply this operation to each match of this inner pattern.”

The verb-object model also encodes “what” as position. Line numbers, motion counts, and named marks are all ways of specifying location. The structural question — “in all strings that look like this, change the part that looks like that” — requires a different kind of addressing.

Structural Regular Expressions

Rob Pike’s 1987 paper “Structural Regular Expressions” introduced a different model. The key concept is “dot,” the current selection. Operations do not address text by position; they address text relative to the current dot, and composing operations means threading dot through a pipeline of commands.

The critical operator is x/pattern/ command: for each non-overlapping match of pattern within dot, set dot to that match and execute command. Its complement, y/pattern/ command, sets dot to each unmatched region between matches. The guard g/pattern/ command executes command only if dot contains a match; v/pattern/ command is the inverse.

Sam, the editor Pike wrote first, exposed this through a command language that interleaves addressing and commands:

# Delete all blank lines in the file
1,$ x/\n/ g/\n\n/ d

# In every function signature, rename a parameter
x/func [A-Za-z]+\([^)]*\)/ s/oldParam/newParam/g

# Pipe the current selection through a formatter
|gofmt

# In every line containing TODO, prepend a comment marker
x/^.*TODO.*\n/ i/\/\/ /

The address 1,$ selects the entire file. x/\n/ extracts each newline as dot. g/\n\n/ tests whether dot is preceded by another newline, an empty line, and executes d only in that case. This reads like a query over the text’s structure rather than an imperative over its positions.

Sam also had cross-file operations. The X/pattern/ command form executed a command in every open file whose name matched a pattern. In 1987, this was workspace-wide structural search-and-replace, composable and addressable from a single command line.

How Acme Inherits the Model

Acme does not expose Sam’s command language through a REPL. Instead, the addressing model lives in the filesystem interface. Each window’s directory under /mnt/acme/ID/ contains an addr file and a data file. Writing an address expression to addr positions the current selection in that window. Reading and writing data operates on whatever addr currently selects.

This means external tools can implement the full Sam addressing model against any Acme window. A script can write 1,/main/ to addr, read the resulting data to get the text from the start of the file to the first occurrence of “main,” modify it, and write it back. The editor does not need to know the script exists; the script does not need to know anything about the editor’s internal state.

Using Russ Cox’s plan9port acme package, this looks like:

w, _ := acme.Open(id, nil)
w.Addr("1,/main/")
buf := make([]byte, 4096)
n, _ := w.Read("data", buf)
// process buf, then:
w.Addr("1,/main/")
w.Write("data", modified)

The | operation from Sam is also present in Acme: selecting text and executing a command with a | prefix in the tag pipes the selection through that command and replaces it with the output. Any Unix program becomes a text transformation: |sort, |uniq, |jq ., |gofmt are all valid Acme operations on selected text.

The Lineage to Kakoune and Helix

Kakoune’s author Maxime Chuillard explicitly cites Sam as an influence on Kakoune’s selection model. Kakoune reverses Vim’s verb-object ordering: you select first, then apply an operation. Multiple selections are first-class. The s key splits selections by a pattern, which is Sam’s x; S splits selections at a pattern boundary.

The result is that Kakoune can express Sam-style structural operations through keyboard sequences. % selects the whole file; s/pattern/<ret> splits into per-match selections; d deletes all of them simultaneously. This is Sam’s 1,$ x/pattern/ d expressed through keyboard interaction rather than a command language.

Helix takes this further by integrating tree-sitter for structural selection. Where Sam and Acme use regular expressions to define structure, Helix can use the actual parse tree. Selecting a function body, all arguments to a function call, or the condition of an if statement are single operations in Helix, not chains of regex approximations.

Neither Kakoune nor Helix carries everything forward from Sam. The y complement operator, which selects text between pattern matches rather than the matches themselves, has no direct keyboard analog in either editor. The g and v guards, as composable conditionals within an address chain, are also absent. Sam’s X multi-file structural operation has partial analogs in project-wide search-and-replace features, but none with the same composability.

What Modern Editors Still Miss

The mainstream editor conversation around multi-cursor editing, introduced widely by Sublime Text around 2011 and later adopted by VS Code and others, solved a recognizable subset of the structural editing problem. Multiple cursors let you apply the same keystroke to multiple positions, which is useful for symmetric edits on visually selected regions.

Sam’s model is more general. Selections in Sam are not symmetric; each match of an x operation can be a different length, in a different structural context. The guard conditions g and v allow operations to be applied conditionally per-match based on the match’s content. The y complement operator is specifically for operating on the gaps between matches, which has no multi-cursor equivalent at all.

The closest modern tool to Sam’s model is probably sed for line-oriented processing, but sed operates strictly on lines and cannot address substructure within them. awk is more powerful but shares the same line-orientation by default.

The gap in most editors is composable structural addressing: the ability to say “in every match of this structure, apply this operation to every match of this sub-structure, but only if the match also contains this guard pattern.” Regex-based batch operations in VS Code and Neovim require either elaborate lookaheads in the regex or manual multi-step workflows. Sam’s command language makes this a short sequence of composed operations.

The Lasting Argument

Acme is the environment where this model becomes habitual. Because every piece of text in every window can be executed, piped through a command, or addressed by the external filesystem API, structural editing stops being a special feature you invoke for complex operations and becomes the natural way of working with text.

The influence of that idea on Kakoune, Helix, and structural regex tooling is measurable. Sam made its argument in 1987: the right way to manipulate text is to address its structure and compose operations through it. Acme embedded that argument in a graphical environment where text is the fundamental computational substrate, addressable by content rather than position. The argument keeps resurfacing because no mainstream editor has fully answered it yet.

Was this interesting?