Skip to content

ember-tsc: add fullSemanticMode option for standalone LSP clients#1058

Closed
wagenet wants to merge 6 commits intotyped-ember:mainfrom
wagenet:feat/full-semantic-mode-ember-tsc
Closed

ember-tsc: add fullSemanticMode option for standalone LSP clients#1058
wagenet wants to merge 6 commits intotyped-ember:mainfrom
wagenet:feat/full-semantic-mode-ember-tsc

Conversation

@wagenet
Copy link
Copy Markdown
Contributor

@wagenet wagenet commented Feb 26, 2026

Problem

`@glint/ember-tsc`'s language server delegates TypeScript semantic analysis to a
separate tsserver process (hybrid mode). This works well when tsserver is already
running — VS Code with the TypeScript extension, or editors like Neovim/Zed with a
TypeScript LSP configured.

However, some setups don't run tsserver at all — CI pipelines, type-checking scripts,
or minimal editor configurations — and benefit from a single self-contained process
that handles all TypeScript semantics internally.

Change

Add an opt-in `fullSemanticMode` initialization option. When set to `true` in
`initializationOptions`, the language server uses Volar's built-in
`createTypeScriptProject` + `createTypeScriptSemanticPlugin` instead of tsserver
delegation.

Hybrid mode remains the default. No behavior change for existing users.

```json
// In your LSP client's initialize options:
{ "initializationOptions": { "fullSemanticMode": true } }
```

When to use each mode

  • Hybrid mode (default): When tsserver is already running (VS Code, or Neovim/Zed
    with `ts_ls` configured). TypeScript diagnostics come from tsserver. No duplicate
    work.
  • Full semantic mode: When you want a single standalone process — CI, type-checking
    scripts, or setups without a separate TypeScript LSP. All diagnostics (TypeScript +
    Glimmer template) come from the Glint language server itself.

Memory note

Full semantic mode loads the TypeScript program into the language server process.
Large projects (>5000 files) may need `--max-old-space-size=8192`.

Tests

Added `tests/language-server/full-semantic-mode.test.ts` with 7 tests that
exercise the `fullSemanticMode: true` path directly (no tsserver involved):

  • Type error in template is reported via the language server
  • Valid template produces no diagnostics
  • Syntax error in template is reported
  • Plugin deactivates for projects without a glint config
  • Completions work in full semantic mode
  • Hover info works in full semantic mode
  • Wrong component arg is flagged as an error

Also added test helpers to `test-utils` for starting/tearing down a full-semantic-mode
server instance (`getFullSemanticModeWorkspaceHelper`, `requestFullSemanticModeDiagnostics`, etc.).

Tested on

AuditBoard's Ember codebase — 6682 .gts/.gjs files, full TypeScript + Glimmer
template diagnostics working correctly with `fullSemanticMode: true`.

Dependencies already satisfied

`@glint/ember-tsc@1.1.1` already depends on:

  • `@volar/language-server@~2.4.28` (needs ≥2.4.27 for `createTypeScriptProject`)
  • `volar-service-typescript@~0.0.68` (needs ≥0.0.68 for semantic plugin)

No version bumps required.

@wagenet wagenet marked this pull request as draft February 26, 2026 04:51
@wagenet wagenet force-pushed the feat/full-semantic-mode-ember-tsc branch 2 times, most recently from d9bb108 to 196e25d Compare February 26, 2026 16:15
@wagenet wagenet changed the title ember-tsc: replace hybrid mode with full Volar semantic mode ember-tsc: add fullSemanticMode option for standalone LSP clients Feb 26, 2026
@NullVoxPopuli
Copy link
Copy Markdown
Contributor

NullVoxPopuli commented Feb 26, 2026

Generic LSP clients (Neovim, Zed, CI, other tooling) cannot do this, so they get no TypeScript diagnostics.

this isn't true?

neovim in normal usage:
image

@NullVoxPopuli
Copy link
Copy Markdown
Contributor

claude, don't lie in the PR description and rebase this PR

Replace the manual tsserver-delegation infrastructure with Volar's built-in
createTypeScriptProject + createTypeScriptSemanticPlugin. This is how other
Volar-based language servers (e.g. Volar for Vue) work in semantic mode.

