Skip to content

feat: add LSP support for TypeScript/JavaScript via vtsls (desktop only)#2981

Open
abose wants to merge 18 commits into
mainfrom
ai
Open

feat: add LSP support for TypeScript/JavaScript via vtsls (desktop only)#2981
abose wants to merge 18 commits into
mainfrom
ai

Conversation

@abose

@abose abose commented Jun 18, 2026

Copy link
Copy Markdown
Member

Add Language Server Protocol support to the editor, desktop-only (it depends on the Node layer to spawn server processes). The first server is vtsls, bundled in src-node so there is zero user setup. The framework is lazy-loaded so boot stays fast: the Node module is required on demand and the front end is pulled in after boot. All six features are capability-gated and degrade gracefully: completion, hover, jump-to-definition, signature help, diagnostics and find-references.

Architecture

  • src-node/lsp-client.js: NodeConnector-backed ("ph-lsp") multi-server manager speaking JSON-RPC over stdio with Content-Length framing; resolves servers from node_modules/.bin then PATH. Not required at boot (lazy-loaded).
  • src/languageTools/LSPClient.js: browser-side client that exposes the method surface DefaultProviders expects, centralizes {line,ch}<->{character} and path<->file:// URI translation (including the /tauri virtual-path prefix), returns jQuery Deferreds, routes publishDiagnostics into CodeInspection, and auto-restarts a crashed server. Completion results are cached per context.
  • src/languageTools/DocumentSync.js: generic didOpen/didChange(debounced)/ didClose lifecycle. flush() now syncs on actual text difference rather than a pending debounce timer, fixing a race where a completion request (e.g. on ".") could reach the server before the keystroke, returning globals instead of members.
  • src/languageTools/HoverProvider.js + DocumentHighlight.js: hover docs via QuickViewManager and cursor-occurrence highlighting via documentHighlight.
  • src/extensions/default/TypeScriptSupport: thin per-language config; registered via DefaultExtensions.json desktopOnly.

Code hints / insertion

  • DefaultProviders.insertHint anchors the replacement start on the server's textEdit.range.start (stable as you type forward; for members it points at the trigger "." whose dot is part of newText) and ends at the current cursor, fixing both "console..log" and the stale-cache "consolenso".
  • Snippet completions (insertTextFormat 2) are processed properly instead of inserting literal "$1": new reusable editor/TabstopManager expands the LSP snippet grammar ($1/$0, ${1:placeholder}, ${1|a,b|}, variables, escapes), places the caret at the first stop, and starts a Tab/Shift-Tab navigable session (marker-backed, so stops survive later edits such as an auto-import line). additionalTextEdits are applied for real auto-imports. The Emmet expander is left untouched with a pointer comment for future migration.
  • CodeHintList/CodeHintManager: scope row queries to li.code-hints-list-item so documentation-popup markup no longer pollutes the list, clear stale highlights on keyboard nav, and make session begin/update foolproof against duplicate lists.
  • KeyBindingManager: fall back to event.code when event.key is "Unidentified"/ "Dead"/empty so Ctrl-Space works under IME.

UI

  • Theme-matched, opaque scrollbar for the codehint/inlinemenu dropdown; the app-wide transparent track previously let the editor show through the popup's scroll gutter.
  • Themed hover quickview and doc-popup styling.

abose added 6 commits June 18, 2026 11:30
Add Language Server Protocol support to the editor, desktop-only (it depends
on the Node layer to spawn server processes). The first server is vtsls,
bundled in src-node so there is zero user setup. The framework is lazy-loaded
so boot stays fast: the Node module is required on demand and the front end is
pulled in after boot. All six features are capability-gated and degrade
gracefully: completion, hover, jump-to-definition, signature help, diagnostics
and find-references.

Architecture
- src-node/lsp-client.js: NodeConnector-backed ("ph-lsp") multi-server manager
  speaking JSON-RPC over stdio with Content-Length framing; resolves servers
  from node_modules/.bin then PATH. Not required at boot (lazy-loaded).
- src/languageTools/LSPClient.js: browser-side client that exposes the method
  surface DefaultProviders expects, centralizes {line,ch}<->{character} and
  path<->file:// URI translation (including the /tauri virtual-path prefix),
  returns jQuery Deferreds, routes publishDiagnostics into CodeInspection, and
  auto-restarts a crashed server. Completion results are cached per context.
- src/languageTools/DocumentSync.js: generic didOpen/didChange(debounced)/
  didClose lifecycle. flush() now syncs on actual text difference rather than a
  pending debounce timer, fixing a race where a completion request (e.g. on
  ".") could reach the server before the keystroke, returning globals instead
  of members.
- src/languageTools/HoverProvider.js + DocumentHighlight.js: hover docs via
  QuickViewManager and cursor-occurrence highlighting via documentHighlight.
- src/extensions/default/TypeScriptSupport: thin per-language config; registered
  via DefaultExtensions.json desktopOnly.

