Skip to content

[VS] Quick actions: Add USING, Generate method, Generate overrides, Convert to property - #2166

Merged
RobertvanderHulst merged 16 commits into
X-Sharp:devfrom
hpetriffer:feature/access-to-property
Nov 9, 2026
Merged

RobertvanderHulst merged 16 commits into
X-Sharp:devfrom
hpetriffer:feature/access-to-property

Conversation

@hpetriffer

Copy link
Copy Markdown
Contributor

Summary

Quick actions (light bulb) for X# in the VS editor, the way C# offers them:

Foo(1)                    -> USING Namespace            (fix, when Foo is a type in another namespace)
SELF:Test(n)              -> Generate method 'Test' in class 'Bar'
on the class header       -> Generate overrides...      (dialog with the overridable base members)
ACCESS Name / ASSIGN Name -> Convert to property
                             Convert all ACCESS/ASSIGN in class 'Bar' to properties
  • Add USING: offered for a name that does not resolve when a type of that name exists in another namespace (project, referenced projects and assemblies). The USING goes after the existing ones.
  • Generate method: for SELF:Foo(), bare calls in a class method and oVar:Foo() on a variable typed as a source class, also when the class is in another file (it is opened). It is offered with the caret on the name, inside the argument list or after the closing parenthesis.
    • The return type comes from the call site, like C#:

      Call Generated
      o:Foo() as a statement AS VOID, body RETURN
      IF o:Foo(), WHILE, CASE, UNTIL, .AND./.OR./.NOT. AS LOGIC
      LOCAL n := o:Foo() AS INT AS INT
      nCount := o:Foo(), SELF:nCount := ... declared type of nCount
      RETURN o:Foo() return type of the enclosing member
      anything else USUAL (OBJECT in Core)
    • A method with a result throws System.NotImplementedException{} until it is written. Parameter names come from the arguments, types from typed arguments.

    • Not offered for names that exist in the class or a base class, classes with NoMethod, SUPER calls, chains, DLL or late-bound receivers, known functions/defines and compiler pseudo functions (PCount, _Args, ...).

  • Generate overrides...: on the class header or a blank line between members. A VO-dialect class can override every method, other dialects virtual/abstract/override members. Members already overridden are skipped, also against DLL bases. Abstract members throw NotImplementedException instead of calling SUPER.
  • Convert to property: on the header of an ACCESS or ASSIGN, for the pair or for all convertible pairs of the class in one undo step.
    • One-line GET expr SET expr when both bodies are a single expression, else GET/SET blocks. The ASSIGN parameter becomes VALUE; the wider visibility goes on the property, the narrower on its accessor.
    • Only offered when the conversion does not change behaviour: not for classes without END CLASS, names a base class has, a pair split across files, differing types/index parameters/modifiers, attributes, a CLASS clause, preprocessor lines inside a member.
LanguageService/LightBulb/QuickActions/
├── QuickActionsProvider.cs   # light-bulb source; caret from Caret.PositionChanged, offers cached per version and position
├── QuickActionContext.cs     # enclosing class, name, receiver, arguments, result use - computed in the background
├── QuickActions.cs           # the actions themselves (insert, open the target file, move the caret)
├── UsingCandidates.cs / UsingInserter.cs
├── GenerateMethodRule.cs / MethodSkeleton.cs / CallResult.cs
├── OverrideCandidates.cs / OverrideSkeleton.cs / OverrideTrigger.cs / OverridesDlg.cs
├── PropertyRule.cs / PropertyConverter.cs / AccessorBody.cs / PropertyEdit.cs / ConvertToPropertyAction.cs
└── CodeStyle.cs              # dialect, keyword case, indentation of the generated code

Only new files plus their entries in both LanguageService project files; no existing code changes.

Evidence

  • Tests: LanguageService.Tests run 356/356 after merging current dev. New test classes in LanguageService.Tests/QuickActions/: Using, GenerateMethodRule, MethodSkeleton, CallResult, Override, OverrideTrigger, PropertyRule, PropertyConverter, AccessorBody, PropertyEdit and ConvertToProperty.
  • Checked by hand in VS 2022 (experimental instance): Add USING, Generate method (return types, caret inside and after the parentheses, target in another file), Generate overrides and Convert to property on a VO and a Core test project. Fixes from that check are in 115dc2f, a4d6791 and ffad58b.
  • The project builds for VS 2019 and VS 2022; only VS 2022 was checked by hand.

Merge Danger

Door: two-way

The actions only change code when the user picks one, and each is one undo step.

Blast Radius: language service

A new light-bulb source; it runs the lookup in the background on caret moves (paused code-model walker, result cached per text version and caret position). An error there would show as a missing or wrong light bulb, not in other features.

The branch was tested against both orders with #2164 (inlay hints): it merges with that branch and with current dev without conflicts.

Known limitations

  • A call used as an argument (Bar(o:Foo())) gets USUAL/OBJECT, not the parameter type of Bar; that needs the overload resolution of [VS] Inlay hints: parameter names and VAR types #2164.
  • With the caret on an identifier passed as an argument (o:Foo(nValue)), the actions are for nValue, not for Foo.

🤖 Generated with Claude Code

