Skip to content

feat(lsp): support JS code intelligence inside HTML script tags#3003

Merged
abose merged 7 commits into
mainfrom
ai
Jul 2, 2026
Merged

feat(lsp): support JS code intelligence inside HTML script tags#3003
abose merged 7 commits into
mainfrom
ai

Conversation

@abose

@abose abose commented Jul 1, 2026

Copy link
Copy Markdown
Member

Summary

Improves TypeScript/JavaScript code hints by removing noisy documentation popups and enabling JS code intelligence inside HTML <script> blocks.

Details

  • Hides the docs popup when completion detail only repeats the selected hint label.

  • Keeps real signatures and documented completions unaffected.

  • Makes the docs popup sit flush with the hint list.

  • Adds embedded JavaScript LSP support for HTML files:

    • extracts <script> blocks as a JavaScript view
    • preserves line/column positions with blank padding
    • enables completion, hover, signature help, definition, and references inside scripts
  • Starts the TypeScript server lazily for HTML files when needed.

  • Prevents unreliable diagnostics from embedded HTML views.

Tests

  • Added TypeScript LSP integration coverage for:

    • echo-only docs popup suppression
    • meaningful signatures
    • documented completions with trivial signatures
    • JS completions inside HTML <script> blocks

abose added 7 commits July 1, 2026 16:14
Inside a <script> block CodeHintManager already routes to the LSP's JS
provider (getLanguageForSelection() === "javascript"), but the request came
back empty: DocumentSync keys off the document's top-level language ("html"),
so vtsls was never sent the HTML document.

Add an opt-in "embedded language" capability. The TS extension registers
`embeddedLanguages: { html: {...} }`; DocumentSync then presents an HTML
document to the server as a JavaScript *view* of itself - every <script>
block kept verbatim, everything else blanked to spaces with newlines
preserved (reusing HTMLUtils.findBlocks, mirroring the legacy Tern path).
Because the blank-padding keeps the exact line/column layout, a position in
the HTML maps 1:1 to the same position in the view - no source map needed -
so completion/hover/signatureHelp/definition/references all work unchanged.

- TypeScriptSupport/main.js: _extractHtmlJs(editor) + EMBEDDED_LANGUAGES;
  HTML files also start the server lazily.
- DocumentSync.js: embedded docs sync the extracted view with languageId
  "javascript" and always full-sync (the view is a transform).
- LSPClient.js: client carries embeddedLanguages; isLintingProviderActive
  reports embedded hosts (so Tern stands down in HTML); publishDiagnostics
  are dropped for embedded views (unreliable without cross-script context).

Integration test opens an HTML fixture and asserts Array completions at
`arr.` inside a <script>.
The documentation popup anchored at list.right + 6px, which read as a wide
gap between the two floating boxes. Reduce it to 2px so the docs sit snug
against the completion list.
For keyword/plain-identifier completions vtsls returns the label itself as
`detail` (`this` -> "this", `throw` -> "throw"), so the docs popup rendered a
box containing only that word - a verbatim echo of the highlighted hint row.
_docPopupHtml now treats a detail that merely restates the label as "no
signature"; with no documentation either it returns "", and _showDocPopup
hides the popup entirely. Items with a real signature (function foo(): void)
or actual documentation are unaffected.

Also drops the hint-list-to-docs-popup gap to 0 so they sit flush.

Covered in the integration:TypeScript LSP suite: the echo cases, a
meaningful signature, and docs-present-with-a-trivial-signature.
The JS view of an HTML doc (script blocks verbatim, everything else blanked
to spaces, 1:1 positions) was rebuilt by HTMLUtils.findBlocks on every
debounced sync AND every flush() before a feature request - i.e. an
O(document) re-tokenize potentially per keystroke on large HTML files.

New core module languageTools/HtmlJsView keeps an invisible text marker on
each <script>'s JS range; CodeMirror auto-adjusts markers on every edit, so
the view is rebuilt from marker.find() ranges - an O(N) memory copy with no
tokenizer - and memoized until the next edit. findBlocks runs only when the
structure could have changed: a full-buffer replace, an edit whose
inserted/removed text contains '<'/'>' (typing </script> inside a script is
invisible to markers), a marker-boundary touch, or a multi-record change
batch. An idle-time findBlocks parity check self-heals (and console.warns)
any drift, so the view can never silently diverge. extract(editor) keeps the
old stateless contract, so DocumentSync is untouched.