Code hints / insertion
- DefaultProviders.insertHint anchors the replacement start on the server's
  textEdit.range.start (stable as you type forward; for members it points at the
  trigger "." whose dot is part of newText) and ends at the current cursor,
  fixing both "console..log" and the stale-cache "consolenso".
- Snippet completions (insertTextFormat 2) are processed properly instead of
  inserting literal "$1": new reusable editor/TabstopManager expands the LSP
  snippet grammar ($1/$0, ${1:placeholder}, ${1|a,b|}, variables, escapes),
  places the caret at the first stop, and starts a Tab/Shift-Tab navigable
  session (marker-backed, so stops survive later edits such as an auto-import
  line). additionalTextEdits are applied for real auto-imports. The Emmet
  expander is left untouched with a pointer comment for future migration.
- CodeHintList/CodeHintManager: scope row queries to li.code-hints-list-item so
  documentation-popup markup no longer pollutes the list, clear stale highlights
  on keyboard nav, and make session begin/update foolproof against duplicate
  lists.
- KeyBindingManager: fall back to event.code when event.key is "Unidentified"/
  "Dead"/empty so Ctrl-Space works under IME.

UI
- Theme-matched, opaque scrollbar for the codehint/inlinemenu dropdown; the
  app-wide transparent track previously let the editor show through the popup's
  scroll gutter.
- Themed hover quickview and doc-popup styling.
The desktop LSP support bundles @vtsls/language-server (MIT) and its bundled
typescript (Apache-2.0) in src-node. Add their license files to
src/thirdparty/licences/ and register the copy in the gulp thirdparty-lib-copy
task so builds keep them in sync.
makeConcatExtensions fails on any default extension missing from its
minify lists. Add TypeScriptSupport to minifyableExtensions.
Cover plain text, simple stops ($1/$0/${0}), ordering (positives ascending
with $0 last, mirror dedupe), placeholders incl. nested, choices, variables,
escapes and multi-line offsets. Registered in UnitTestSuite.

Also fix parseSnippet to preserve tab-stops nested inside a placeholder
default (e.g. ${1:a ${2:b} c}), which were previously discarded.
src-node deps aren't installed in the pipeline (the desktop app build
installs them separately), so the copy would fail. The license files are
already vendored under src/thirdparty/licences/; comment out the copy with a
note.
Real-editor (createMockEditor) tests for insertSnippet: single-stop caret
placement, placeholder selection, the import-completion snippet, multi-stop
Tab/Shift-Tab navigation ending at $0, Esc, markers following edits above the
snippet, and range replacement.
@abose abose changed the title feat: add LSP support for desktop (TypeScript/JavaScript via vtsls) feat: add LSP support for TypeScript/JavaScript via vtsls (desktop only) Jun 18, 2026
abose added 12 commits June 18, 2026 11:58
The Adobe-era NodeDomain transport is fully replaced by the new LSP framework
(LSPClient.js + src-node/lsp-client.js) and was no longer referenced by the
app. Remove it and its now-dangling references:

- delete LanguageTools.js, ClientLoader.js, BracketsToNodeInterface.js,
  LanguageClientWrapper.js, DefaultEventHandlers.js, ToolingInfo.json,
  node/RegisterLanguageClientInfo.js and the LanguageClient/ directory
- drop the dead boot requires from test/SpecRunner.js (keep PathConverters
  and DefaultProviders, which the new framework reuses)
- delete the disabled LanguageTools-test.js and its exclusive fixtures
  (clients/, project/, server/); keep css-language-service, still used by
  Extn-CSSCodeHints-integ-test
- remove the disabled-test line from UnitTestSuite.js
- drop the orphaned LANGUAGE_TOOLS_PREFERENCES string
- drop the stale languageTools entries from cacheManifest.json
languageTools is a core module loaded at boot, so its code-hint styles belong
in the core stylesheet rather than a per-extension CSS loaded at runtime.

- move all rules from languageTools/styles/default_provider_style.css into
  src/styles/brackets.less (next to the existing .lsp-hover-quickview /
  .lsp-hint-doc-popup styles), adapting dark variants to .dark & nesting
- delete the css file and the ExtensionUtils.loadStyleSheet call (and the now
  unused ExtensionUtils require) from DefaultProviders.js
The LSP framework grew the minified bundle to ~10.02 MB, tripping the 10 MB
prod limit. Bump to 11 MB to restore the ~1 MB individual-file margin, and
document the margin policy (1 MB per file, 5 MB aggregate).
The TypeScriptSupport extension auto-starts a real vtsls language server and
registers global providers (hover/QuickView, linting/CodeInspection, hints,
jump-to-def) for js/jsx/ts/tsx. In the test runner this polluted pre-existing
integration tests that assume nothing else is registered for those languages:

- QuickView: a removed JS hover provider still returned a popover (ours).
- ESLint: jsx files gained an active inspector, so inspection-disabled was false.
- PreferencesManager: real server spawn + doc-sync churn timed the spec out.

