[BUG] documentSymbol request returns stale result [typescript-languageserver]

Status Fixed / completed
Reported on v2.1.87
Maintainer reply ✓ Yes — claude[bot]
Activity 6 comments · opened Mar 31, 2026 · closed Apr 26, 2026
💡 Likely answer: A maintainer (claude[bot], contributor) responded on this thread — see the highlighted reply below.

Preflight Checklist

  • [x] I have searched existing issues and this hasn't been reported yet
  • [x] This is a single bug report (please file separate reports for different bugs)
  • [x] I am using the latest version of Claude Code

What's Wrong?

Setup

	npm i -g typescript-language-server typescript
	claude plugin install typescript-lsp

documentSymbol returns stale result

Prompts:
1) use lsp to find the the documentSymbols
2) add a new function to the file
3) use lsp to find the documentSymbols

==> The list of returned symbols does not include the newly added symbol.

What Should Happen?

The LSP documentSymbols operation should return the latest symbols, including the one that was added.

Error Messages/Logs

The debug log looks OK. It shows that the Claude LSP Client sends `textDocument/didChange`


2026-03-30T19:10:35.339Z [DEBUG] File /Users/egamma/Projects/videos/src/server.ts written atomically
2026-03-30T19:10:35.344Z [DEBUG] [LSP PROTOCOL plugin:typescript-lsp:typescript] Sending notification 'textDocument/didChange'.
2026-03-30T19:10:35.350Z [DEBUG] [LSP PROTOCOL plugin:typescript-lsp:typescript] Sending notification 'textDocument/didSave'.
2026-03-30T19:10:35.356Z [DEBUG] Getting matching hook commands for PostToolUse with query: Edit

Steps to Reproduce

Execute the following prompt in a typescript project:

❯ test the lsp documentSymbol request. make a call, make a change, test whether
the lsp documentSymbol sees the change

⏺ Step 1: Get document symbols from src/server.ts

⏺ LSP(operation: "documentSymbol", file: "src/server.ts")
⎿  Found 86 symbols (ctrl+o to expand)

⏺ Good, I can see the symbols. Now let me read the top of the file to pick a
good spot to add something.

Read 1 file (ctrl+o to expand)
⎿  Found 2 new diagnostic issues in 1 file (ctrl+o to expand)

⏺ Step 2: Add a new function to the file

⏺ Update(src/server.ts)
⎿  Added 4 lines
13 const app = express();
14 const PORT = process.env.PORT || process.env.WEBSITE_PORT || 30
00;
15
16 +function testSymbolDetection(): string {
17 + return 'LSP can see me';
18 +}
19 +
20 // Apply security headers to all responses
21 app.use(securityHeaders);
22

⏺ Step 3: Call documentSymbol again to see if LSP picks up the change

⏺ LSP(operation: "documentSymbol", file: "src/server.ts")
⎿  Found 86 symbols (ctrl+o to expand)
⎿  Found 3 new diagnostic issues in 1 file (ctrl+o to expand)

⏺ Interesting — the LSP did not show testSymbolDetection in the symbol list,
even though the TypeScript diagnostic did pick it up (the warning about it
being declared but never read confirms the LSP server sees the edited file
content).

Looking more carefully at the results, I notice the symbols are sorted
alphabetically starting with app. The function testSymbolDetection with a t
should appear after PORT. Let me check — it's not there. The list jumps from
PORT (Constant) - Line 14 to app (Constant) - Line 13 with no
testSymbolDetection entry.

Wait, let me look again more carefully... The top-level symbols are: app,
app.get('/access'), app.get('/health'), app.listen(), extended, PORT. No
testSymbolDetection.

This is unexpected. The diagnostic confirms the LSP sees the change, but
documentSymbol isn't returning the new function. Let me revert the change and
try a different approach — adding a top-level constant instead.

