Skip to content

[VS] Inlay hints: parameter names and VAR types - #2164

Merged
RobertvanderHulst merged 31 commits into
X-Sharp:devfrom
hpetriffer:feature/inlay-hints
Nov 9, 2026
Merged

RobertvanderHulst merged 31 commits into
X-Sharp:devfrom
hpetriffer:feature/inlay-hints

Conversation

@hpetriffer

Copy link
Copy Markdown
Contributor

Summary

Inlay hints for X# in the VS editor: parameter names at call sites and the inferred type of VAR declarations, the way C# shows its inline hints.

VAR b := Box<INT>{nSize: 3, lFixed: TRUE}
VAR e AS Encoding := System.Text.Encoding.UTF8
o:Open(cFile: "a.dbf", lShared: TRUE)
  • Off by default, like C#. All hints show while Alt+F1 is held down. The X# Intellisense options turn parameter and type hints on permanently.
  • Parameter hints appear only where the argument does not say what it means (a literal, NIL/NULL_*, TRUE/FALSE, Type{...}), and only when exactly one overload fits the call.
  • Type hints appear for VAR, FOREACH VAR and FOR VAR when the initializer does not already show the type and the lookup resolves it. Double-clicking a type hint writes the type into the code.
  • Hover shows the same text as QuickInfo. The colours come from the C# "Inline Hints" item.
  • Only the visible part is computed: the visible lines plus one screen above and below. Scrolling computes the rest.
LanguageService/InlayHints/
├── HintCandidates.cs      # calls and VAR / FOREACH / FOR declarations from the tokens
├── HintRules.cs           # which hints show, and what they say
├── InlayHintComputer.cs   # lookup per candidate in the computed range
├── InlayHintQueue.cs      # one background computation per document, debounced
├── InlayHintRange.cs      # visible range, covered spans, merge on scroll
├── InlayHintTagger.cs     # IntraTextAdornmentTag, Alt+F1
├── InlayHintAdornment.cs  # the hint itself, tooltip, double-click
├── TypeRewrite.cs         # VAR x := ...  ->  LOCAL x := ... AS Type
└── InlayHintTriggers.cs   # settings, Alt+F1, recompute triggers

Changes to shared code that other features use too:

  • Lookup: thread-safe state, and the walker pauses while hints are computed. FOREACH VAR now works over a typed array literal, over a VAR local and over <T>{}.

  • OverloadFilter:

    • untyped parameters make an implicit CLIPPER method;
    • commas inside type arguments no longer split an argument;
    • Foo<T>(...) and Foo<T>{...} are recognised as calls.

    Find References benefits from all three.

  • Find References now finds generic source classes (Box`1) and their constructors.

  • Go To Definition: a keyword is no longer a target. This removes the "Cannot navigate to the symbol under the cursor" message after F12 on a creation.

  • Code model (XProject.prg, ModelWalker.prg): building a source type no longer looks up the file for each member.

Evidence

  • Tests: LanguageService.Tests run 330/330. New test classes:

    • for the hints themselves: HintCandidates, HintRules, InlayHintRange, InlayHintQueue, InlayHintSettings, TypeRewrite and InlayHintColors;
    • LookupThreading and GotoDefinition.

    OverloadFilter, FindReferencesEngine and ReferenceCandidateScanner have new cases.

  • Performance:

    • Before: computing the whole file at once and finding call arguments by name made a large file take seconds, and the scan was quadratic.
    • After: a 775 KB file from a large VO code base takes 80–661 ms per screen.
    • ALargeFileIsScannedInLinearTime guards against the quadratic scan; with it put back in, 4× the calls took 23× as long.
  • Checked by hand in VS 2022:

    • hints with Alt+F1 and with the options;
    • tooltip, double-click edit and colours;
    • generic calls;
    • F12 and Find References on generic classes.

    The project builds for VS 2019 and VS 2022; only VS 2022 was checked by hand.

Merge Danger

Door: two-way

The hints are off by default, and nothing is stored in the user's code unless they double-click a hint.

Blast Radius: language service

The shared lookup, OverloadFilter, SymbolResolver.ForCreation and the code model changed. A regression there would show in Find References, Go To Definition / Implementation or completion, not only in the hints.

Known limitations (deferred)

  • Foo(a < b, c > (d)) is read as type arguments (the same rule as C#).
  • A positional keyword used as a name cannot take type arguments.
  • A static-style call of an extension method gets no parameter hints.
  • Each resolved call still searches the token list twice; this is limited to the computed range.
  • Test gaps: the FOREACH lookup itself, the Box → Box`1 resolution, and the tagger's Alt+F1 flow.

🤖 Generated with Claude Code

hpetriffer and others added 30 commits November 7, 2026 12:09
The squash merge of X-Sharp#2156 on top of X-Sharp#2154 mixed the old body of
FindReferencesCommand.SelectTarget with the Go To Implementation version
(await in a non-async method, duplicate locals, undefined doc/name), so
the language service did not compile. SelectTarget now uses
TargetUnderCursor.Resolve, as Find References did in X-Sharp#2156, and keeps the
TargetSelection result that Rename (X-Sharp#2154) needs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
XSharpLookup.nestedSearches is per thread now: lookups run in the
background (inlay hints, Find References) next to QuickInfo on the UI
thread and shared the recursion guard. ModelWalker.Suspend/Resume use
Interlocked, they are called from several threads.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
HintCandidates finds calls with arguments (Name(...), Type{...}) and
implied declarations (VAR, LOCAL IMPLIED, FOREACH VAR, FOR VAR, USING VAR)
on lexer tokens. Declarations (FUNCTION Foo(...)) and OUT VAR are skipped.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
HintRules decides which hints show: parameter names only for arguments
that say nothing (literals, NIL, NULL_*, signed numbers, Type{...}), only
when exactly one overload fits the call, none beyond the last parameter
or after the first PARAMS argument; extension methods called on an
instance skip their first parameter. Type hints are hidden when the
initializer names the type. Type names are shown without namespaces.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The double-click on a type hint writes the type: VAR x := e and
LOCAL IMPLIED x := e become LOCAL x := e AS T (X# puts the type after the
initializer), FOREACH VAR x IN c becomes FOREACH x AS T IN c. No edit for
FOR, USING, STATIC and statements with several variables.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
InlayHintComputer resolves the candidates of a document snapshot on a
private copy of its tokens (the classifier refills the live document),
inside one ModelWalker.Suspend/Resume, and logs the time per file. A type
hint is silent when the initializer ends in an ambiguous call or the type
is unknown. InlayHintQueue runs one computation at a time for the IDE;
a newer request for the same document replaces a queued one and cancels
a running one; RequestDebounced waits for a quiet second. The classifier
raises ParseCompleted after each parse.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
InlayHintTagger (IViewTaggerProvider, IntraTextAdornmentTag) shows the
hints of the last computed snapshot, translated to the current one; a
hint whose text was edited disappears until the next result. Hover shows
the parameter (with its XML doc and the member prototype) or the type;
a double-click on a type hint writes the type (one undo step). Colours
in Fonts and Colors (X# Inlay Hint). Options: Show parameter name hints,
Show inferred type hints (both on). Open documents are recomputed after
a quiet second when the code model walker is done (new
ModelWalker.WalkCompleted) or another file of the project was saved.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
VO METHOD Open(cName, lShared) accepts any number of arguments, but the
code model only marks CLIPPER when the keyword is written. Inlay hints
were missing for calls with fewer or more arguments (Find References
kept them only by the no-match fallback).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Fonts and Colors stores colours without alpha: the hint background came
back as opaque #808080, the same grey as the text, and the hints showed
as empty grey boxes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… text

The parser stores the initializer text as the type name of a VAR; when
the lookup cannot resolve it, that text stays. FOREACH VAR d IN
<DbFile>{o} showed ' AS <DbFile>{o}' and double-click wrote it out.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
FOREACH VAR d IN <DbFile>{o} now types d as DbFile. VAR list := s:Load()
kept List<T> instead of the declared List<STRING>, so FOREACH VAR item
IN list could not type item; implied assignments now keep the type
arguments of the declaration, as FOREACH variables already did.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Type hint stays silent when the overloads of the final call return
  different types: the lookup takes the first overload, not the one the
  arguments pick (Math.Max(1, 2) said BYTE).
- Type hint stays silent when a type argument does not resolve
  (List<TSource> from ToList(), List<T> from a generic class).
- The double-click edit writes the namespace unless a USING or the
  enclosing namespace brings it in, so the edit compiles.
- The lookup's array type cache is a ConcurrentDictionary: the lookup
  now also runs in the background.
- The hint computation no longer suspends the code model walker; a
  suspend made the walker drop and restart its current project after
  every parse.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The tooltip of a hint was a plain WPF ToolTip with plain text: white
in every theme and without syntax colours. It now uses the editor's
IToolTipService with the same content elements as the X# QuickInfo
(icon + classified text), so it follows the theme and the
classification colours like the C# hints do.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
As in C#: a third option "Show all hints while pressing Alt+F1" (on by
default) shows the switched off kinds while Alt+F1 is held down in an
X# editor. Releasing F1 or Alt, or moving the focus away, hides them
again at once. A key processor tracks the hold; the tagger computes the
missing kinds without the debounce and filters by the current setting.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
XProject.GetType rebuilds a source type from the database on every lookup. For each member,
XDbResult.UpdateLocation looked the file up through XSolution.FindFile, which creates a new XFile
and reads it from the database; GetType then replaced that file with its own cached one. For a
VO class with many members this was most of the time of a lookup.

UpdateLocation now takes the file from the caller: GetType passes the file it resolves per file id,
the extension method path looks its single file up once.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A large file was resolved as a whole before any hint appeared. The tagger now computes the visible
lines and one screen above and below, and when the view scrolls to lines that are not covered it
computes those after a short pause and merges them with the hints of the same snapshot.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
HintCandidates.Calls looked each call name up in the token list to find its arguments. In the
editor the token index does not match the position in that list, so every call searched the whole
list: a 775 KB file spent about a second on this before any call was resolved.

OverloadFilter.Arguments now also takes the position, and the scan passes it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The hints were grey on a translucent grey background in every theme. They now take the colours of
the C# "inline hints" item in Fonts and Colors: the Visual Studio themes colour it, so the hints
follow light and dark like the C# hints, and a colour the user picks for C# applies to both.

Without that item (no Roslyn) the X# item is used, now with the C# defaults. It is no longer shown
in Fonts and Colors, where it would change nothing while the C# item exists.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
VAR d := Dictionary<STRING, INT>{} was split at the comma between the type
arguments into two declarators. The double-click edit then wrote the type
after STRING, in the middle of the type arguments, and the code no longer
compiled. The commas of Name<A, B>{...}, Name<A, B>(...) and <T>{...} now stay
in the initializer; a comparison such as VAR a := x < y, b := 2 still splits.

A generic constructor (VAR l := List<STRING>{}) now counts as a visible type,
like Customer{}, so it no longer gets a redundant type hint.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The element type name of a typed array literal was stored as the type of the
loop variable before the lookup searched it, so an unknown T showed as an
inlay hint (and in QuickInfo). The name is now only taken when the type
resolves; otherwise the variable stays unresolved and gets no hint.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…cument

A pending debounced computation was asked for an older state; after a direct
request for the same document it ran once more for nothing. Request now
cancels it. The CancellationTokenSources of the debounce are disposed when
their delay ends.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…uments

OverloadFilter.Arguments split Foo(Dictionary<STRING, INT>{}, 1) at the comma
between the type arguments and counted three arguments. Find References then
dropped the matching overload, and the inlay hints found no parameter for the
call or shifted the names of a CLIPPER method. The commas of Name<A, B>{...},
Name<A, B>(...) and <T>{...} now stay in their argument; a comparison such as
Foo(a < b, c > d, e) still counts three.

The type argument check moved from HintCandidates to OverloadFilter, where
both use it; it now skips tokens off the default channel.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e name

List<STRING>{10} and o:Foo<INT>(1, 2) had no argument list: the argument
reader and the inlay hint candidates expected ( or { right after the name,
and a generic creation was not narrowed to the constructors. So these calls
got no parameter hints, and Find References did not filter their overloads.

OverloadFilter.ListAfterName finds the ( or { after the name, also after
type arguments; Arguments, SymbolResolver.ForCreation and
HintCandidates.Calls use it. Foo < 3 and Foo < b, c > d stay no call.
Go To Definition on Foo in Foo<T>{1} now reaches the constructor, as it
already did for Foo{1}.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CLASS Box<T> is stored as Box`1. Find References searched the files for
"Box`1", which no source contains, and did not recognise the declaration,
so a generic class had no references at all. The search name and the
declaration check now drop the arity.

The constructor candidates only took Box{...}; Box<INT>{...} is now one too
(OverloadFilter.ListAfterName), so Find References on the CONSTRUCTOR of a
generic class finds its creations.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
F12 on a keyword (the caret on CLASS) showed "Cannot navigate to the symbol
under the cursor"; when nothing is found F12 stays silent, a keyword is now
treated the same. Seen after F12 on Foo{1} through DTE.ExecuteCommand: the
shell sent the command twice, the first call opened the class with the caret
at the start of its line, the second one hit the CLASS keyword.

The deduplication moved into XSharpGotoDefinition.Targets, which also drops
the keywords.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
As C# (Roslyn: EnabledForParameters and EnabledForTypes false,
DisplayAllHintsWhilePressingAltF1 true): parameter and type hints are off
until switched on in Tools - Options; holding Alt+F1 shows both. The
defaults are constants in InlayHintSettings, used by the options page and
as the start values.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
5df8da4 dropped the end check in HintRules.IsConstructor: for an argument
or initializer made only of IDs and dots (Foo(Color.Red), VAR e :=
Encoding.UTF8) it read past the end of the token list. The exception left
the computation, so the whole range showed no hints. Found in review.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Merging with the older hints of the same snapshot kept every old hint the
fresh result had no hint for at the same place, also inside the range just
recomputed. After the walker finished or another file was saved, a call
that had become ambiguous kept its stale parameter name until the next
edit. Inside the freshly covered range only the fresh hints count now.
Found in review.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…time limit in the linear-time test

FinalCallName took the token before the argument list as the call name, so for
VAR x := o:Foo<INT>(1) it found '>' and skipped the check that the call is
unambiguous; the type hint could come from the wrong overload. It now steps back
over the type arguments to the name (checked with OverloadFilter.ListAfterName).

ALargeFileIsScannedInLinearTime had a 1000 ms wall-clock limit that can fail on
the E-cores. It now compares 5000 with 20000 calls (fastest of three runs each):
linear is about 4x, the test allows less than 8x; the quadratic scan it guards
against measured 23x.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
f80b56f says: "Go To Definition on Foo in Foo<T>{1} now reaches the
constructor, as it already did for Foo{1}." That is wrong. Correct is:

Find References on Foo in Foo<T>{1} now resolves to the constructors, as it
already did for Foo{1}; Go To Definition still goes to the class, for Foo{1}
as well.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Conflict in FindReferencesCommand.cs: 7a0507a and X-Sharp#2161 are the same
build fix; took the dev version.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@RobertvanderHulst
RobertvanderHulst merged commit 11f3cae into X-Sharp:dev Nov 9, 2026
1 check passed
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