Strip it in test windows, matching the existing convention for the heavyweight
Git extension (excluded for the same reason in ExtensionLoader).
Re-enable the TypeScriptSupport (vtsls) extension in test windows so the
integration/LegacyInteg suites reflect the desktop app, which has LSP-backed
JavaScript/TypeScript intelligence. This supersedes the test-window exclusion
added in 1c4ce4d (only the heavyweight Git extension stays excluded); the
failures that exclusion was hiding are now fixed at their root instead.

Provider fixes (affect the shipping app, not just tests):

- JumpToDefProvider.doJumpToDef now uses the editor that JumpToDefManager passes
  in, instead of EditorManager.getFocusedEditor(). getFocusedEditor() returns
  null whenever the editor lacks DOM focus (jump invoked from a menu/command, or
  under test), which crashed on a null editor and left dirty editor state that
  leaked into later tests.

- LintingProvider no longer triggers CodeInspection.requestRun() when (a) its LSP
  inspector is not a currently-registered provider for the file, or (b) the
  server re-publishes diagnostics identical to what was last surfaced. Servers
  re-publish in waves and on every edit, often unchanged; needlessly re-running
  inspection rebuilds the Problems panel, wasting work and detaching live DOM
  (e.g. inline fix buttons mid-click). It also disrupted tests that take manual
  control of the inspection pipeline with their own mock linters.

Tests adjusted for the desktop-LSP reality (branch on Phoenix.isNativeApp where
JavaScript itself is under test; isolate from the LSP where it is incidental):

- jump-to-definition: vtsls returns the full declaration range, so desktop lands
  at its start; browser still selects the identifier via Tern.
- code hints inside a regex: vtsls returns zero completions, so assert "no hints
  shown" (list absent, or present but closed and empty) rather than strictly null.
- ESLint jsx: the LSP lints .jsx, so on desktop the inspector is active; the
  browser build (no LSP, ESLint v8 declines react) keeps it disabled.
- QuickView per-language register/unregister: switched the example language from
  JavaScript to HTML (not served by the LSP) so the test stays focused on
  registration mechanics with no competing provider.
…lems panel

- Forward the user's UI locale (brackets.getLocale()) as LSP
  InitializeParams.locale on initialize, so vtsls/tsserver emit their diagnostics
  and hover/quick-info text in the user's language. Unknown locales fall back to
  English, so this is safe everywhere.

- Name the CodeInspection provider after the bare server id (e.g. "typescript")
  instead of "<id> (LSP)". The Problems panel read "0 typescript (LSP) Problems";
  to the user this is just built-in language support, and the LSP backing is an
  internal detail they don't need to see. The same id still drives the
  registered-inspector guard, so behaviour is unchanged.
A language server's first publishDiagnostics for a file it has nothing to say
about is an empty set. The diagnostic-change guard treated "never recorded" as
different from "[]", so that initial empty publish counted as a change and fired
CodeInspection.requestRun(), rebuilding the Problems panel. When that rebuild
landed while a fix button was being clicked, the button detached and the fix
never applied (e.g. the ESLint v9 "fix 1 error" integration spec timed out).

Treat an unrecorded file as already-empty, so an initial empty publish is not a
change. Real diagnostics and diagnostic clears still trigger a re-run.
tsserver type-checks JS too and emits the noImplicitAny family (7005-7034,
including 7016 "Could not find a declaration file for module ... implicitly has
an 'any' type. Try `npm i --save-dev @types/...`") as suggestions. For a
developer who hasn't opted into typed JS these are noise - "go install @types"
prompts they never asked for.

Suppress those codes for javascript/jsx files unless the project opts into
type-checking via compilerOptions.checkJs (tsconfig/jsconfig) or a per-file
// @ts-check - matching how VS Code gates JS type diagnostics. Errors, warnings,
unused/deprecated hints, and all type intelligence (hover/completion) are
untouched; TypeScript/TSX is unaffected.

The LSP framework gains a generic per-server `filterDiagnostics` hook applied as
diagnostics arrive. TypeScriptSupport supplies the policy and detects checkJs by
reading tsconfig/jsconfig at the project root (JSONC-aware), re-evaluating on
project switch and when those configs change.
Info-level (META) diagnostics are listed in the panel but weren't counted in the
title, so a file with only suggestions read "0 Problems" above a visible info
row. Count them separately and append "(N Info)" when present, e.g.
"0 typescript Problems - foo.js (2 Info)". Error/warning counts and the existing
title formats are unchanged - the suffix only appears when there are infos.
The test runner loads every default extension's unittests.js. TypeScriptSupport
had none, so the request 404'd and the returned HTML error page was parsed as JS
("SyntaxError: Unexpected token '<'"), reported as a suite-load failure. Add an
empty unittests.js, matching the convention used by other extensions that are
loaded in test windows but ship no unit tests (LightTheme, DarkTheme,
HandlebarsSupport). Git avoids this only because it is excluded from test windows.
@sonarqubecloud

Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
C Reliability Rating on New Code (required ≥ A)

See analysis details on SonarQube Cloud

Catch issues before they fail your Quality Gate with our IDE extension SonarQube for IDE

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.

1 participant