[BUG] LSP tool: @angular/language-server returns empty results — harness does not open companion .ts files, so template-to-component resolution fails
Summary
Claude Code's LSP tool successfully spawns @angular/language-server (ngserver), but every template navigation operation against .html files returns empty (\"No definition found\" / \"No hover information available\"). Root cause: the harness sends textDocument/didOpen only for the file being queried. Angular Language Server requires the client to open the companion component .ts files (and ideally all workspace .ts) so it can build the project graph used to resolve template bindings.
This is the same family as #16804, #42013, #17312, #43100, #31364 (closed), but ngserver is a particularly clean reproduction because every Angular template query depends on workspace context — there is no \"local symbol\" fallback that masks the issue.
Environment
- Claude Code: 2.1.123 (
claude --version) - Platform: macOS (Darwin 25.3.0, arm64)
- Node: v22.21.1
- LSP server:
ngserverfrom@angular/language-server@21.2.10(installed globally) - Workspace: Angular CLI multi-project workspace, Angular 21.0.9, with
@angular/language-service@21.0.9innode_modules/ - Plugin under test: locally maintained
lsp-servers@w3geekery-lsps(https://github.com/w3geekery/claude-code-lsps),.lsp.jsonat plugin root with validlspServers.angularentry including--tsProbeLocationsand--ngProbeLocationspointing at the repo'snode_modules.
What works
- TypeScript LSP (typescript-lsp@claude-plugins-official) — works correctly against
.tsfiles. - some-sass-language-server (registered via the same plugin) — works correctly against
.scssfiles (e.g.documentSymbolreturns mixin/function/variable list). ngserveritself — confirmed to start cleanly in standalone mode when given probe locations. Crashes without them (\"Failed to resolve typescript/lib/tsserverlibrary\"), so the registration is being read correctly by the harness.
What's broken
After plugin install + Claude Code restart, against any .html file in the Angular workspace:
LSP goToDefinition on .../security-group-chips.component.html line 13 char 14
(cursor on \`group\` inside \`{{group.name}}\`)
=> \"No definition found. This may occur if the cursor is not on a symbol,
or if the definition is in an external library not indexed by the LSP server.\"
LSP hover on the same position
=> \"No hover information available. This may occur if the cursor is not on a symbol,
or if the LSP server has not fully indexed the file.\"
LSP documentSymbol on the same file
=> \"Unhandled method textDocument/documentSymbol\"
(expected — Angular LSP doesn't implement documentSymbol for HTML; this just confirms
the server is alive and responding to LSP requests, ruling out a hang or transport issue.)
The component class (SecurityGroupChipsComponent) is defined in the sibling .ts file with @Component({ templateUrl: './security-group-chips.component.html' }), declares public activeGroups = [];, and references it from the template via @for (group of activeGroups; ...). A correctly-driven Angular LSP can resolve this.
Why this is a harness issue, not a server / config issue
- VSCode's official Angular extension drives the same
ngserverbinary against the same workspace and resolves every binding correctly. That extension's behavior at startup includes a workspace-widetextDocument/didOpensweep over all.tsfiles matching the project'stsconfig. It also injectsrootUri/workspaceFoldersfrom the VSCode workspace.
@angular/language-serverdocuments this requirement: its design (https://github.com/angular/vscode-ng-language-service/wiki/Design) explicitly relies on the client opening project files so the embeddedtsserver+ Angular plugin can build the program. Per-filedidOpenfor.htmlonly is insufficient; the server has no way to know which.tsfiles declare the components referenced by that template.
- Empirically: the Claude Code harness sends
didOpenonly for the file the user runsLSPagainst. Confirmed by the symptom pattern (server alive, responds, returns empty for cross-file queries) and by the existing reports in #16804 / #32067 / #32499.
Reproduction
npm install -g @angular/language-server typescript- Install a Claude Code plugin that registers
ngserverfor.html. Minimal.lsp.json:
``json``
{
\"angular\": {
\"command\": \"ngserver\",
\"args\": [
\"--stdio\",
\"--tsProbeLocations\", \"/abs/path/to/angular/workspace/node_modules\",
\"--ngProbeLocations\", \"/abs/path/to/angular/workspace/node_modules\"
],
\"transport\": \"stdio\",
\"extensionToLanguage\": { \".html\": \"html\" }
}
}
- From within an Angular CLI workspace, run
LSP goToDefinitionon any property reference inside an{{interpolation}}or structural directive in a component's template. Expected: jump to the property declaration in the sibling.ts. Actual: \"No definition found.\"
Suggested fix (per #31364 discussion)
The harness needs, on LSP initialize or shortly after initialized:
- Detect the workspace root for the file being queried (walk up from
filePathto findtsconfig.json/package.json/angular.json/ a configurable marker). - Send
rootUri/workspaceFoldersin theinitializerequest. - After
initialized, for any LSP server that registers extensions implying a multi-file project (TS, Angular, Java, Rust, Scala, etc.), enumerate matching files in the workspace and sendtextDocument/didOpenfor each. This is the behavior every \"real\" LSP client (VSCode, Neovim+nvim-lspconfig, Emacs+lsp-mode, Helix) implements.
A more conservative variant: when LSP <op> is called against a .html file handled by an LSP that also handles .ts/.tsx, also didOpen the sibling .ts files in the same directory before forwarding the request. That alone would fix the most common Angular case (component + template colocated), without a full workspace sweep.
Why this matters beyond Angular
Any LSP that builds a cross-file project graph hits this — TypeScript with project references, Rust analyzer with cargo workspaces, gopls with multi-package modules, jdtls with Maven/Gradle. The Angular case is just an unusually clean repro because it has a hard cross-file dependency (template → component) with no fallback.
Related
- #16804 — typescript-lsp didOpen never sent (open)
- #42013 — LSP hangs after init (open, stale)
- #17312 — LSP empty results despite server responding (open)
- #43100 — LSP servers lost during reinitialize (open)
- #31364 — No cross-file indexing (closed) — same root cause
- #31365 — rootUri null on Windows (closed)
- #32067 / #32499 — empty hover/goToDefinition (closed, stale)
This issue has 3 comments on GitHub. Read the full discussion on GitHub ↗