HtmlJsView is preloaded in brackets.js/SpecRunner.js: languageTools/* is
otherwise lazy-loaded, and the extension's synchronous getModule at load
time crashed the whole extension ("Module name ... has not been loaded yet").

Tests (integration:TypeScript LSP) generate a ~2.6k-line, 120-<script> HTML
file programmatically (temp dir, no checked-in asset) and cover: member
completion in the large file, marker-view/findBlocks parity after edits,
zero recompiles across safe in-script keystrokes, structural-edit recompile,
self-heal recovery, and blank-markup/verbatim-JS at 1:1 columns.
…cript>

vtsls sees the embedded script view under the host .html URI, so it doesn't
recognize string context there and answers with a junk "all globals +
auto-imports" dump inside quotes (e.g. console.log("|")).

Add a generic shouldSuppressHints(editor) config hook to the LSP
CodeHintsProvider (same pattern as shouldAutoTrigger/filterDiagnostics); the
TypeScript extension supplies the policy: veto hints when the doc is an
embedded HTML host, the selection language is javascript, and CodeMirror's
token at the cursor is a string/template literal. Plain JS/TS files are
untouched - the server handles their in-string behaviour itself.
The TS-LSP embedded-HTML approach (blanked JS view synced under the .html
URI) hit a hard vtsls limitation: it won't semantically analyze an in-memory
.html document, so bare-dot member completion (arr.) returned a junk
globals/auto-import dump and only prefix-typed completions worked. Tern's
legacy inline-<script> support is simply better here - real member
completion on a bare dot, native in-string suppression, parameter hints,
jump-to-def - and its machinery ("html" in HintUtils.SUPPORTED_LANGUAGES,
Session.getJavascriptText, ScopeManager's blanked-view send) was fully
intact, just gated off. So: the LSP keeps serving real .js/.ts/.jsx/.tsx
files; HTML <script> goes back to Tern.

Two gates were keeping Tern silent in HTML, both opened here:

1. Session gate - isLintingProviderActive("html") was true purely because
   the client declared html in embeddedLanguages, so JavaScriptCodeHints
   never created a Tern session for HTML files. Remove the embedded-HTML
   machinery: EMBEDDED_LANGUAGES + the in-string hint veto (Tern suppresses
   in strings natively) in TypeScriptSupport, the embeddedLanguages field /
   isLintingProviderActive disjunct / publishDiagnostics drop-guard in
   LSPClient, the DocumentSync embedded branch (back to its pre-embedded
   form), and the HtmlJsView module with its brackets.js/SpecRunner preloads
   and tests.

2. Priority starvation - LSP providers are selected by the language at the
   CURSOR ("javascript" inside a <script> or a markdown ```js fence) at
   priority 1 with capability-only checks, so they claimed requests in host
   documents the server never syncs, returned nothing, starved Tern
   (priority 0), and logged "Cannot find provider for hover/signatureHelp"
   errors. New LanguageClient.servesDocument(editor) gates all five feature
   providers (completion, signature help, jump-to-def, references, hover)
   off the DOCUMENT's language, so the LSP stands down in any document it
   doesn't serve - HTML, markdown fences, and future embedded contexts.

The embedded-HTML completion test now asserts at the provider layer (LSP
gate closed for html + Tern's registered provider returns Array members):
the previous SHOW_CODE_HINTS path needs a FOCUSED editor
(CodeHintManager._startNewSession -> getFocusedEditor), which an unfocused
test-runner window never has, so the UI-driven variant could never pass in
automation. integration:TypeScript LSP is 12/12 green.
…create + TS upgrade

The auto-created project config now embeds an autoGeneratedByPhoenixCode
marker object as its first key: a human-readable `doc` (why the file exists,
delete to opt out) and `autoManage: true` with its own doc. autoManage is
the management contract: while true, Phoenix may silently rewrite or upgrade
the file, always preserving the user's compilerOptions; setting it to false
(or any unparseable/unmarked config) makes the file user-owned - Phoenix
never touches it again. TypeScript ignores unknown top-level keys in
ts/jsconfig (ts-node and Angular stash custom fields there), so the marker
is safe.

Because the file now self-documents, the automatic flows go silent:
- Creation shows no toast. Projects that already contain TypeScript get a
  tsconfig.json directly; JS projects get jsconfig.json as before.
- New upgrade path: the first time a TypeScript file becomes active in a
  project scoped by a Phoenix-managed jsconfig.json, it is silently upgraded
  to tsconfig.json (compilerOptions preserved, old jsconfig removed, server
  restarted). A hand-written tsconfig or non-managed jsconfig is never
  touched.
- Any tsconfig we write carries allowJs (JS stays first-class in mixed
  projects) and noEmit (tsconfig, unlike editor-only jsconfig, is read by
  real build tools - an accidental `npx tsc` must never emit files).

The only remaining toast is the Problems-panel "Enable" click - an explicit
action deserves visible confirmation. That flow and the toast's "Enable
TypeScript" (checkJs) button now target whichever config type fits instead
of hardcoding jsconfig.json.

Adds a contract unit test (marker shape, compilerOptions preservation,
checkJs semantics, tsconfig allowJs/noEmit) to the integration:TypeScript
LSP suite via a test-only _configContent export; the event-driven flow stays
inert in test windows. Suite green 13/13; all six UX scenarios verified live
(silent create, upgrade preserving a hand-added checkJs, autoManage:false
hands-off, TS-first direct tsconfig, delete + re-enable toast).
@sonarqubecloud

sonarqubecloud Bot commented Jul 2, 2026

Copy link
Copy Markdown

@abose abose merged commit 67f79d3 into main Jul 2, 2026
7 of 22 checks passed
@abose abose deleted the ai branch July 2, 2026 08:16
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