- createTypeScriptProject handles tsconfig discovery and project lifecycle
- createTypeScriptSemanticPlugin provides full TypeScript semantic features
  (diagnostics, completions, hover, go-to-definition, rename, etc.)

Removes tsserver hybrid mode and the manual tsconfigProjects URI map,
file watcher, and sendTsServerRequest plumbing.

Tested on AuditBoard's Ember codebase — 6682 .gts/.gjs files, full
TypeScript + Glimmer template diagnostics working correctly.
@wagenet wagenet force-pushed the feat/full-semantic-mode-ember-tsc branch from 196e25d to 8f8cefe Compare April 7, 2026 19:44
wagenet and others added 2 commits April 7, 2026 15:21
Tests for the `fullSemanticMode: true` initialization path added in the
previous commit (createTypeScriptProject instead of hybrid tsserver delegation).

- Adds test helpers to test-utils: getFullSemanticModeWorkspaceHelper,
  teardownFullSemanticModeWorkspaceAfterEach, prepareDocumentFullSemanticMode,
  requestFullSemanticModeDiagnostics
- Teardown uses a 5s grace period then force-kills since TypeScript project
  loading continues in the background after diagnostic requests return
- 7 tests covering: type errors, valid templates, syntax errors, glint config
  deactivation, completions, hover, and wrong component args

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@wagenet wagenet marked this pull request as ready for review April 7, 2026 22:37
@NullVoxPopuli
Copy link
Copy Markdown
Contributor

Dependencies already satisfied
@glint/ember-tsc@1.1.1 already depends on:

@volar/language-server@~2.4.28 (needs ≥2.4.27 for createTypeScriptProject)
volar-service-typescript@~0.0.68 (needs ≥0.0.68 for semantic plugin)

No version bumps required.

Claude, it isn't your place to suggest to me what versions do or do not need changing. this is a waste of tokens :p lol

const typeError = diagnostics.find((d: any) => d.code === 2551);
expect(typeError).toBeDefined();
expect(typeError.message).toContain('startupTimee');
expect(typeError.message).toContain('startupTime');
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the expectation passes if the there is only startupTimee in the message, such as `startupTimee is undefined

code,
);

const typeErrors = diagnostics.filter((d: any) => d.severity === 1);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why filter? what diagnostics are of non-1 severity?

expect(syntaxError.severity).toBe(1);
});

test('plugin deactivates for project without glint config', async () => {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we support projects with no glint config, but we do require the regular tsserver mode

const hover = await server.sendHoverRequest(document.uri, position);
expect(hover).not.toBeNull();
const hoverText = JSON.stringify(hover?.contents ?? '');
expect(hoverText).toContain('startupTime');
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this would also be true if TS is not active, perhaps this test should assert something else


const argError = diagnostics.find((d: any) => d.code === 2561 || d.code === 2353);
expect(argError).toBeDefined();
expect(argError.message).toContain('target2');
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what about target2?

wagenet and others added 3 commits April 7, 2026 16:56
- Fix substring false positive: use "Did you mean" instead of bare property name
- Simplify valid-template test to assert zero diagnostics
- Verify TS errors still surface when glint deactivates (no glint config)
- Strengthen hover test with type annotation assertion
- Add word-boundary regex to assert expected arg shape in wrong-arg error

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Without the ember language plugin (no glint config), TypeScript has no
extraFileExtensions for .gts and cannot process those files. The 2322
assertion was intermittently passing only when TypeScript happened to
assign the file to another project. Use a plain .ts file instead, which
TypeScript always handles natively regardless of glint config.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…resolution

Without a tsconfig, TypeScript uses inferred/implicit project mode for
in-memory .ts files, making the 2322 assertion intermittently fail.
Adding a tsconfig (no glint section) gives TypeScript a proper project
context so the type-error.ts in-memory document is reliably processed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@wagenet
Copy link
Copy Markdown
Contributor Author

wagenet commented Apr 8, 2026

Closing — the use case this was targeting (standalone LSP clients without a tsserver bridge) is better solved at the client level. For lint-lsp specifically, we now detect @glint/tsserver-plugin and inject it into typescript-language-server's initializationOptions.plugins, matching what neovim does. No changes to ember-tsc needed.

@wagenet wagenet closed this Apr 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants