Skip to content

Show hover tooltips for dynamic member accesses#3906

Merged
siegfriedpammer merged 9 commits into
masterfrom
dynamic-tooltips
Jul 23, 2026
Merged

Show hover tooltips for dynamic member accesses#3906
siegfriedpammer merged 9 commits into
masterfrom
dynamic-tooltips

Conversation

@siegfriedpammer

Copy link
Copy Markdown
Member

Dynamic member accesses (d.Property, d.Method(...), d[index], new T(dynamicArg)) previously produced no tooltip at all in the text view, because there is no metadata entity behind them. This PR gives them proper hovers by synthesizing a symbol from the encoded callsite:

  • DynamicMemberResolveResult/DynamicInvocationResolveResult gain a Symbol, populated in ExpressionBuilder with a synthesized FakeField/FakeMethod/FakeProperty (member get/set, invoke-member incl. generic type parameters, get/set index, constructor). Parameter/return types come from CSharpArgumentInfo.CompileTimeType where the compiler recorded a static type, else dynamic.
  • A third reference mode is introduced: ReferenceSegment.IsLocal becomes ReferenceMode { Link, LocalHighlight, HoverOnly }. Synthesized dynamic members are HoverOnly — tooltip, arrow cursor, but no navigation and no occurrence highlight, since there is no real member to jump to. Local variables keep the highlight behavior.
  • The dynamic keyword itself and local variables/parameters also get hovers.

The decompiled text output is unchanged; only reference/tooltip metadata is affected. Covered by new HoverOnlyReferenceTests (end-to-end through the headless UI), CSharpAmbienceTests rendering cases, and the existing dynamic pretty tests stay green.

🤖 Generated with Claude Code

Dynamic member accesses and invocations carried only the member name
(DynamicMemberResolveResult / DynamicInvocationResolveResult), so
GetSymbol returned null and the editor emitted no reference or hover.

Synthesize a member on the target type - a dynamic field for a member
access, a dynamic-returning method for a member invocation - named after
the accessed member and typed from the callsite delegate: each argument
uses its recorded compile-time type when the binder set one (statically
typed or constant arguments), dynamic otherwise, and the declaring type
comes from the receiver's argument info. Route these through GetSymbol;
TextTokenWriter and the hover renderer already turn an IEntity into a
tooltip. The synthesized members have no metadata token, so they render
a signature on hover but are not navigation targets.

Assisted-by: Claude:claude-fable-5:Claude Code
A member synthesized for a dynamic access has no metadata token, so the
navigation link it produced went nowhere. Emit it as a local-style
reference (WriteLocalReference) instead: the hover renderer still shows
the signature (it resolves any IEntity reference regardless of IsLocal),
but the identifier is no longer a navigation target, matching how local
variables are treated. The written text is unchanged.

Assisted-by: Claude:claude-fable-5:Claude Code
Local variable references are emitted as reference segments (via
WriteLocalReference), but their reference object is an ILVariable, which
BuildHoverContent could not resolve to an IEntity - so hovering a local
showed nothing. Render the declared type (syntax-highlighted, via a new
Language.GetRichText(IType) overload) with the name and whether it is a
parameter or a local, directly from the ILVariable.

Assisted-by: Claude:claude-fable-5:Claude Code
The dynamic keyword fell through WritePrimitiveType's default arm, so it
was emitted as plain text with no reference segment and thus could not
carry a tooltip - unlike int/string/object, which reference their
metadata type. Emit dynamic as a hover-only reference to
SpecialType.Dynamic (no navigation target, since it has no metadata
definition) and render it in BuildHoverContent.

Assisted-by: Claude:claude-fable-5:Claude Code
A dynamic object creation (new T(b) where an argument is dynamic) gave
its ObjectCreateExpression only a plain ResolveResult(T), so neither the
type name nor the parentheses referenced a constructor - unlike a normal
new expression, whose CSharpInvocationResolveResult lets both hover the
ctor. Synthesize a constructor on the created type (parameters typed from
the callsite delegate) and attach it the same way. Since it has no
metadata, route it hover-only, like the other dynamic members, so the
type name's existing navigation is not replaced by a dead link.

Assisted-by: Claude:claude-fable-5:Claude Code
References had two behaviors encoded in one bool: navigable links, and
"local" references that are non-navigable but highlight all occurrences
on click. Synthesized dynamic members were routed through the latter, so
they picked up the occurrence highlight even though each use is a
distinct synthetic member with nothing to group.

Model the three modes explicitly with a ReferenceMode enum on
ReferenceSegment (Link / LocalHighlight / HoverOnly). Dynamic members,
the dynamic constructor and the dynamic keyword now use HoverOnly: they
still show a hover tooltip (BuildHoverContent resolves any IEntity
reference regardless of mode) but get the arrow cursor, do not navigate,
and do not highlight. Local variables keep LocalHighlight. Plumbed via a
new isHoverOnly flag on ITextOutput.WriteLocalReference.

Assisted-by: Claude:claude-fable-5:Claude Code
A dynamic invocation with explicit type arguments (a.Method<int>())
synthesized a non-generic fake method, so the hover showed Method(...)
without the generic list. Give the fake a matching set of conventionally
named type parameters (T, T2, ...) and specialize it with the actual
arguments, as a real generic call produces a SpecializedMethod, so the
hover shows the method's generic parameters.

Assisted-by: Claude:claude-fable-5:Claude Code
Lock the hover content the dynamic-tooltip work produces. Ambience-level
cases (CSharpAmbienceTests) pin that SpecialType.Dynamic renders as
"dynamic" and that a synthetic dynamic method renders its return and
per-argument types - including the full hover form, confirming the
unnamed synthetic parameters collapse to their types with no dangling
name. An end-to-end case (HoverOnlyReferenceTests) decompiles a dynamic
call and renders the symbol GetSymbol hands back, exercising the actual
synthesis (argument typing from the callsite delegate), not a hand-built
stand-in.

Assisted-by: Claude:claude-fable-5:Claude Code
A dynamic index access (a[b]) gave its IndexerExpression a
DynamicInvocationResolveResult with no symbol, so the brackets carried no
tooltip. Synthesize an indexer (FakeProperty, IsIndexer) on the target
type with the index parameters typed from the callsite delegate, and
attach it. Route it hover-only by detecting a DynamicInvocationResolveResult
directly on the node - which also covers an invoke-member's own
parentheses, so those stop producing a dead navigation link too.

Assisted-by: Claude:claude-fable-5:Claude Code
@siegfriedpammer
siegfriedpammer merged commit a926ae7 into master Jul 23, 2026
13 checks passed
@christophwille
christophwille deleted the dynamic-tooltips branch July 23, 2026 04:57
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