Skip to content

SyntaxEditor

David Sisco edited this page Jan 6, 2026 · 3 revisions

SyntaxEditor

The SyntaxEditor provides batched mutations with atomic commit and undo/redo support.

Creating an Editor

using TinyTokenizer.Ast;

var tree = SyntaxTree.Parse("hello world");

// Create editor from tree
var editor = tree.CreateEditor();

// Make changes
editor
    .Replace(Query.Ident("hello"), "goodbye")
    .Commit();

Working with SyntaxNode References

When you have specific nodes, use them directly (preferred for known nodes):

var tree = SyntaxTree.Parse("a + b");
var idents = tree.Select(Query.AnyIdent).ToList();  // [Ident("a"), Ident("b")]

tree.CreateEditor()
    .Replace(idents[0], "x")           // Replace "a" node
    .Replace(idents[1], "y")           // Replace "b" node
    .InsertBefore(idents[0], "(")      // Insert before node
    .InsertAfter(idents[1], ")")       // Insert after node
    .Commit();
// Result: "(x + y)"

Working with Queries

Use queries for pattern-based selection:

var tree = SyntaxTree.Parse("a + b");

tree.CreateEditor()
    .Replace(Query.Ident("a"), "x")    // Replace by query
    .Replace(Query.Ident("b"), "y")
    .InsertBefore(Query.AnyOperator.First(), "(")
    .InsertAfter(Query.AnyOperator.First(), ")")
    .Commit();
// Result: "(x + y)"

Insert Operations

InsertBefore / InsertAfter

Insert content relative to a node:

tree.CreateEditor()
    .InsertBefore(node, "/* prefix */ ")
    .InsertAfter(node, " /* suffix */")
    .Commit();

Insert with Queries

Use Query API queries:

tree.CreateEditor()
    .InsertBefore(Query.Ident("foo").First(), "// comment\n")
    .InsertAfter(Query.Ident("foo").First(), "();")
    .Commit();

Insert Inside Blocks

Use .Start() and .End() on block queries to insert at block boundaries:

tree.CreateEditor()
    .InsertAfter(Query.BraceBlock.First().Start(), "// entry\n")
    .InsertBefore(Query.BraceBlock.First().End(), "// exit\n")
    .Commit();

// Before: "function { body }"
// After:  "function { // entry\nbody// exit\n }"
Position Method Description
After { InsertAfter(block.Start(), ...) Insert at beginning of block content
Before } InsertBefore(block.End(), ...) Insert at end of block content
Before { InsertBefore(block, ...) Insert before the entire block
After } InsertAfter(block, ...) Insert after the entire block

## Replace Operations

### Replace with String

```csharp
tree.CreateEditor()
    .Replace(Query.Ident("old"), "new")
    .Commit();

Replace with Transformation

Transform each matched node:

tree.CreateEditor()
    .Replace(Query.AnyIdent, n => ((SyntaxToken)n).Text.ToUpper())
    .Commit();

// Before: "hello world"
// After:  "HELLO WORLD"

Replace with Another Node

Copy a node from another tree:

var sourceTree = SyntaxTree.Parse("replacement");
var newNode = sourceTree.Select(Query.AnyIdent).First();  // Ident("replacement")

tree.CreateEditor()
    .Replace(Query.Ident("old"), newNode)
    .Commit();

Batch Replace

Replace multiple nodes at once:

var tree = SyntaxTree.Parse("a b c");
var nodes = tree.Select(Query.AnyIdent).ToList();  // [Ident("a"), Ident("b"), Ident("c")]

tree.CreateEditor()
    .Replace(nodes, "X")               // Replace all with same value
    .Commit();
// Result: "X X X"

Edit Operations (Trivia-Preserving)

The Edit methods transform content while preserving surrounding whitespace:

var tree = SyntaxTree.Parse("  hello   world  ");

tree.CreateEditor()
    .Edit(Query.AnyIdent, content => content.ToUpper())
    .Commit();
// Result: "  HELLO   WORLD  " (whitespace preserved)

Edit vs Replace

Method Transformer Input Trivia Handling
Replace(query, node => ...) Full SyntaxNode You handle trivia
Edit(query, content => ...) Content string only Auto-preserved
// Edit: transformer receives "hello" (no trivia)
tree.CreateEditor()
    .Edit(Query.Ident("hello"), content => $"[{content}]")
    .Commit();

// Replace: transformer receives full node with trivia
tree.CreateEditor()
    .Replace(Query.Ident("hello"), node => $"[{((SyntaxToken)node).Text}]")
    .Commit();

Remove Operations

var tree = SyntaxTree.Parse("keep remove keep");
var toRemove = tree.Select(Query.Ident("remove")).First();  // Ident("remove")

tree.CreateEditor()
    .Remove(toRemove)
    .Commit();
// Result: "keep  keep"

Remove by Query

tree.CreateEditor()
    .Remove(Query.AnyComment)          // Remove all comments
    .Commit();

Using Query.Exact

When you have a node but need a query:

var tree = SyntaxTree.Parse("foo baz");
var node = tree.Select(Query.Ident("foo")).First();  // Ident("foo")

tree.CreateEditor()
    .Replace(Query.Exact(node), "bar")  // Same as .Replace(node, "bar")
    .Commit();
// Result: "bar baz"

Committing Changes

Changes aren't applied until Commit():

var editor = tree.CreateEditor()
    .Replace(Query.Ident("a"), "x")
    .Replace(Query.Ident("b"), "y");

// Tree unchanged here

editor.Commit();  // Now tree is updated

Undo / Redo

All commits support undo/redo:

var tree = SyntaxTree.Parse("hello");

tree.CreateEditor()
    .Replace(Query.Ident("hello"), "goodbye")
    .Commit();

Console.WriteLine(tree.Root.ToFullString()); // "goodbye"

// Undo
tree.Undo();
Console.WriteLine(tree.Root.ToFullString()); // "hello"

// Redo
tree.Redo();
Console.WriteLine(tree.Root.ToFullString()); // "goodbye"

Multiple Undos

Each Commit() creates an undo point:

tree.CreateEditor().Replace(q1, "a").Commit();  // State 1
tree.CreateEditor().Replace(q2, "b").Commit();  // State 2
tree.CreateEditor().Replace(q3, "c").Commit();  // State 3

tree.Undo();  // Back to State 2
tree.Undo();  // Back to State 1
tree.Redo();  // Forward to State 2

Common Patterns

Wrap a Node

var tree = SyntaxTree.Parse("function { body }");
var block = tree.Select(Query.BraceBlock).First();  // Block("{ body }")

tree.CreateEditor()
    .InsertBefore(block, "/* before */ ")
    .InsertAfter(block, " /* after */")
    .Commit();
// Result: "function /* before */ { body } /* after */"

Transform All Identifiers

tree.CreateEditor()
    .Edit(Query.AnyIdent, s => s.ToLowerInvariant())
    .Commit();

Add Entry/Exit Logging

tree.CreateEditor()
    .InsertAfter(Query.BraceBlock.First().Start(), "\n    console.log('enter');")
    .InsertBefore(Query.BraceBlock.First().End(), "\n    console.log('exit');")
    .Commit();

Rename Identifier

tree.CreateEditor()
    .Replace(Query.Ident("oldName"), "newName")
    .Commit();

Remove All Comments

tree.CreateEditor()
    .Remove(Query.AnyComment)
    .Commit();

See Also

Clone this wiki locally