Filed via gh, so the VS Code Editor Issue form wasn't applied — reproducing its fields below. Suggested label: Domain: Editor.
Extension Version
0.20260708.2 (darwin-arm64)
VS Code Version
1.128.0
Operating system Version
macOS 26.5.1 (arm64)
Steps to reproduce
- On macOS, enable the TypeScript Native Preview extension.
- Open a
.ts file containing:
export function clamp(val: number, lo: number, hi: number): number {
return Math.max(lo, Math.min(hi, val))
}
export function parse(input: string): number | null {
let num = parseInt(input)
return isNaN(num) ? null : num
}
console.log(clamp(5, 0, 3))
- Run Developer: Inspect Editor Tokens and Scopes on
console, Math, parseInt, or isNaN.
Issue
On a case-insensitive filesystem, tsgo never sets the defaultLibrary semantic token modifier on any symbol. Token types are correct; only the modifier bit is missing.
Visible effect: globals from lib.*.d.ts lose their themed color, because themes key off defaultLibrary to select the support.* scopes.
Expected (matches typescript@6.0):
console → type variable, modifiers defaultLibrary → support.variable
parseInt → type function, modifiers defaultLibrary → support.function
Actual (tsgo):
console → type variable, modifiers (none) → variable.other.readwrite
parseInt → type function, modifiers (none) → entity.name.function
I drove the extension's own binary (lib/tsc --lsp --stdio) directly as an LSP client, advertising the same semantic-token capabilities VS Code advertises, and decoded textDocument/semanticTokens/full against the server's own legend. Over one ~480-token file: 0 tokens carried defaultLibrary. Not one, for any symbol.
The server's legend is fine — it advertises defaultLibrary at index 9, matching the client list — so encoding and legend negotiation are not implicated.
Verified on macOS. The same defect is expected on Windows by the mechanism below (also case-insensitive, so useCaseSensitiveFileNames is false), but I have not tested it there.
Root cause
internal/ls/semantictokens.go:262 (and again at :268 in the symbol.Declarations fallback):
declSourceFile := ast.GetSourceFileOfNode(decl)
if declSourceFile != nil && program.IsSourceFileDefaultLibrary(tspath.Path(declSourceFile.FileName())) {
tokenModifier |= tokenModifierDefaultLibrary
}
IsSourceFileDefaultLibrary is a map lookup — p.libFiles[path] — and libFiles is keyed by canonical tspath.Path values. Canonicalization runs GetCanonicalFileName, which lowercases the entire path when useCaseSensitiveFileNames is false.
This call site doesn't canonicalize. It string-casts FileName(), which preserves original casing. On macOS the lib files live under a path like /Users/<name>/.vscode/extensions/…/lib/lib.dom.d.ts, while the map key is /users/<name>/…. The capital U alone guarantees the lookup misses, so the modifier is never applied.
On a case-sensitive filesystem GetCanonicalFileName is the identity function, the lookup succeeds, and everything works — which is presumably why this survived CI.
Every other caller in the repo passes the canonical path correctly (program.go:694, rename.go:184, checker/services.go:1120 all use sourceFile.Path()). This is the one place that hand-rolls the cast.
Confirming the cause
Same binary, same source file, same client capabilities. The only variable changed was the casing of the directory holding the lib files:
| lib directory |
tokens with defaultLibrary |
/Users/<name>/.vscode/extensions/…/lib |
0 / 483 |
| copy of that same dir at an all-lowercase path |
102 / 483 |
Relocating the libs to a lowercase path makes console, Math, parseInt, and isNaN all correctly report defaultLibrary.
Suggested fix
Use the source file's canonical path at both sites:
if declSourceFile != nil && program.IsSourceFileDefaultLibrary(declSourceFile.Path()) {
A regression test for this would need to run on a case-insensitive filesystem (or force useCaseSensitiveFileNames: false with a mixed-case lib path) to be meaningful — a Linux-only test would pass either way.
Behavior verified against main @ 8a749379d556.
Extension Version
0.20260708.2(darwin-arm64)VS Code Version
1.128.0Operating system Version
macOS 26.5.1 (arm64)
Steps to reproduce
.tsfile containing:console,Math,parseInt, orisNaN.Issue
On a case-insensitive filesystem,
tsgonever sets thedefaultLibrarysemantic token modifier on any symbol. Token types are correct; only the modifier bit is missing.Visible effect: globals from
lib.*.d.tslose their themed color, because themes key offdefaultLibraryto select thesupport.*scopes.Expected (matches
typescript@6.0):console→ typevariable, modifiersdefaultLibrary→support.variableparseInt→ typefunction, modifiersdefaultLibrary→support.functionActual (
tsgo):console→ typevariable, modifiers (none) →variable.other.readwriteparseInt→ typefunction, modifiers (none) →entity.name.functionI drove the extension's own binary (
lib/tsc --lsp --stdio) directly as an LSP client, advertising the same semantic-token capabilities VS Code advertises, and decodedtextDocument/semanticTokens/fullagainst the server's own legend. Over one ~480-token file: 0 tokens carrieddefaultLibrary. Not one, for any symbol.The server's legend is fine — it advertises
defaultLibraryat index 9, matching the client list — so encoding and legend negotiation are not implicated.Verified on macOS. The same defect is expected on Windows by the mechanism below (also case-insensitive, so
useCaseSensitiveFileNamesis false), but I have not tested it there.Root cause
internal/ls/semantictokens.go:262(and again at:268in thesymbol.Declarationsfallback):IsSourceFileDefaultLibraryis a map lookup —p.libFiles[path]— andlibFilesis keyed by canonicaltspath.Pathvalues. Canonicalization runsGetCanonicalFileName, which lowercases the entire path whenuseCaseSensitiveFileNamesis false.This call site doesn't canonicalize. It string-casts
FileName(), which preserves original casing. On macOS the lib files live under a path like/Users/<name>/.vscode/extensions/…/lib/lib.dom.d.ts, while the map key is/users/<name>/…. The capitalUalone guarantees the lookup misses, so the modifier is never applied.On a case-sensitive filesystem
GetCanonicalFileNameis the identity function, the lookup succeeds, and everything works — which is presumably why this survived CI.Every other caller in the repo passes the canonical path correctly (
program.go:694,rename.go:184,checker/services.go:1120all usesourceFile.Path()). This is the one place that hand-rolls the cast.Confirming the cause
Same binary, same source file, same client capabilities. The only variable changed was the casing of the directory holding the lib files:
defaultLibrary/Users/<name>/.vscode/extensions/…/libRelocating the libs to a lowercase path makes
console,Math,parseInt, andisNaNall correctly reportdefaultLibrary.Suggested fix
Use the source file's canonical path at both sites:
A regression test for this would need to run on a case-insensitive filesystem (or force
useCaseSensitiveFileNames: falsewith a mixed-case lib path) to be meaningful — a Linux-only test would pass either way.Behavior verified against
main@8a749379d556.