Skip to content

Add classified text output for VS Find All References#4030

Open
navya9singh wants to merge 5 commits into
mainfrom
navyasingh/FARbug2799994
Open

Add classified text output for VS Find All References#4030
navya9singh wants to merge 5 commits into
mainfrom
navyasingh/FARbug2799994

Conversation

@navya9singh
Copy link
Copy Markdown
Member

Problem:
Visual Studio uses a custom LSP extension textDocument/_vs_references (instead of the standard textDocument/references) that returns richer data — specifically, grouped reference items with ClassifiedTextRun[] for definition text. This is what powers the colored syntax-highlighted definitions in VS's FAR panel (e.g., showing class Foo with "class" in blue and "Foo" in teal).

The standard LSP textDocument/references only returns Location[] (file + range) — no definition text, no classification. VS needs the extra metadata to render its FAR panel properly, which is why _vs_referencesProvider and _vs_supportsVisualStudioExtensions exist as VS-specific capability extensions
When using Find All References in VS with Corsa, the FAR panel was rendering definition text as flat uncolored "text" instead of properly syntax-highlighted runs. For example, constructor globalClass(): globalClass would appear as a single gray string instead of showing constructor as a keyword (blue), globalClass as a class name (teal), and () as punctuation.

Additionally, 8 FAR test cases were panicking due to nil pointer dereferences when processing triple-slash references and module references in the VS code path.

How it worked in Strada

In the Strada architecture, the tsserver returned pre-classified DisplayParts[] for each definition — an array where every part had a kind (e.g., "keyword", "className", "punctuation", "parameterName"). The C# client-side FindAllReferencesService then mapped each display part kind to a Roslyn ClassificationTypeName via TSSymbolDisplayPart.GetClassificationName():

// TSSymbolDisplayPart.cs

 public string GetClassificationName() => Kind switch {
     "className" => ClassificationTypeNames.ClassName,
     "keyword" => ClassificationTypeNames.Keyword,
     "punctuation" => ClassificationTypeNames.Punctuation,
     "parameterName" => ClassificationTypeNames.ParameterName,
     ...
 };

This produced richly-colored ClassifiedTextRun[] elements that VS rendered with proper syntax highlighting in the FAR panel.

How it works now (Corsa)

In the native LSP server, there is no tsserver returning pre-classified parts. Instead, we have the checker's TypeToStringEx/SignatureToStringEx/SymbolToStringEx which produce flat strings.
The fix uses the same internal machinery that the checker uses — the node builder + printer pipeline — but routes output through the displayPartsWriter which classifies each token by type:

  1. Node builder converts types/signatures into AST nodes (e.g., TypeToTypeNode, SignatureToSignatureDeclaration), and tracks an idToSymbol map linking each synthesized identifier to its original symbol.
  2. Printer walks the AST node and calls typed write methods on the writer (WriteKeyword, WritePunctuation, WriteSymbol, etc.) instead of concatenating a flat string.
  3. displayPartsWriter receives these typed writes and produces ClassifiedTextRun[] with proper classification names.

The printer was previously unable to classify identifiers because the Go port doesn't attach symbols directly to AST nodes (unlike the old TypeScript emitter). We bridge this gap by passing the node builder's idToSymbol map to the printer, enabling it to call WriteSymbol(text, symbol) for identifiers — which then uses classificationForSymbol to determine the correct classification from the symbol's flags.

This produces output like:

 { "ClassificationTypeName": "keyword", "Text": "constructor " },
 { "ClassificationTypeName": "class name", "Text": "globalClass" },

This display writer was created and similar changes were added for SignatureHelp as well in #3821. These are the same fourslash FAR scenarios that already exist in the non-VS test suite, re-run with the VS capability flag enabled. This lets us baseline the ClassifiedTextRun[] output and verify that definitions render with proper per-token classification (keywords, type names, punctuation, etc.) rather than flat text.

Copilot AI review requested due to automatic review settings May 22, 2026 22:27
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Copilot wasn't able to review this pull request because it exceeds the maximum number of files (300). Try reducing the number of changed files and requesting a review from Copilot again.


meaning := getIntersectingMeaningFromDeclarations(originalNode, symbol, ast.SemanticMeaningAll)

info := getQuickInfoAndDeclarationAtLocation(c, symbol, originalNode, nil /*vsCapability */, vsCapability, meaning)
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

This is the equivalent of getSymbolDisplayPartsDocumentationAndSymbolKind() in Strada

Comment thread internal/ls/hover.go
Comment on lines +335 to +352
if !vsCapability {
dpw.Write(c.TypeToStringEx(t, enclosing, flags, vc))
return
}
emitContext := printer.NewEmitContext()
idToSymbol := make(map[*ast.IdentifierNode]*ast.Symbol)
nb := checker.NewNodeBuilderEx(c, emitContext, idToSymbol)
combinedFlags := nodebuilder.Flags(flags&checker.TypeFormatFlagsNodeBuilderFlagsMask) | classifiedNodeBuilderFlags
typeNode := nb.TypeToTypeNode(t, enclosing, combinedFlags, nodebuilder.InternalFlagsNone, nil)
if typeNode == nil {
dpw.Write(c.TypeToStringEx(t, enclosing, flags, vc))
return
}
p := printer.NewPrinter(printer.PrinterOptions{NewLine: core.NewLineKindLF}, printer.PrintHandlers{}, emitContext)
p.IdToSymbol = idToSymbol
tempDpw := newDisplayPartsWriter(true)
p.Write(typeNode, sourceFile, tempDpw, nil)
dpw.WriteFrom(tempDpw)
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

When VS capability is enabled, we can't use TypeToStringEx (which produces a flat string). Instead, we go through NodeBuilder → Printer → displayPartsWriter so each token (keywords, type names, punctuation) gets its own classification for FAR coloring.

@@ -139,6 +139,7 @@ type Printer struct {
makeFileLevelOptimisticUniqueName func(string) string
commentStateArena core.Arena[commentState]
sourceMapStateArena core.Arena[sourceMapState]
IdToSymbol map[*ast.IdentifierNode]*ast.Symbol
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

The NodeBuilder tracks which symbol each synthesized identifier maps to, but the Go port doesn't attach symbols directly to AST nodes like the old TS emitter did. IdToSymbol bridges that gap so the printer can call WriteSymbol (instead of plain Write) for identifiers, enabling proper classification (e.g., "class name", "interface name"). This field is nil by default and only set in VS-capability code paths, so it has no impact on normal printer usage.

Comment on lines 49 to +53
"ClassificationTypeName": "method name",
"Text": "foo"
"Text": "foo",
"MarkerTagType": null,
"Style": 0,
"_vs_type": "ClassifiedTextRun"
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

The sig help baselines changed because ClassifiedTextRun struct now always serializes _vs_type, MarkerTagType, and Style fields (previously omitted with omitzero). The sig help logic itself didn't change — it's just the JSON shape of the same struct that FAR also uses. Without this, VS wouldn't be able to reliably deserialize the runs in the FAR response.

@navya9singh navya9singh changed the title VS Add classified text output for VS Find All References May 22, 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