hpetriffer and others added 16 commits November 6, 2026 06:33
Pure helpers for the coming "Add USING" light bulb: which namespaces
hold a type of the name under the caret (without the ones the file
already uses or sits in), and where the USING line goes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The text of a method generated from a call: parameter names from the
arguments (p1, p2 otherwise, duplicates numbered), known argument types
or the dialect's untyped type (USUAL, OBJECT in Core), AS VOID in Core,
a CLASS clause for VO-style classes without END CLASS.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
When "Generate method" is offered and into which class: SELF calls and
bare calls go into the enclosing class, oVar:Foo() into the variable's
source class. Not for SUPER, chains, DLL/unknown receivers, names that
exist in the class or a base class, classes with NoMethod, and bare
calls to a known function or define.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The members a class can override (every method of a VO-dialect class,
otherwise virtual/abstract/override ones; nearest base first, members
already declared with the same kind, name and parameter types skipped)
and the text of an override: same signature, REF/OUT kept, SUPER call,
CLIPPER methods via _Args(), ACCESS/ASSIGN and properties.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
One light-bulb source resolves the caret once per text version and
position in the background and offers:
- USING Namespace for a name that does not resolve, when a type of that
  name exists in another namespace (project, referenced projects and
  assemblies);
- Generate method 'Foo' in class 'Bar' for SELF:Foo(), bare calls and
  calls on a variable typed as a source class; the method goes before
  END CLASS or, for VO-style classes, after the last member with a CLASS
  clause, also in another file (which is opened);
- Generate overrides... with a dialog of the overridable base members.
OverrideSkeleton gets the CLASS clause for VO-style classes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Generated VO methods end with RETURN NIL (no /vo9 needed); the caret
  goes on the empty body line.
- Overrides of CLIPPER methods keep CLIPPER and the return type, also
  without parameters; abstract members throw NotImplementedException
  instead of calling SUPER; DLL properties get only their accessors;
  Finalize is not offered.
- Already overridden members are recognised against DLL bases: parameter
  types compared by simple name, ACCESS/ASSIGN matched with properties.
- Generate method is not offered for names the lookup resolves
  (extension methods) or for compiler pseudo functions (PCount, _Args...).
- END CLASS is recognised by its keywords, so a VO-style class whose
  last line is END METHOD no longer gets members inserted inside it.
- No overrides bulb when System.Object members are the only candidates.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- VS passes the caret line as range and calls the source off the UI
  thread: the caret is now taken from Caret.PositionChanged.
- Base classes are resolved like Find References does (BaseType is only
  set by ForceComplete), with the USINGs of the class part in the file.
- In VO an unknown name comes back as an undeclared variable: it counts
  as unresolved for "Add USING" and "Generate method".
- "Add USING" only for names without a receiver (SELF:Missing offered
  USING System.Reflection) and also for types the lenient code model
  finds without their USING.
- FindFunction & co. throw when nothing is found: treated as not found.
- Argument types in X# names (INT instead of System.Int32); generated
  members take the indentation of the methods, not of the fields.
- A log line per light-bulb query when logging is enabled.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
"Generate overrides..." is offered on the class header or on a blank line
between members (followed by a member, a type, END CLASS or the end of
the file), no longer everywhere in the class, and as a refactoring
(screwdriver) instead of a fix. The enclosing class is taken from the
member at the caret only when that member really holds the caret: on a
class header the lookup returned the member before it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The ASSIGN parameter becomes VALUE on identifier tokens only (strings,
comments and longer names stay), and RETURN x becomes RETURN; a longer
expression (RETURN SELF:_c := c) is kept as a statement of its own.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The PROPERTY for an ACCESS/ASSIGN pair: one line (GET expr SET expr) when
both bodies are a single expression on one line, else GET/SET blocks
with the bodies re-indented; ACCESS only or ASSIGN only; index
parameters; the wider visibility on the property, the narrower on its
accessor; a final bare RETURN in the setter is dropped.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Pairs ACCESS and ASSIGN of a class by name and keeps only the pairs that
convert without changing behaviour: not in classes without END CLASS,
not for names a base class has, not when the other part is in another
file, when types, index parameters or non-visibility modifiers differ,
or when a member carries an attribute.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A refactoring on the header of an ACCESS or ASSIGN: "Convert to property"
for the pair, "Convert all ACCESS/ASSIGN in class 'X' to properties" for
every convertible pair of the class, in one undo step. The property
takes the place of the ACCESS (else the ASSIGN); the comments above the
removed member move with it; members in between stay. Headers with other
modifiers than the visibility are left alone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Member names after : . -> :: are not renamed (SELF:oOwner := oOwner
  became SELF:VALUE := VALUE).
- Preprocessor lines (#region, #endif ...) stay outside the moved and
  removed lines; a member with one inside is not converted. Only line
  comments move with a member, block comments are never cut.
- A final RETURN after ";" no longer takes the statement before it.
- RETURN expressions: without calls or assignments they are dropped, an
  assignment or call becomes a statement, anything mixed or a RETURN
  after ";" on its line is not converted.
- The ASSIGN's index names are renamed to the ACCESS's.
- Comments in a body force the GET/SET block form.
- Not offered for headers with other modifiers, a CLASS clause or a ";"
  continuation, for INTERNAL vs PROTECTED, or for members outside the
  class block.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The generated method always has a return type now; it used to have none in
the VO dialect (RETURN NIL) and AS VOID without RETURN elsewhere.

- a call that is a statement of its own: AS VOID, body RETURN
- IF/ELSEIF/WHILE/CASE/UNTIL condition, operand of .AND./.OR./.NOT.: AS LOGIC
- LOCAL x := Foo() AS T: AS T
- x := Foo() / SELF:x := Foo(): the declared type of x
- RETURN Foo(): the return type of the enclosing member
- anything else: USUAL, or OBJECT in Core

A method with a result throws System.NotImplementedException{} until it is
written, as C# does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The light bulb only looked at the name under the caret, so "Generate method"
was offered on Foo in Foo(1, 2) but not with the caret inside the argument
list or after the closing parenthesis. When the caret is on no name, the
innermost call whose argument list holds the caret, or the call that ends the
statement before the caret, is used now. A literal array or a creation {...}
around the caret stops the search.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@RobertvanderHulst
RobertvanderHulst merged commit 67b3288 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