[BUG] documentSymbol request returns stale result [typescript-languageserver]
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:
- Before edit: app.get('/health') reported at line 35
- After adding a line at line 15: app.get('/health') is at line 36 in the
actual file, but documentSymbol still reports line 35
- New symbols (testSymbolDetection, TEST_SYMBOL, LSP_TEST_SYMBOL) never
appeared in the results
- 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.
This issue has 6 comments on GitHub. Read the full discussion on GitHub ↗