Skip to content

Add Extract to let binding and Extract to literal refactorings - #20537

Open
xperiandri wants to merge 6 commits into
dotnet:mainfrom
xperiandri:feature/extract-let-binding-refactoring
Open

xperiandri wants to merge 6 commits into
dotnet:mainfrom
xperiandri:feature/extract-let-binding-refactoring

Conversation

@xperiandri

@xperiandri xperiandri commented Sep 14, 2026

Copy link
Copy Markdown
Contributor

Offers Ctrl+. on a selected expression to bind it to a let in front of the statement that uses it, or, for a constant in a module-level declaration, to a [<Literal>] above that declaration. A caret inside a constant offers the literal; a caret in the fun … -> header of a parenthesized lambda extracts the lambda.

The value is computed at the start of the enclosing statement, so it can run earlier than where it was written, as with C#'s Introduce Local. The selection must match a whole expression; nothing is offered inside quotations, interpolation holes, while conditions or when guards.

🤖 Generated with Claude Code

…g or a literal

The selected expression is bound to a new name in front of the statement that uses it: above a statement that starts its line, or, when the body of a binding, match clause, branch or lambda shares the line with its keyword, on new lines under that keyword. A constant inside a module-level declaration can instead become a [<Literal>] in front of that declaration. The shared selection and placement logic lives in Refactor/RefactoringHelpers.fs, and the refactoring test framework gains helpers that run a provider on a selected span.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@github-actions

github-actions Bot commented Sep 14, 2026

Copy link
Copy Markdown
Contributor

❗ Release notes required

You can open this PR in browser to add release notes: open in github.dev


✅ Found changes and release notes in following paths:

Change path Release notes path Description
`vsintegration/src` docs/release-notes/.VisualStudio/18.vNext.md


tryGetRefactoringActionsForSpan code (selectionOf code selected) context (new FSharpExtractLetBindingRefactoring())
|> Seq.map _.Title
|> List.ofSeq

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Suggested change
|> List.ofSeq
|> Seq.toList

xperiandri and others added 5 commits September 14, 2026 13:23
C# offers Introduce Constant without a selection. With a caret alone only the
literal is offered; a let binding still needs a selection.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A caret between 'fun' and the end of '->' acts as a selection of the
parenthesized lambda; in the body a caret offers nothing new.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…e of its section

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@xperiandri
xperiandri marked this pull request as ready for review September 14, 2026 19:40
@xperiandri
xperiandri requested a review from a team as a code owner September 14, 2026 19:40
@github-actions github-actions Bot added the ⚠️ Affects-Design-Time Tooling check: PR touches type providers or dependency manager label Sep 14, 2026
@github-actions

Copy link
Copy Markdown
Contributor

🔍 Tooling Safety Check — Affects-Design-Time
Affects-Design-Time: Modifies Visual Studio code executed while projects are open.

Generated by PR Tooling Safety Check · gpt56 1.8M ·

@T-Gro T-Gro left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

🤖🕵️ Please shorten the description using this guidance. Focus on the problem and why the change is needed, in simplified technical English. Leave the implementation inventory to the Files tab and retain necessary caveats.

@github-project-automation github-project-automation Bot moved this from New to In Progress in F# Compiler and Tooling Sep 15, 2026
@xperiandri xperiandri changed the title Refactorings to extract a selected expression into a let binding or a literal Add Extract to let binding and Extract to literal refactorings Sep 15, 2026
@xperiandri

Copy link
Copy Markdown
Contributor Author

Covered cases

Offered

  • An expression inside a statement is bound on the line before it: selecting w * h + 1 in printfn "%d" (w * h + 1)let extracted = w * h + 1 + printfn "%d" (extracted); selecting (w * h + 1) removes the parentheses: printfn "%d" extracted
  • Method-call parentheses stay: selecting (1 + 2) in sb.Append(1 + 2)sb.Append(extracted)
  • A multi-line pipeline on the right of let total = is bound in front of that let as let extracted = + the indented pipeline, and let total = then holds extracted; also when whole lines are selected with their indentation and line break
  • A body sharing its line with -> moves to its own lines: | Some v -> v * 2 + offset and (fun x -> x + 1) become -> followed by let extracted = … and extracted
  • A right-hand side on the = line keeps its trailing comment: let r = compute a b // slowlet extracted = compute a b + extracted // slow
  • Inside a computation expression the binding goes before the statement: return parse raw |> List.length }let extracted = parse raw |> List.length + return extracted }
  • The name avoids existing identifiers: extracted1 when extracted is already used
  • CRLF line endings are kept
  • Lines inside a multi-line triple-quoted string (String.Format("""Hello{0}""", name)) are not re-indented
  • Extract to literal inserts a declaration in front of the module-level let: "Hello %s"[<Literal>] let ExtractedConstant = "Hello %s"; a negative number g -1let ExtractedConstant = -1
  • Which actions appear: for 42 in a module let, both "Extract to let binding" and "Extract to literal"; only the let binding for 42 in member _.M() = 42 and for x + 1
  • With no selection, a caret at the start of, inside (llo %s) or at the end of "Hello %s" gives the literal extraction; a caret inside 42 offers only the literal
  • With no selection, a caret on fun, the parameter, the arrow or right after it in xs |> List.map (fun x -> x + 1) extracts the whole lambda: let extracted = fun x -> x + 1 + xs |> List.map extracted (only the let binding is offered)

Not offered

  • With no selection, a caret in the lambda body (x + 1), + 1)), on the opening (fun parenthesis, or on a lambda without parentheses let f = fun x -> x + 1
  • With no selection, a caret on a non-constant g x, on 42 in member _.M() = 42, or on 1 in the interpolation $"{a + 1}"
  • A selection that is not a whole expression: w * h + in w * h + 1, or 2 + 3 in 1 + 2 + 3
  • A then branch sharing its line with else: x + 1 in if c then x + 1 else y
  • Selections inside a while condition i < n, a when guard v > 0, an interpolation $"{a + b}", or the right operand of &&: s.Length > 0
  • return id in a computation expression, the argument tuple a, b of sb.Insert(a, b), or a lone identifier x in x.ToString() + ""
  • An empty selection outside any constant or parenthesized lambda (let f x = x + 1)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

⚠️ Affects-Design-Time Tooling check: PR touches type providers or dependency manager

Projects

Status: In Progress

Development

Successfully merging this pull request may close these issues.

2 participants