⏺ Update(src/server.ts)
⎿  Added 1 line, removed 4 lines
12
13 const app = express();
14 const PORT = process.env.PORT || process.env.WEBSITE_PORT || 30
00;
15 +const TEST_SYMBOL = 'hello';
16
16 -function testSymbolDetection(): string {
17 - return 'LSP can see me';
18 -}
19 -
17 // Apply security headers to all responses
18 app.use(securityHeaders);
19

⏺ LSP(operation: "documentSymbol", file: "src/server.ts")
⎿  Found 86 symbols (ctrl+o to expand)
⎿  Found 3 new diagnostic issues in 1 file (ctrl+o to expand)

⏺ Same result — TEST_SYMBOL doesn't appear in the symbol list, yet the
diagnostic confirms the LSP sees line 15 with TEST_SYMBOL.

However, I notice the line numbers did shift — app.get('/health') moved from
line 35 to 36, and app.get('/access') from 45 to 46. So the LSP is seeing the
file change (line numbers updated), but it's filtering out the new unused
symbols from documentSymbol results.

Actually wait — PORT is on line 14 in both results, but the original had
app.get('/health') at line 35, now it's at line 36 (shifted by 1 for the added
line). So the LSP definitely sees the edit.

The missing symbol is likely because TypeScript's documentSymbol
implementation filters out unused local declarations, or there may be a
results limit. Let me revert and check if PORT was always visible.

Looking at the results: PORT (Constant) - Line 14 — yes, PORT is shown. And
TEST_SYMBOL would be on line 15. It's simply not appearing.

Let me try an exported symbol instead:

⏺ Update(src/server.ts)
⎿  Added 1 line, removed 1 line
12
13 const app = express();
14 const PORT = process.env.PORT || process.env.WEBSITE_PORT || 30
00;
15 -const TEST_SYMBOL = 'hello';
15 +export const LSP_TEST_SYMBOL = 'hello';
16
17 // Apply security headers to all responses
18 app.use(securityHeaders);

⏺ LSP(operation: "documentSymbol", file: "src/server.ts")
⎿  Found 86 symbols (ctrl+o to expand)

⏺ Still no LSP_TEST_SYMBOL. But wait — notice that app.get('/health') is back at
line 35, not 36. That means the LSP might be returning stale/cached results,
or the line shift I saw earlier was coincidental. Let me check the current
file state:

Read 1 file (ctrl+o to expand)

⏺ The health endpoint is on line 36 in the actual file, but LSP reported it on
line 35. This confirms the LSP documentSymbol is returning stale results —
it's operating on the pre-edit file content.

Let me revert the file and summarize findings.

⏺ Update(src/server.ts)
⎿  Removed 1 line
12
13 const app = express();
14 const PORT = process.env.PORT || process.env.WEBSITE_PORT || 30
00;
15 -export const LSP_TEST_SYMBOL = 'hello';
15
16 // Apply security headers to all responses
17 app.use(securityHeaders);

⏺ Results

The LSP documentSymbol does NOT see edits made in the current session. Here's
the evidence:

  1. Before edit: app.get('/health') reported at line 35
  2. After adding a line at line 15: app.get('/health') is at line 36 in the

actual file, but documentSymbol still reports line 35

  1. New symbols (testSymbolDetection, TEST_SYMBOL, LSP_TEST_SYMBOL) never

appeared in the results

  1. However, TypeScript diagnostics (the [6133] warnings) did see the changes

immediately — reporting TEST_SYMBOL on line 15 as unused

Conclusion: The documentSymbol response appears to be cached/stale and based
on the file as it was when the LSP server first loaded it. The LSP server's
diagnostic engine syncs with edits (via textDocument/didChange), but
documentSymbol does not reflect those changes. This is a bug or limitation in
how the Claude Code LSP integration synchronizes file edits with the language
server.

Claude Model

Opus

Is this a regression?

No, this never worked

Last Working Version

_No response_

Claude Code Version

2.1.87 (Claude Code)

