Skip to content

feat(lsp): JSHint defers to the language server; expose server lifecycle events#2994

Merged
abose merged 9 commits into
mainfrom
ai
Jun 27, 2026
Merged

feat(lsp): JSHint defers to the language server; expose server lifecycle events#2994
abose merged 9 commits into
mainfrom
ai

Conversation

@abose

@abose abose commented Jun 27, 2026

Copy link
Copy Markdown
Member

On desktop the TypeScript language server (vtsls) always lints JS, yet JSHint still also linted plain-JS projects (it only deferred to ESLint), so the Problems panel showed duplicate diagnostics. JSHint now defers to the language server the same way it defers to ESLint: it runs only when it is the sole JS diagnostics source, when a .jshintrc opts it in, or as a fallback when no server is running. ESLint and the language server keep coexisting (rules vs. compiler), like VS Code. Browser is unchanged - no server there, so JSHint stays the JS linter.

  • LSPClient: add isLintingProviderActive(languageId); make the module an EventDispatcher exposing EVENT_LANGUAGE_SERVER_STARTED / _STOPPED for extensions. A new _announceServerStarted() fires STARTED and re-runs inspection on every path that brings a server up (initial start, restart, crash auto-restart) - so linters that defer re-evaluate even on a clean file, whose empty publishDiagnostics wouldn't otherwise trigger a re-run.
  • JSHint: resolve a desktop-only LSPClient handle and defer in canInspect, deriving the language id from LanguageManager.
  • Tests: new "defers JSHint to the language server in a plain JS project" spec; update the two ESLint-failure-fallback specs to expect the LSP (not JSHint) to cover the file on desktop.

abose added 9 commits June 27, 2026 12:17
…cle events

On desktop the TypeScript language server (vtsls) always lints JS, yet JSHint
still also linted plain-JS projects (it only deferred to ESLint), so the Problems
panel showed duplicate diagnostics. JSHint now defers to the language server the
same way it defers to ESLint: it runs only when it is the sole JS diagnostics
source, when a .jshintrc opts it in, or as a fallback when no server is running.
ESLint and the language server keep coexisting (rules vs. compiler), like VS Code.
Browser is unchanged - no server there, so JSHint stays the JS linter.

- LSPClient: add isLintingProviderActive(languageId); make the module an
  EventDispatcher exposing EVENT_LANGUAGE_SERVER_STARTED / _STOPPED for extensions.
  A new _announceServerStarted() fires STARTED and re-runs inspection on every path
  that brings a server up (initial start, restart, crash auto-restart) - so linters
  that defer re-evaluate even on a clean file, whose empty publishDiagnostics
  wouldn't otherwise trigger a re-run.
- JSHint: resolve a desktop-only LSPClient handle and defer in canInspect, deriving
  the language id from LanguageManager.
- Tests: new "defers JSHint to the language server in a plain JS project" spec;
  update the two ESLint-failure-fallback specs to expect the LSP (not JSHint) to
  cover the file on desktop.
…t switch

The server was spawned eagerly on app boot and repointed on every project switch,
regardless of language - so a Python-only project still ran vtsls and re-indexed
it on switch. Mirror VS Code's onLanguage model instead: start the server only
when a served-language (JS/TS/JSX/TSX) file is the active editor, and repoint
(workspace/didChangeWorkspaceFolders) only when a served file is active right
after a project switch - never on ordinary file switches. Once started the server
stays alive idle (no proactive shutdown).

- main.js: _ensureServerForActiveEditor() driven by activeEditorChange + an initial
  evaluation; a pendingRepoint flag (set on EVENT_PROJECT_OPEN) gates the repoint so
  file switches don't touch the workspace-folder/restart machinery. Report a start
  failure via window.logger.reportError, deduped (start is retried lazily). Drop the
  eager appReady start and the now-meaningless readiness flag.
- LSPClient.changeWorkspaceRoot: check same-root first so a redundant call is a no-op
  and never restarts a server that lacks live workspace-folder support.
- tests: the lazy "never started" state isn't observable in the reused integration
  window (keep-alive, no stop path), so don't assert it; drop the readiness-flag wait
  (window load already guarantees the extension's appReady ran).
…e clipped detail

The highlighted auto-import row rendered token.detail inline - a long
"Add import from <module> …" string that clipped to a useless "Add import from
"fs" names…", while non-highlighted rows showed the short module. Skip the inline
signature for auto-imports (their detail is that import line; the doc popup still
shows it in full) and keep the short source-module tag visible on the highlighted
row. In-scope items are unchanged - they still show their type signature inline.
…ctable

Restyle the LSP documentation popup and the parameter-hint popup so all
three code-hint surfaces (completion list, doc popup, parameter hints)
share one visual identity: same menu background, border radius and
shadow, in both light and dark themes.

Redesign the fenced code blocks inside the doc popup as elevated panels
and add a per-block copy button (with copied feedback).

Make the doc popup text selectable: add user-select and an I-beam
cursor, and stop the popup's own mousedown/click from bubbling to
Bootstrap's outside-click handler (which was tearing the popup down
before a drag-select could start). Native selection is preserved
because the handler does not preventDefault.
Replace the separate-coloured signature/code panel with a faint wash
derived from the popup surface (low-opacity black/white), a hairline
border and soft corners. Distinct enough to register as a code region,
quiet enough not to look like a pasted-in card, and consistent across
light and dark themes.
…tive

The legacy Tern engine and vtsls both ran for JS files on desktop: the
priority system meant the LSP's hints always won, but Tern still spun up
its worker and indexed the whole project in parallel - pure waste.

Gate it: JavaScriptCodeHints now skips creating a Tern session (so
ScopeManager never indexes) whenever the language server serves the
file's language, mirroring JSHint's lazy desktop-only LSPClient handle so
the browser build is untouched. The server starts lazily and can come up
after Tern already indexed the first file, so we also reset Tern on
EVENT_LANGUAGE_SERVER_STARTED and let it resume on _STOPPED.
Previously every keystroke (debounced) shipped the entire file to the
language server, ignoring the server's advertised incremental capability -
costly on large files under sustained typing (vtsls re-reads the whole
buffer each time).

Now DocumentSync reads the server's textDocumentSync kind and, when it is
Incremental, accumulates the CodeMirror change records during the debounce
window and sends them as ordered LSP range edits; it falls back to full
text when the server wants full sync, when a change record can't be mapped,
or on a refresh-from-disk. notifyDidChange takes the contentChanges array
so the caller decides the shape.

Adds two integration tests that drive many real edits (sequential inserts,
mid-document replacement, deletion, and a same-line multi-cursor batch) and
use the server's own diagnostics as the sync oracle.
A feature-request flush() can race the document-change listener: it ships
a full update that already includes a keystroke which is also still queued
in pendingChanges, so the debounced incremental send replays that edit and
the server applies it twice - leaving a phantom stray token (e.g. a "}"
reported as "Declaration or statement expected") that no restart cleared.

Fix: before trusting the accumulated incremental edits, replay them on the
last-sent text and require they reproduce the current document exactly;
otherwise send a full resync. This makes divergence impossible regardless
of flush/keystroke ordering. The decision and replay are factored into
pure helpers (_contentChangesFor / _applyIncremental) and covered by a new
unit:DocumentSync suite (14 specs), including the double-apply case.
@abose abose merged commit 4b535ca into main Jun 27, 2026
7 of 21 checks passed
@abose abose deleted the ai branch June 27, 2026 13:43
@sonarqubecloud

Copy link
Copy Markdown

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