Line-anchored edit patches are precise but brittle against reformatting. Structural edits operate on the AST instead: the same pattern matches foo( x ), foo(x), and foo(/* comment */ x). For symbol-aware renames that touch every importer (TypeScript-flavored, language-server backed), reach for lsp rename instead — it understands scope and re-exports in a way ast-grep does not.

ast_edit

ast_edit rewrites code structurally with ast-grep. Each op is { pat, out }: pat matches an AST shape, out is the replacement template. Patterns match structure, not text — whitespace and comments are ignored.

MetavariableMatches
$AOne AST node, captured as $A for the template.
$_One AST node, not captured.
$$$ARGSZero or more nodes, captured as $$$ARGS for the template.
$$$Zero or more nodes, not captured.

Reusing the same metavariable forces identity: $A == $A matches x == x but not x == y. Language is inferred from the file extensions in paths; narrow each call to one language for deterministic rewrites.

# rename every callsite of legacyFn to newFn, preserving args
ast_edit ops=[{ pat: "legacyFn($$$ARGS)", out: "newFn($$$ARGS)" }] \
         paths=["src/**/*.ts"]
 
# delete every console.log, regardless of argument shape
ast_edit ops=[{ pat: "console.log($$$)", out: "" }] paths=["src/"]
 
# rewrite a CommonJS require to a const binding
ast_edit ops=[{ pat: "$F = require($M)", out: "const $F = require($M)" }] \
         paths=["src/"]
 
# modernize to optional chaining; identity enforced by $A
ast_edit ops=[{ pat: "$A && $A()", out: "$A?.()" }] paths=["src/"]

Every ast_edit run stages as a preview. The TUI shows the diff and the replacement count; nothing hits disk until the model calls resolve with { action: "apply" } (see below). Gate the tool with astEdit.enabled; when on, it auto-enables alongside edit.

For one-off local text edits, prefer edit. For read-only structural search, reach for ast_grep: same pattern grammar, no rewrite, returns anchor-prefixed match lines with metavariable captures inlined.

conflict://

When a file contains git merge markers, read registers each <<<<<<< / ======= / >>>>>>> block as a virtual conflict://N URL. The agent picks a side and writes back; the splice happens in place.

URLEffect
conflict://NThe Nth conflict block in the file. Write content to splice it in.
conflict://*Bulk form; resolves every block with the same content or shorthand.
read path:conflictsOne-line-per-block index of every unresolved conflict in path.

The shorthands @ours, @theirs, and @base stand in for the three sides of the merge. One line of @theirs is the whole edit when that side is the right one wholesale.

# 1. see what's unresolved
read src/session.ts:conflictsconflicts
 
# 2. pick a side for one block
write path="conflict://1" content="@theirs"
 
# 3. or resolve every block in the file the same way
write path="conflict://*" content="@ours"
 
# 4. mixed: read the block, write a hand-merged splice
read conflict://2
write path="conflict://2" content="const merged = { ...base, ...theirs, ...ours };
"

No merge UI, no dedicated tool. The same read and write pair handles the whole flow.

resolve

resolve finalizes a pending action. Tools that stage changes (ast_edit, extension-provided previews, plan approval) queue a callback that must be applied or discarded before anything is written. The contract is { action: "apply" | "discard", reason }; the producer’s callback runs and its result becomes the tool response.

# 1. stage an AST rewrite (returns a (proposed) preview card)
ast_edit ops=[{ pat: "console.log($X)", out: "" }] paths=["src/auth.ts"]
# → 3 replacements in 1 file (proposed)
 
# 2. accept the preview
resolve action="apply" reason="redundant logging in auth path"
# → Applied 3 replacements in 1 file.
 
# 3. or reject it
resolve action="discard" reason="keep logs until after the release"

Calling resolve with no pending action errors out, so the agent can’t accidentally accept a stale preview from a previous turn. Discards are single-call and atomic; there are no partial applies.

When to reach for which

ast_edit Rewriting a syntactic shape across many files: renaming a call, swapping an API, deleting matching statements. Ignores formatting. lsp rename Symbol-aware rename across every importer, respecting scope and re-exports. The right call when ast-grep’s text-shape match would catch a same-named symbol from another module. conflict://Merge resolution. Read to enumerate, write to splice, repeat until the file has no markers.resolve Accept or reject any staged preview. One contract for ast_edit, plan approvals, and custom-tool previews. edit Line-anchored patches against a specific slice you just read. Use when the change is local and the surrounding text is stable.\n

Ask Docs

AI assistant to help answer questions about the documentation. Answers are read-only and cite docs/source.

Hi! How can I help you with the documentation today? Answers are read-only and cite docs/source.

Ctrl+Enter to send