Platform

Anthropic API

Operating System

macOS

Terminal/Shell

iTerm2

Additional Information

This problem does not occur when using the swift-language server.

I don't think is an issue of the typescript-language server. The language server maintains a cache for document symbols (navtree). The cache is using [documentUrl, docversion] has the key. The cache is used when responding the docmentSymbol request.

I suspect the Claude Claude LSP client does not properly update the document version when a document is changed. The client should sent a monotonic increasing textDocument.version number with the didChange and didOpen request. Since the version number is not properly updated the stale cached version is returned from the navtree cache.

View original on GitHub ↗

6 Comments

github-actions[bot] · 5 months ago

Found 3 possible duplicate issues:

  1. https://github.com/anthropics/claude-code/issues/17979
  2. https://github.com/anthropics/claude-code/issues/32499
  3. https://github.com/anthropics/claude-code/issues/32265

This issue will be automatically closed as a duplicate in 3 days.

  • If your issue is a duplicate, please close it and 👍 the existing issue instead
  • To prevent auto-closure, add a comment or 👎 this comment

🤖 Generated with Claude Code

JVL1 · 4 months ago

Adding a datapoint that isolates where the staleness lives, which I don't see called out explicitly in this thread: workspaceSymbol stays fresh while documentSymbol goes permanently stale on the same file at the same moment. That points the finger at the language server's in-memory document buffer specifically — consistent with #30622 (didChange sent with a non-incrementing version, discarded by the server per LSP spec).

Repro

Claude Code 2.1.101, typescript-lsp plugin, typescript-language-server@5.1.3, macOS 15.3.

  1. Create /tmp/lsp-probe/tsconfig.json (standard) and /tmp/lsp-probe/probe.ts:

``ts
export function funcAlpha(): string {
return "alpha";
}
``

  1. Call LSP documentSymbol on probe.ts → returns funcAlpha ✓ (baseline — first didOpen captures current disk state)
  2. Use the Edit tool to rename funcAlphafuncBeta.
  3. Call LSP documentSymbol immediately → still returns funcAlpha
  4. Wait 12s, call again → still funcAlpha
  5. touch probe.ts, call again → still funcAlpha
  6. Call Edit again on the file, call again → still funcAlpha
  7. rm probe.ts && Write probe.ts with funcGamma, call again → still funcAlpha
  8. At the same moment as step 8, call LSP workspaceSymbol for funcGammareturns funcGamma ✓ (fresh)

What this tells us

  • The language server's workspace indexer is working — it's picking up filesystem changes via its own watcher and workspaceSymbol reflects the current disk state.
  • The language server's per-file in-memory document buffer never updates after the initial didOpen, for the rest of the session, regardless of: waiting, touch, subsequent Edit tool calls, or even deleting and recreating the file.
  • This rules out "it self-heals eventually" and "the filesystem watcher nudges it."
  • It's consistent with #30622: Claude Code sends didChange (I can see LSP: Sent didChange for in the cli.js source), but if the version number isn't monotonically increasing, typescript-language-server will silently discard the update and the buffer stays frozen.

Practical impact

All per-file LSP operations that read from the document buffer — documentSymbol, hover, position-based goToDefinition / findReferences — are effectively unusable for post-edit verification in any session where the file was queried before being edited. Agents fall back to running tsc --noEmit / tests, which burns tokens chasing a non-existent problem. workspaceSymbol is the only per-session-reliable option.

Happy to provide cli.js line offsets or additional traces if it helps narrow down where the version counter should be advanced.

SanjoSolutions · 4 months ago

Seems to have been fixed with 2.1.111.

claude[bot] contributor · 4 months ago

This is a duplicate of #17979, which was fixed as of version 2.1.111.

github-actions[bot] · 3 months ago

This issue has been automatically locked since it was closed and has not had any activity for 7 days. If you're experiencing a similar issue, please file a new issue and reference this one if it's relevant.