Skip to content

Commit 219766f

Browse files
jedudenclaude
andauthored
Plan 131: LSP symbol navigation for agents (Claude) (#238)
* Add plan 130: LSP symbol navigation for agents (Claude) Extend `mdsmith lsp` with documentSymbol, definition, implementation, references, workspaceSymbol, and callHierarchy methods so Claude's LSP tool (and any other LSP-aware client) can navigate Markdown by heading outline, anchor and file links, and the include/catalog dependency graph. https://claude.ai/code/session_01RoS5UiVF9ucNHh1Vzs97Np * Renumber LSP plan from 130 to 131 130 is taken by an in-flight plan; bump this one to the next free slot. https://claude.ai/code/session_01RoS5UiVF9ucNHh1Vzs97Np * Address Copilot review comments on plan 131 - Link MDS027 to README.md, matching the convention used by other docs. - Fix goldmark type name: it is `*ast.LinkReferenceDefinition`, not `*ast.LinkReferenceDef`. Plan 129 already uses the correct name. https://claude.ai/code/session_01RoS5UiVF9ucNHh1Vzs97Np --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent bd7240e commit 219766f

2 files changed

Lines changed: 308 additions & 0 deletions

File tree

PLAN.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ footer: |
4242
| 128 || sonnet | [Reject undefined reference-link labels](plan/128_no-undefined-reference-labels.md) |
4343
| 129 || sonnet | [Flag unused or duplicate link reference definitions](plan/129_no-unused-link-definitions.md) |
4444
| 130 | 🔳 | opus | [Distribute mdsmith binaries via npm, PyPI, asdf, mise, and the VS Code marketplaces](plan/130_binary-distribution-and-versioning.md) |
45+
| 131 | 🔲 | opus | [LSP symbol navigation for agents (Claude)](plan/131_lsp-symbol-navigation.md) |
4546
| 52 || | [Archetype / Template Library for Agentic Patterns](plan/52_archetype-template-library.md) |
4647
| 61 || | [Required Structure Rule Hardening](plan/61_required-structure-hardening.md) |
4748
| 65 || | [Spike WASM-Embedded Weasel Inference](plan/65_spike-wasm-embedded-inference.md) |

plan/131_lsp-symbol-navigation.md

Lines changed: 307 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,307 @@
1+
---
2+
id: 131
3+
title: LSP symbol navigation for agents (Claude)
4+
status: "🔲"
5+
model: opus
6+
summary: >-
7+
Extend `mdsmith lsp` with the document-symbol,
8+
definition, implementation, references,
9+
workspace-symbol, and call-hierarchy methods so an
10+
LSP-aware agent (Claude's LSP tool, Neovim, Helix)
11+
can navigate Markdown by heading outline, anchor
12+
and file links, and the include/catalog graph.
13+
---
14+
# LSP symbol navigation for agents (Claude)
15+
16+
## Goal
17+
18+
Let an LSP client navigate Markdown like code: list
19+
the file outline, jump from links to targets,
20+
enumerate references to a heading, search by name,
21+
and walk the include/catalog graph as a call
22+
hierarchy. The server reuses the existing AST and
23+
config plumbing.
24+
25+
## Background
26+
27+
Plan 121 shipped `mdsmith lsp` with diagnostics and
28+
code actions. Plan 122 adds hover for rule and
29+
directive docs. Neither covers symbol navigation.
30+
31+
Claude's LSP tool exposes nine methods:
32+
33+
| LSP method | Agent intent |
34+
|-------------------------------------|---------------------------------|
35+
| `textDocument/documentSymbol` | List symbols in this file |
36+
| `textDocument/definition` | Where is this defined? |
37+
| `textDocument/implementation` | Where is the concrete behavior? |
38+
| `textDocument/hover` | What is this? |
39+
| `textDocument/references` | Who uses this? |
40+
| `workspace/symbol` | Find a symbol by name |
41+
| `textDocument/prepareCallHierarchy` | Anchor a call-graph view |
42+
| `callHierarchy/incomingCalls` | Who calls this? |
43+
| `callHierarchy/outgoingCalls` | What does this call? |
44+
45+
mdsmith already understands the edges these methods
46+
need: headings, anchor and file links, link-ref
47+
defs, front-matter `kind:`, and directive
48+
arguments. The
49+
[MDS027 rule](../internal/rules/MDS027-cross-file-reference-integrity/README.md)
50+
walks every link target. Catalog expands globs.
51+
52+
## Non-Goals
53+
54+
- New transports. Stdio stays.
55+
- Rename refactoring. Heading rewrites need anchor
56+
fixups across the workspace; separate plan.
57+
- Type hierarchy. No Markdown analogue.
58+
- Code lens, inlay hints, semantic tokens.
59+
- Indexing outside the workspace root. The index
60+
obeys the existing
61+
[`internal/discovery`](../internal/discovery)
62+
walk.
63+
64+
## Design
65+
66+
### Symbol model
67+
68+
A symbol is one of four things, with a `SymbolKind`
69+
chosen so picker UIs bucket each sensibly:
70+
71+
| Concept | `SymbolKind` | Container |
72+
|---------------------------|----------------|---------------------------|
73+
| Heading (H1–H6) | `String` (15) | parent heading |
74+
| Link-reference definition | `Key` (20) | file |
75+
| Front-matter field | `Property` (7) | file |
76+
| Directive (`<?name … ?>`) | `Event` (24) | enclosing heading or file |
77+
78+
Headings drive the outline; the others sit flat.
79+
The cross-document key is `(file, anchor)` for
80+
headings (slug from
81+
[`mdtext.CollectTOCItems`](../internal/mdtext/mdtext.go))
82+
and `(file, label)` for link refs.
83+
84+
### Workspace index
85+
86+
A new package `internal/lsp/index` holds the
87+
symbol graph. It stores headings, link-reference
88+
defs, front-matter top-level keys, directives, and
89+
both directions of the reference edges (link
90+
targets, include / catalog / build targets).
91+
92+
Build is lazy on the first symbol request. Update
93+
is incremental:
94+
95+
- `didOpen` / `didChange` / `didSave` re-parses one
96+
buffer and swaps its slice.
97+
- `**/*.md` watcher (added here; today's watcher
98+
covers only `.mdsmith.yml`) invalidates one file.
99+
- `.mdsmith.yml` change rebuilds the whole index
100+
because kind / ignore globs may shift scope.
101+
102+
The index calls
103+
[`lint.ParseFile`](../internal/lint/file.go) once
104+
per file. Existing visitors cover headings and
105+
link targets. A new visitor captures
106+
`*ast.LinkReferenceDefinition` and front-matter keys.
107+
Memory at 10 000 files is ~300K entries, well
108+
under plan 121's 512 MB `GOMEMLIMIT`.
109+
110+
### `textDocument/documentSymbol`
111+
112+
Returns a `DocumentSymbol[]` tree rooted at H1s.
113+
Each heading carries name, anchor in `detail`,
114+
range from heading to next sibling, and children.
115+
Front-matter keys hang off a synthetic top-of-file
116+
symbol. Directives become children of their
117+
enclosing heading.
118+
119+
Capability: `documentSymbolProvider = true`.
120+
121+
### `textDocument/definition` and `…/implementation`
122+
123+
Both share one `resolveTarget(uri, position)` core.
124+
`Implementation` returns multi-target sets where
125+
`Definition` returns one.
126+
127+
| Cursor on… | `Definition` | `Implementation` adds |
128+
|--------------------------------|------------------------------|----------------------------|
129+
| `[text](#anchor)` | heading in this file ||
130+
| `[text](./other.md)` | line 1 of `other.md` ||
131+
| `[text](./other.md#anchor)` | heading in `other.md` ||
132+
| `[text][label]` | matching `[label]: url` ||
133+
| `<?include file: "x.md"?>` arg | `x.md` line 1 ||
134+
| `<?build source: "x.md"?>` arg | `x.md` line 1 ||
135+
| `kind:` value in front matter | kind block in `.mdsmith.yml` | every file with that kind |
136+
| Heading line | the heading | every link target matching |
137+
138+
A small helper `internal/lsp/index/locate.go` maps
139+
a position to an AST node and a token tag (heading,
140+
anchorLink, fileLink, refUse, refDef, directiveArg,
141+
frontMatterKey, frontMatterValue). One unit test
142+
per token tag.
143+
144+
Capabilities: `definitionProvider = true`,
145+
`implementationProvider = true`.
146+
147+
### `textDocument/references`
148+
149+
| Cursor on… | References returned |
150+
|---------------------------|--------------------------------------------------|
151+
| Heading | every workspace link to `(file, anchor)` |
152+
| `[label]: url` definition | every `[text][label]` and shortcut in the file |
153+
| File line 1 | every link target with this path (no anchor) |
154+
| `kind:` value | every file with that kind assignment |
155+
| Directive block | every directive whose `file:` / `source:` = this |
156+
157+
`includeDeclaration: false` excludes the heading or
158+
definition itself. Capability:
159+
`referencesProvider = true`.
160+
161+
### `workspace/symbol`
162+
163+
The query is a case-insensitive substring. It
164+
matches heading text, link-ref labels, kind names,
165+
and front-matter `title:`. The relative path goes
166+
in `containerName`. Capability:
167+
`workspaceSymbolProvider = true`.
168+
169+
### Call hierarchy
170+
171+
A Markdown file is the unit of "function"; an
172+
outbound reference is a "call". This fits doc
173+
workflows: `incomingCalls` answers "who depends on
174+
this runbook?", `outgoingCalls` answers "what does
175+
this overview embed?".
176+
177+
`prepareCallHierarchy` accepts three cursor
178+
positions. On line 1, the item is the file. On a
179+
heading, the item is that heading section and
180+
calls are scoped to its range. On a directive arg,
181+
the item is the target file.
182+
183+
`incomingCalls` returns every edge into the item.
184+
Sources include cross-file links, `<?include?>`,
185+
`<?catalog?>` matches, and `<?build?>`. Each entry
186+
carries the source file and the reference line.
187+
`outgoingCalls` returns every edge out of the
188+
item. Catalog matches reuse the cached glob
189+
expansions for MDS019.
190+
191+
Capability: `callHierarchyProvider = true`.
192+
193+
### Position and performance
194+
195+
Ranges follow the UTF-16 column convention plan
196+
121 set; the
197+
[`utf16Length`](../internal/lsp/diagnostics.go)
198+
helper extends unchanged. Budgets: cold build
199+
under 1 s on 1 000 files, incremental update under
200+
20 ms per `didChange`. A new
201+
`internal/lsp/index/bench_test.go` measures both
202+
on synthetic 100 / 1 000 / 10 000-file workspaces;
203+
the plan 121 benchmark CI step picks it up.
204+
205+
### Backwards compatibility
206+
207+
Diagnostics and code-action behavior are
208+
unchanged. New capabilities are additive. A client
209+
that ignores them sees the post-plan-121 server.
210+
211+
## Tasks
212+
213+
1. Add `internal/lsp/index` with the symbol graph
214+
types and `Build` / `Update` / `Remove` entry
215+
points. Cover heading collection, link-ref defs,
216+
front-matter keys, and directive parsing in unit
217+
tests. Reuse `mdtext.CollectTOCItems`.
218+
2. Add the inbound / outbound edge tables. Sources:
219+
anchor links, file links, `<?include?>`,
220+
`<?catalog?>`, `<?build?>`. Reuse
221+
[`lint/pi_parser.go`](../internal/lint/pi_parser.go).
222+
3. Add `internal/lsp/index/locate.go` mapping a
223+
document URI plus position to an AST node and
224+
token tag. One test per tag.
225+
4. Wire the index into the server. Build lazily on
226+
first symbol request; update on document
227+
events; rebuild on `.mdsmith.yml` change;
228+
invalidate on `**/*.md` watcher events. Extend
229+
[`registerWatchers`](../internal/lsp/server.go).
230+
5. Implement `textDocument/documentSymbol` and add
231+
the capability. Integration test against a
232+
fixture with H1/H2/H3 headings, directives, and
233+
link refs.
234+
6. Implement `textDocument/definition` and
235+
`textDocument/implementation`. Cover every row
236+
in the design table.
237+
7. Implement `textDocument/references`. Cover the
238+
five rows; verify `includeDeclaration`.
239+
8. Implement `workspace/symbol`. Cover heading,
240+
kind, and `title:` matches.
241+
9. Implement `prepareCallHierarchy`,
242+
`incomingCalls`, `outgoingCalls`. Cover file /
243+
heading / directive prepare paths and round-
244+
trip on a three-file include chain.
245+
10. Add the bench file and the budget thresholds.
246+
The CI step from plan 121 covers it.
247+
11. Extend
248+
[`docs/reference/cli/lsp.md`](../docs/reference/cli/lsp.md)
249+
with a "Symbol navigation" section, the
250+
symbol-kind table, and call-hierarchy
251+
semantics.
252+
12. Update
253+
[the VS Code guide](../docs/guides/editors/vscode.md)
254+
with a short "Outline and Go to Definition"
255+
note.
256+
13. Add an end-to-end test in
257+
[`cmd/mdsmith`](../cmd/mdsmith) driving
258+
`initialize``didOpen``documentSymbol`
259+
`definition``references`
260+
`prepareCallHierarchy``incomingCalls`
261+
`shutdown``exit`.
262+
263+
## Acceptance Criteria
264+
265+
- [ ] `documentSymbol` returns a hierarchical
266+
outline whose nesting matches heading levels.
267+
- [ ] `definition` jumps to a heading from an
268+
anchor link, to line 1 of a file from a
269+
relative link, and to the matching reference
270+
def from `[text][ref]`.
271+
- [ ] `implementation` on a `kind:` value returns
272+
one location per file assigned that kind.
273+
- [ ] `references` on a heading returns every
274+
workspace anchor link to it;
275+
`includeDeclaration: false` excludes the
276+
heading itself.
277+
- [ ] `workspace/symbol` substring queries match
278+
headings, link-ref labels, front-matter
279+
titles, and kind names.
280+
- [ ] `prepareCallHierarchy` on a file returns one
281+
item; `incomingCalls` lists files that
282+
include / link to it; `outgoingCalls` lists
283+
files it includes / links to.
284+
- [ ] Cold-build benchmark reports under 1 s on
285+
1 000 files; incremental-update under 20 ms
286+
per `didChange`. Invocation:
287+
`go test -run=^$ -bench=. ./internal/lsp/...`
288+
- [ ] [`docs/reference/cli/lsp.md`](../docs/reference/cli/lsp.md)
289+
lists every new capability and the symbol-
290+
kind table.
291+
- [ ] All tests pass: `go test ./...`.
292+
- [ ] `go tool golangci-lint run` reports no
293+
issues.
294+
- [ ] `mdsmith check .` passes including the new
295+
docs and the updated `PLAN.md` catalog.
296+
297+
## Open Questions
298+
299+
- **Catalog glob expansion in `incomingCalls`.** A
300+
glob like `**/*.md` would inflate result lists.
301+
The first pass collapses each catalog block to
302+
one entry. Add an `expandCatalog` flag later if
303+
needed.
304+
- **`findReferences` across include boundaries.**
305+
A heading in an included file is reachable via
306+
both files. The first pass reports both; flag
307+
if noisy.

0 commit comments

Comments
 (0)