Every programming language has a formal grammar. Your compiler reads it, builds a parse tree, and rejects anything that doesn’t fit. Your editor ignores the grammar entirely and lets you type whatever you want.
That disconnect is the source of a surprising amount of friction. Autocomplete suggests identifiers that make no sense in context. Syntax highlighting breaks mid-refactor. You get “Unexpected token” errors for mistakes the editor watched you make. We treat this as normal because text editors are all we’ve had for forty years.
The text is an intermediate representation. The compiler throws it away the moment it parses. So why is the text what we edit?
What Structured Editing Actually Means
Structured editing means the source of truth is the Abstract Syntax Tree, not a character buffer. The editor renders the tree as text for your benefit, but every operation manipulates nodes directly. FunctionDeclaration. BinaryExpression. IfStatement. These are the atoms.
Want to add a parameter? You don’t position your cursor between parentheses and type. You invoke an “add parameter” operation on the FunctionDeclaration node. The editor creates a new Parameter node with a placeholder. The rendered text updates. You never create unbalanced parentheses because parentheses aren’t characters you type. They’re visual boundaries generated by the renderer.
This is not science fiction. Lisp has worked this way since the 1960s because S-expressions are trees by definition. Smalltalk treated methods as selectable objects in a browser, not greppable text files. More recently, the Unison language stores code as a Merkle tree of AST nodes, and JetBrains MPS is a projectional editor where the grammar defines both the language and the editing experience.
The Structural Safety Guarantee
Here’s what a tree editor does when you refactor. This Python script approximates the internal logic:
import ast
# The canonical representation is a tree, not text
tree = ast.parse("result = calculate(x, y)")
# Find the Call node and append a keyword argument
for node in ast.walk(tree):
if isinstance(node, ast.Call):
node.keywords.append(
ast.keyword(arg='debug', value=ast.Constant(value=True))
)
# Render to text only when needed (Python 3.9+)
print(ast.unparse(tree))
# result = calculate(x, y, debug=True)
The operation is structurally safe. You cannot accidentally produce calculate(x, y debug=True) because keywords is a typed list of keyword nodes, not a string buffer. The renderer inserts the comma and equals sign. The tree is always syntactically valid.
This is how most formatters and linters already work. Black, prettier, and rustfmt all parse to an AST, transform the tree, and print. The difference is they read text, transform, and write text. A tree editor skips the text round-trip.
Why Nobody Uses Tree Editors
If this is so good, why does every professional developer still use VS Code, Vim, or Emacs?
Speed of interaction is the first reason. Text editing optimizes for a tight loop: you think of a change, your fingers type characters, your eyes verify. Every keypress is the same operation. In a tree editor, “insert an if statement,” “rename a variable,” and “extract a method” are all different gestures. You can’t just mash keys. The cognitive overhead adds up.
The ecosystem wall is the second reason. Every tool in your pipeline assumes text. git diff, grep, GitHub code review, code search, every tutorial on the internet. Structured formats exist, but none have displaced raw text for source code. The network effect is absolute.
The visual rendering problem is the third. Programmers read code as text with decades of typographic convention. Where does a comment go when it describes half a node? How do you display a tree mid-operation when the user hasn’t finished specifying what they want? Rendering a tree as readable, editable text is harder than rendering text as a tree.
Structural Commands in Text Editors
You don’t need to switch editors to get tree-aware benefits. Several tools already operate on the AST while presenting a text interface.
Paredit and Smartparens for Lisps treat S-expressions as trees. You “slurp” an expression into a parent scope or “barf” it out. The parentheses remain text, but the editing commands are tree operations. You move entire subtrees without ever creating a syntax error.
Modern IDE refactorings parse your code, build an AST, determine which variables become parameters, and rewrite the tree. When you “extract method” in IntelliJ, it isn’t doing text replacement. It’s performing a structural transformation and printing the result.
Language servers build an AST internally for every operation. Go-to-definition, rename, and inline hints all traverse the tree. The protocol communicates text positions for compatibility, but the intelligence is structural.
How to Experiment Without Changing Your Editor
If you want to close the gap between how you write code and how the compiler understands it, start here:
-
Use Python’s
astmodule. Write a script that parses real code, walks the tree withast.walk, and transforms it. Most Python tooling (flake8, mypy, Black) does exactly this. -
Explore Tree-sitter. Parse a file in the online playground and inspect the concrete syntax tree. See what the parser actually sees.
-
Try structural editing for Lisps. If you write Clojure or Emacs Lisp, install Paredit. Force yourself to navigate by expression rather than character for one day. The slowness is educational.
The text file is not going anywhere. Too much tooling, too much history, too much muscle memory. But the gap between text editing and structured understanding is a real source of bugs, friction, and cognitive load. You don’t need a tree editor to fix it. You need to stop pretending that the text is the real thing.