[BUG] LSP tool: @angular/language-server returns empty results — harness does not open companion .ts files, so template-to-component resolution fails

Resolved 💬 3 comments Opened Apr 29, 2026 by w3geekery Closed May 3, 2026

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: ngserver from @angular/language-server@21.2.10 (installed globally)
  • Workspace: Angular CLI multi-project workspace, Angular 21.0.9, with @angular/language-service@21.0.9 in node_modules/
  • Plugin under test: locally maintained lsp-servers@w3geekery-lsps (https://github.com/w3geekery/claude-code-lsps), .lsp.json at plugin root with valid lspServers.angular entry including --tsProbeLocations and --ngProbeLocations pointing at the repo's node_modules.

What works

  • TypeScript LSP (typescript-lsp@claude-plugins-official) — works correctly against .ts files.
  • some-sass-language-server (registered via the same plugin) — works correctly against .scss files (e.g. documentSymbol returns mixin/function/variable list).
  • ngserver itself — 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

  1. VSCode's official Angular extension drives the same ngserver binary against the same workspace and resolves every binding correctly. That extension's behavior at startup includes a workspace-wide textDocument/didOpen sweep over all .ts files matching the project's tsconfig. It also injects rootUri / workspaceFolders from the VSCode workspace.
  1. @angular/language-server documents this requirement: its design (https://github.com/angular/vscode-ng-language-service/wiki/Design) explicitly relies on the client opening project files so the embedded tsserver + Angular plugin can build the program. Per-file didOpen for .html only is insufficient; the server has no way to know which .ts files declare the components referenced by that template.
  1. Empirically: the Claude Code harness sends didOpen only for the file the user runs LSP against. Confirmed by the symptom pattern (server alive, responds, returns empty for cross-file queries) and by the existing reports in #16804 / #32067 / #32499.

Reproduction

  1. npm install -g @angular/language-server typescript
  2. Install a Claude Code plugin that registers ngserver for .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\" }
}
}
``

  1. From within an Angular CLI workspace, run LSP goToDefinition on 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:

  1. Detect the workspace root for the file being queried (walk up from filePath to find tsconfig.json / package.json / angular.json / a configurable marker).
  2. Send rootUri / workspaceFolders in the initialize request.
  3. 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 send textDocument/didOpen for 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)

View original on GitHub ↗

This issue has 3 comments on GitHub. Read the full discussion on GitHub ↗