From 7887e238bcd058ce9d62ae4346df2a4f49c97435 Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Mon, 14 Sep 2026 01:32:25 +0200 Subject: [PATCH 1/9] Add refactorings that extract a selected expression into a let binding 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 [] 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) --- docs/release-notes/.VisualStudio/18.vNext.md | 1 + .../src/FSharp.Editor/FSharp.Editor.fsproj | 2 + .../src/FSharp.Editor/FSharp.Editor.resx | 6 + .../Refactor/ExtractLetBinding.fs | 84 +++ .../Refactor/RefactoringHelpers.fs | 507 ++++++++++++++++++ .../Telemetry/TelemetryReporter.fs | 3 + .../FSharp.Editor/xlf/FSharp.Editor.cs.xlf | 10 + .../FSharp.Editor/xlf/FSharp.Editor.de.xlf | 10 + .../FSharp.Editor/xlf/FSharp.Editor.es.xlf | 10 + .../FSharp.Editor/xlf/FSharp.Editor.fr.xlf | 10 + .../FSharp.Editor/xlf/FSharp.Editor.it.xlf | 10 + .../FSharp.Editor/xlf/FSharp.Editor.ja.xlf | 10 + .../FSharp.Editor/xlf/FSharp.Editor.ko.xlf | 10 + .../FSharp.Editor/xlf/FSharp.Editor.pl.xlf | 10 + .../FSharp.Editor/xlf/FSharp.Editor.pt-BR.xlf | 10 + .../FSharp.Editor/xlf/FSharp.Editor.ru.xlf | 10 + .../FSharp.Editor/xlf/FSharp.Editor.tr.xlf | 10 + .../xlf/FSharp.Editor.zh-Hans.xlf | 10 + .../xlf/FSharp.Editor.zh-Hant.xlf | 10 + .../FSharp.Editor.Tests.fsproj | 1 + .../Refactors/ExtractLetBindingTests.fs | 170 ++++++ .../Refactors/RefactorTestFramework.fs | 27 + 22 files changed, 931 insertions(+) create mode 100644 vsintegration/src/FSharp.Editor/Refactor/ExtractLetBinding.fs create mode 100644 vsintegration/src/FSharp.Editor/Refactor/RefactoringHelpers.fs create mode 100644 vsintegration/tests/FSharp.Editor.Tests/Refactors/ExtractLetBindingTests.fs diff --git a/docs/release-notes/.VisualStudio/18.vNext.md b/docs/release-notes/.VisualStudio/18.vNext.md index ba03f663967..8041eeef17d 100644 --- a/docs/release-notes/.VisualStudio/18.vNext.md +++ b/docs/release-notes/.VisualStudio/18.vNext.md @@ -2,6 +2,7 @@ * Code-fixes for FS3888 (compiler-semantic attribute on the `.fs` but not the `.fsi`): copy the attribute into the `.fsi`, or remove it from the `.fs`. ([Issue #19560](https://github.com/dotnet/fsharp/issues/19560), [PR #19880](https://github.com/dotnet/fsharp/pull/19880)) * Expand `` in IDE tooltips, completion, and signature help, inheriting XML documentation from base classes, interfaces, overridden members, and constructors. ([Issue #19175](https://github.com/dotnet/fsharp/issues/19175), [PR #19188](https://github.com/dotnet/fsharp/pull/19188)) +* **Extract to let binding** and **Extract to literal** refactorings for a selected expression. The value is bound in front of the statement that uses it, so it is computed before the code that preceded the selection in that statement; a constant in a module-level declaration becomes a `[]` in front of that declaration. ### Fixed diff --git a/vsintegration/src/FSharp.Editor/FSharp.Editor.fsproj b/vsintegration/src/FSharp.Editor/FSharp.Editor.fsproj index 319bdd5a264..4759355028a 100644 --- a/vsintegration/src/FSharp.Editor/FSharp.Editor.fsproj +++ b/vsintegration/src/FSharp.Editor/FSharp.Editor.fsproj @@ -101,10 +101,12 @@ + + diff --git a/vsintegration/src/FSharp.Editor/FSharp.Editor.resx b/vsintegration/src/FSharp.Editor/FSharp.Editor.resx index 1f1f632d770..b645ff73aa8 100644 --- a/vsintegration/src/FSharp.Editor/FSharp.Editor.resx +++ b/vsintegration/src/FSharp.Editor/FSharp.Editor.resx @@ -368,4 +368,10 @@ Use live (unsaved) buffers for analysis Returns: + + Extract to let binding + + + Extract to literal + \ No newline at end of file diff --git a/vsintegration/src/FSharp.Editor/Refactor/ExtractLetBinding.fs b/vsintegration/src/FSharp.Editor/Refactor/ExtractLetBinding.fs new file mode 100644 index 00000000000..947e5c4871a --- /dev/null +++ b/vsintegration/src/FSharp.Editor/Refactor/ExtractLetBinding.fs @@ -0,0 +1,84 @@ +// Copyright (c) Microsoft Corporation. All Rights Reserved. See License.txt in the project root for license information. + +namespace Microsoft.VisualStudio.FSharp.Editor + +open System.Composition + +open Microsoft.CodeAnalysis.CodeActions +open Microsoft.CodeAnalysis.CodeRefactorings +open Microsoft.CodeAnalysis.Formatting +open Microsoft.CodeAnalysis.Text +open Microsoft.VisualStudio.FSharp.Editor.Telemetry + +open FSharp.Compiler.Syntax + +open RefactoringHelpers +open CancellableTasks + +[] +type internal FSharpExtractLetBindingRefactoring [] () = + inherit CodeRefactoringProvider() + + static let register + (context: CodeRefactoringContext) + (sourceText: SourceText) + (title: string) + (kind: string) + (changes: TextChange list) + = + let changedDocument = + cancellableTask { + TelemetryReporter.ReportSingleEvent( + TelemetryEvents.RefactoringActivated, + [| "name", box (nameof FSharpExtractLetBindingRefactoring); "kind", box kind |] + ) + + return context.Document.WithText(sourceText.WithChanges changes) + } + + context.RegisterRefactoring(CodeAction.Create(title, changedDocument, title)) + + override _.ComputeRefactoringsAsync context = + cancellableTask { + let document = context.Document + + if not (context.Span.IsEmpty || document.IsFSharpSignatureFile) then + let! cancellationToken = CancellableTask.getCancellationToken () + let! sourceText = document.GetTextAsync cancellationToken + let! parseResults = document.GetFSharpParseResultsAsync(nameof FSharpExtractLetBindingRefactoring) + + match tryExtractionTarget sourceText parseResults.ParseTree context.Span with + | ValueNone -> () + | ValueSome target -> + let! options = document.GetOptionsAsync cancellationToken + + let indentSize = + options.GetOption(FormattingOptions.IndentationSize, FSharpConstants.FSharpLanguageName) + + let names = usedNames parseResults.ParseTree + let literalLines = linesInsideLiterals parseResults.ParseTree + + match anchorsOf target.Expr target.Path with + | anchor :: _ -> + let name = uniqueName "extracted" names + + match tryDeclareInFront sourceText target anchor $"let {name}" name indentSize literalLines with + | ValueSome changes -> register context sourceText (SR.ExtractToLetBinding()) "let" changes + | ValueNone -> () + | [] -> () + + let constant = + match target.Expr with + | SynExpr.Paren(expr = inner) -> inner + | expr -> expr + + if isLiteralConstant constant then + let name = uniqueName "ExtractedConstant" names + + match + tryDeclareInFrontOfModuleLet sourceText target [ "[]" ] $"let {name}" name indentSize literalLines + with + | ValueSome changes -> register context sourceText (SR.ExtractToLiteral()) "literal" changes + | ValueNone -> () + } + |> CancellableTask.startAsTask context.CancellationToken diff --git a/vsintegration/src/FSharp.Editor/Refactor/RefactoringHelpers.fs b/vsintegration/src/FSharp.Editor/Refactor/RefactoringHelpers.fs new file mode 100644 index 00000000000..f69c579a364 --- /dev/null +++ b/vsintegration/src/FSharp.Editor/Refactor/RefactoringHelpers.fs @@ -0,0 +1,507 @@ +// Copyright (c) Microsoft Corporation. All Rights Reserved. See License.txt in the project root for license information. + +module internal Microsoft.VisualStudio.FSharp.Editor.RefactoringHelpers + +open System +open System.Collections.Generic +open System.Text + +open Microsoft.CodeAnalysis.Text + +open FSharp.Compiler.Syntax +open FSharp.Compiler.SyntaxTrivia +open FSharp.Compiler.Text + +/// A place in front of which a declaration used by the selected expression can be inserted. +[] +type Anchor = + /// An expression that is a statement of its block. + | Statement of start: pos + /// The body of a binding, match clause, branch or lambda, after the keyword ending at keywordEnd. + | Clause of keywordEnd: pos * body: range + +[] +type ExtractionTarget = + { + Expr: SynExpr + Path: SyntaxVisitorPath + /// The selection without its parentheses when the selected expression is parenthesized. + Content: TextSpan + /// The text the new name replaces; the parentheses of a method call stay. + Replaced: TextSpan + } + +let private isSame (expr: SynExpr) (other: SynExpr) = obj.ReferenceEquals(expr, other) + +let private hasName (name: string) (ident: Ident) = + String.Equals(ident.idText, name, StringComparison.Ordinal) + +let textSpanOf (sourceText: SourceText) (m: range) = + RoslynHelpers.FSharpRangeToTextSpan(sourceText, m) + +let positionOf (sourceText: SourceText) (offset: int) = + let linePosition = sourceText.Lines.GetLinePosition offset + Position.mkPos (Line.fromZ linePosition.Line) linePosition.Character + +let leadingSpaces (sourceText: SourceText) (line: TextLine) = + let mutable position = line.Start + + while position < line.End && sourceText[position] = ' ' do + position <- position + 1 + + position - line.Start + +let lineBreakOf (sourceText: SourceText) = + sourceText.Lines + |> Seq.tryFind (fun line -> line.EndIncludingLineBreak > line.End) + |> Option.map (fun line -> sourceText.ToString(TextSpan.FromBounds(line.End, line.EndIncludingLineBreak))) + |> Option.defaultValue Environment.NewLine + +let isLineLeading (sourceText: SourceText) (position: pos) = + let line = sourceText.Lines[Line.toZ position.Line] + leadingSpaces sourceText line = position.Column + +/// Whether only closing brackets and a line comment follow the position on its line. +let restOfLineIsClosers (sourceText: SourceText) (position: pos) = + let line = sourceText.Lines[Line.toZ position.Line] + + let rest = + sourceText.ToString(TextSpan.FromBounds(line.Start + position.Column, line.End)) + + let code = + match rest.IndexOf("//", StringComparison.Ordinal) with + | -1 -> rest + | comment -> rest.Substring(0, comment) + + code + |> Seq.forall (fun c -> Char.IsWhiteSpace c || c = ')' || c = ']' || c = '}' || c = '|') + +let linesInsideLiterals (parseTree: ParsedInput) = + (HashSet(), parseTree) + ||> ParsedInput.fold (fun lines _ node -> + match node with + | SyntaxNode.SynExpr(SynExpr.Const(range = m)) + | SyntaxNode.SynExpr(SynExpr.InterpolatedString(range = m)) + | SyntaxNode.SynPat(SynPat.Const(range = m)) -> + for line in m.StartLine + 1 .. m.EndLine do + lines.Add(Line.toZ line) |> ignore + | _ -> () + + lines) + +let usedNames (parseTree: ParsedInput) = + (HashSet(StringComparer.Ordinal), parseTree) + ||> ParsedInput.fold (fun names _ node -> + match node with + | SyntaxNode.SynExpr(SynExpr.Ident ident) + | SyntaxNode.SynExpr(SynExpr.LongIdent(longDotId = SynLongIdent(id = ident :: _))) + | SyntaxNode.SynPat(SynPat.Named(ident = SynIdent(ident, _))) + | SyntaxNode.SynPat(SynPat.LongIdent(longDotId = SynLongIdent(id = ident :: _))) -> names.Add ident.idText |> ignore + | _ -> () + + names) + +let uniqueName (baseName: string) (used: HashSet) = + Seq.initInfinite (fun i -> if i = 0 then baseName else $"{baseName}{i}") + |> Seq.find (used.Contains >> not) + +/// The text of the span with every line after the first moved by as many columns as the first line moves to reach +/// column. Lines inside multi-line string literals are kept as they are; a line that would need a negative indentation +/// makes the whole text unavailable. +let tryIndentedText (sourceText: SourceText) (span: TextSpan) (column: int) (literalLines: HashSet) = + let lines = sourceText.Lines + let first = lines.GetLineFromPosition span.Start + let last = lines.GetLineFromPosition span.End + let shift = column - (span.Start - first.Start) + + let text = + StringBuilder(sourceText.ToString(TextSpan.FromBounds(span.Start, min first.End span.End))) + + let rec append lineNumber = + if lineNumber > last.LineNumber then + ValueSome(text.ToString()) + else + let previous = lines[lineNumber - 1] + let line = lines[lineNumber] + + let content = + sourceText.ToString(TextSpan.FromBounds(line.Start, min line.End span.End)) + + let moved = + match literalLines.Contains lineNumber with + | true -> ValueSome content + | false when String.IsNullOrWhiteSpace content -> ValueSome content + | false when shift >= 0 -> ValueSome(String(' ', shift) + content) + | false when leadingSpaces sourceText line >= -shift -> ValueSome(content.Substring(-shift)) + | false -> ValueNone + + match moved with + | ValueSome moved -> + text.Append(sourceText.ToString(TextSpan.FromBounds(previous.End, previous.EndIncludingLineBreak))).Append(moved) + |> ignore + + append (lineNumber + 1) + | ValueNone -> ValueNone + + append (first.LineNumber + 1) + +let private trimmed (sourceText: SourceText) (span: TextSpan) = + let mutable start = span.Start + let mutable finish = span.End + + while start < finish && Char.IsWhiteSpace sourceText[start] do + start <- start + 1 + + while finish > start && Char.IsWhiteSpace sourceText[finish - 1] do + finish <- finish - 1 + + TextSpan.FromBounds(start, finish) + +let private directiveLine (directive: ConditionalDirectiveTrivia) = + match directive with + | ConditionalDirectiveTrivia.If(range = m) + | ConditionalDirectiveTrivia.Elif(range = m) + | ConditionalDirectiveTrivia.Else(range = m) + | ConditionalDirectiveTrivia.EndIf(range = m) -> m.StartLine + +let private isOperator (name: string) (expr: SynExpr) = + match expr with + | SynExpr.LongIdent(longDotId = SynLongIdent(id = [ operator ])) -> hasName name operator + | _ -> false + +let private isCall (path: SyntaxVisitorPath) = + match path with + | SyntaxNode.SynExpr(SynExpr.App(flag = ExprAtomicFlag.Atomic) | SynExpr.New _) :: _ -> true + | _ -> false + +let private isMethodArgument (path: SyntaxVisitorPath) = + match path with + | SyntaxNode.SynExpr(SynExpr.Paren _) :: call + | SyntaxNode.SynExpr(SynExpr.Tuple _) :: SyntaxNode.SynExpr(SynExpr.Paren _) :: call -> isCall call + | _ -> false + +let private isExtractableShape (expr: SynExpr) (path: SyntaxVisitorPath) = + match expr with + | SynExpr.App(isInfix = true) + | SynExpr.Ident _ + | SynExpr.LongIdent(longDotId = SynLongIdent(id = [ _ ])) + | SynExpr.Const(SynConst.Unit, _) + | SynExpr.Paren(rightParenRange = None) + | SynExpr.ComputationExpr _ + | SynExpr.YieldOrReturn _ + | SynExpr.YieldOrReturnFrom _ + | SynExpr.DoBang _ + | SynExpr.MatchBang _ + | SynExpr.WhileBang _ + | SynExpr.ImplicitZero _ + | SynExpr.SequentialOrImplicitYield _ + | SynExpr.JoinIn _ + | SynExpr.ArbitraryAfterError _ + | SynExpr.FromParseError _ + | SynExpr.DiscardAfterMissingQualificationAfterDot _ + | SynExpr.Typar _ + | SynExpr.TraitCall _ + | SynExpr.IndexRange _ + | SynExpr.IndexFromEnd _ + | SynExpr.Fixed _ + | SynExpr.AddressOf _ + | SynExpr.Do _ + | SynExpr.Dynamic _ + | SynExpr.LongIdentSet _ + | SynExpr.Set _ + | SynExpr.DotSet _ + | SynExpr.DotIndexedSet _ + | SynExpr.NamedIndexedPropertySet _ + | SynExpr.DotNamedIndexedPropertySet _ -> false + | SynExpr.LetOrUse letOrUse -> not letOrUse.IsBang + | SynExpr.Tuple _ -> not (isMethodArgument path) + | SynExpr.App(isInfix = false; funcExpr = SynExpr.App(isInfix = true; funcExpr = equals; argExpr = SynExpr.Ident _)) when + isOperator "op_Equality" equals + -> + not (isMethodArgument path) + | _ -> true + +let private isInExcludedContext (expr: SynExpr) (path: SyntaxVisitorPath) = + let rec loop (child: SyntaxNode) (path: SyntaxVisitorPath) = + match path, child with + | [], _ -> false + | SyntaxNode.SynExpr(SynExpr.InterpolatedString _ | SynExpr.Quote _ | SynExpr.Lazy _) :: _, _ -> true + | SyntaxNode.SynExpr(SynExpr.While(whileExpr = condition) | SynExpr.WhileBang(whileExpr = condition)) :: _, SyntaxNode.SynExpr expr when + isSame condition expr + -> + true + | SyntaxNode.SynMatchClause(SynMatchClause(whenExpr = Some guard)) :: _, SyntaxNode.SynExpr expr when isSame guard expr -> true + | SyntaxNode.SynExpr(SynExpr.App(isInfix = false; funcExpr = SynExpr.App(isInfix = true; funcExpr = operator); argExpr = right)) :: _, + SyntaxNode.SynExpr expr when + isSame right expr + && (isOperator "op_BooleanAnd" operator || isOperator "op_BooleanOr" operator) + -> + true + | parent :: rest, _ -> loop parent rest + + loop (SyntaxNode.SynExpr expr) path + +/// Whether the expression contains a computation expression construct outside any computation expression of its own. +let private hasStatementOnlyConstruct (expr: SynExpr) = + let containers = ResizeArray() + let statements = ResizeArray() + + [ SyntaxNode.SynExpr expr ] + |> SyntaxNodes.fold + (fun () _ node -> + match node with + | SyntaxNode.SynExpr(SynExpr.ComputationExpr(range = m) | SynExpr.ArrayOrListComputed(range = m)) -> containers.Add m + | SyntaxNode.SynExpr(SynExpr.YieldOrReturn(range = m) | SynExpr.YieldOrReturnFrom(range = m) | SynExpr.DoBang(range = m) | SynExpr.MatchBang( + range = m) | SynExpr.WhileBang(range = m) | SynExpr.JoinIn(range = m)) -> statements.Add m + | SyntaxNode.SynExpr(SynExpr.LetOrUse letOrUse) when letOrUse.IsBang -> statements.Add letOrUse.Range + | _ -> ()) + () + + statements + |> Seq.exists (fun statement -> + not ( + containers + |> Seq.exists (fun container -> Range.rangeContainsRange container statement) + )) + +let tryExtractionTarget (sourceText: SourceText) (parseTree: ParsedInput) (selection: TextSpan) = + let span = trimmed sourceText selection + + match parseTree with + | ParsedInput.ImplFile file when not span.IsEmpty -> + let start = positionOf sourceText span.Start + let finish = positionOf sourceText span.End + + let crossesDirective = + file.Trivia.ConditionalDirectives + |> List.exists (fun directive -> + let line = directiveLine directive + line >= start.Line && line <= finish.Line) + + let exact = + (start, parseTree) + ||> ParsedInput.tryPickLast (fun path node -> + match node with + | SyntaxNode.SynExpr expr when Position.posEq expr.Range.Start start && Position.posEq expr.Range.End finish -> + Some(expr, path) + | _ -> None) + + match exact with + | Some(expr, path) when + not crossesDirective + && isExtractableShape expr path + && not (isInExcludedContext expr path) + && not (hasStatementOnlyConstruct expr) + -> + let content = + match expr with + | SynExpr.Paren(expr = inner) -> textSpanOf sourceText inner.Range + | _ -> span + + let replaced = + match expr with + | SynExpr.Paren _ when isCall path -> content + | _ -> span + + ValueSome + { + Expr = expr + Path = path + Content = content + Replaced = replaced + } + | _ -> ValueNone + | _ -> ValueNone + +let private isFunctionBinding (binding: SynBinding) = + match binding with + | SynBinding(headPat = SynPat.LongIdent(argPats = SynArgPats.Pats(_ :: _))) -> true + | _ -> false + +let private anchorOf (child: SyntaxNode) (parent: SyntaxNode) (grandparent: SyntaxNode voption) = + match parent, child with + | SyntaxNode.SynExpr(SynExpr.Sequential _), SyntaxNode.SynExpr expr -> ValueSome(Anchor.Statement expr.Range.Start) + | SyntaxNode.SynExpr(SynExpr.LetOrUse letOrUse), SyntaxNode.SynExpr expr when isSame letOrUse.Body expr -> + ValueSome(Anchor.Statement expr.Range.Start) + | SyntaxNode.SynExpr(SynExpr.For(doBody = body) | SynExpr.ForEach(bodyExpr = body) | SynExpr.While(doExpr = body) | SynExpr.TryWith( + tryExpr = body) | SynExpr.TryFinally(tryExpr = body) | SynExpr.ComputationExpr(expr = body)), + SyntaxNode.SynExpr expr when isSame body expr -> ValueSome(Anchor.Statement expr.Range.Start) + | SyntaxNode.SynModule(SynModuleDecl.Expr _), SyntaxNode.SynExpr expr -> ValueSome(Anchor.Statement expr.Range.Start) + | SyntaxNode.SynBinding(SynBinding(expr = rhs; trivia = trivia) as binding), SyntaxNode.SynExpr expr when isSame rhs expr -> + match grandparent, trivia.EqualsRange with + | ValueSome(SyntaxNode.SynExpr(SynExpr.LetOrUse letOrUse)), _ when not letOrUse.IsRecursive && not (isFunctionBinding binding) -> + ValueSome(Anchor.Statement letOrUse.Range.Start) + | _, Some equals -> ValueSome(Anchor.Clause(equals.End, expr.Range)) + | _ -> ValueNone + | SyntaxNode.SynMatchClause(SynMatchClause(resultExpr = result; trivia = trivia)), SyntaxNode.SynExpr expr when isSame result expr -> + match trivia.ArrowRange with + | Some arrow -> ValueSome(Anchor.Clause(arrow.End, expr.Range)) + | None -> ValueNone + | SyntaxNode.SynExpr(SynExpr.IfThenElse(thenExpr = thenExpr; elseExpr = elseExpr; trivia = trivia)), SyntaxNode.SynExpr expr -> + match elseExpr, trivia.ElseKeyword with + | _ when isSame thenExpr expr -> ValueSome(Anchor.Clause(trivia.ThenKeyword.End, expr.Range)) + | Some elseExpr, Some elseKeyword when isSame elseExpr expr -> ValueSome(Anchor.Clause(elseKeyword.End, expr.Range)) + | _ -> ValueNone + | SyntaxNode.SynExpr(SynExpr.Lambda(parsedData = Some(_, body); trivia = trivia)), SyntaxNode.SynExpr expr when isSame body expr -> + match trivia.ArrowRange with + | Some arrow -> ValueSome(Anchor.Clause(arrow.End, expr.Range)) + | None -> ValueNone + | _ -> ValueNone + +/// The places a declaration used by the expression can go, innermost first. +let anchorsOf (expr: SynExpr) (path: SyntaxVisitorPath) = + let rec loop (child: SyntaxNode) (path: SyntaxVisitorPath) = + match path with + | [] -> [] + | parent :: rest -> + let grandparent = + match rest with + | node :: _ -> ValueSome node + | [] -> ValueNone + + match anchorOf child parent grandparent with + | ValueSome anchor -> anchor :: loop parent rest + | ValueNone -> loop parent rest + + loop (SyntaxNode.SynExpr expr) path + +let isLiteralConstant (expr: SynExpr) = + match expr with + | SynExpr.Const((SynConst.Bool _ | SynConst.SByte _ | SynConst.Byte _ | SynConst.Int16 _ | SynConst.UInt16 _ | SynConst.Int32 _ | SynConst.UInt32 _ | SynConst.Int64 _ | SynConst.UInt64 _ | SynConst.IntPtr _ | SynConst.UIntPtr _ | SynConst.Single _ | SynConst.Double _ | SynConst.Char _ | SynConst.String _), + _) -> true + | _ -> false + +/// The module-level let declaration containing the path, with its first binding. +let tryEnclosingModuleLet (path: SyntaxVisitorPath) = + path + |> List.tryPick (function + | SyntaxNode.SynModule(SynModuleDecl.Let(bindings = binding :: _; range = m)) -> Some struct (binding, m) + | _ -> None) + +let private padding (width: int) = String(' ', width) + +/// `header = `, with a multi-line content starting on its own line at bodyColumn. +let private tryDeclaration (sourceText: SourceText) (content: TextSpan) (header: string) (bodyColumn: int) literalLines lineBreak = + tryIndentedText sourceText content bodyColumn literalLines + |> ValueOption.map (fun body -> + let lines = sourceText.Lines + + if lines.GetLineFromPosition(content.Start).LineNumber = lines.GetLineFromPosition(content.End).LineNumber then + $"{header} = {body}" + else + $"{header} ={lineBreak}{padding bodyColumn}{body}") + +/// Changes that declare `header = ` in front of the anchor and put replacement where the selection was. +/// A body that shares its line with its keyword moves to new lines under that keyword, when only closing brackets +/// and a comment follow it. +let tryDeclareInFront + (sourceText: SourceText) + (target: ExtractionTarget) + (anchor: Anchor) + (header: string) + (replacement: string) + (indentSize: int) + (literalLines: HashSet) + = + let lines = sourceText.Lines + let lineBreak = lineBreakOf sourceText + let replacementChange = TextChange(target.Replaced, replacement) + + let inFrontOfLine (start: pos) = + let line = lines[Line.toZ start.Line] + + tryDeclaration sourceText target.Content header (start.Column + indentSize) literalLines lineBreak + |> ValueOption.map (fun declaration -> + [ + TextChange(TextSpan(line.Start, 0), $"{padding start.Column}{declaration}{lineBreak}") + replacementChange + ]) + + match anchor with + | Anchor.Statement start when isLineLeading sourceText start -> inFrontOfLine start + | Anchor.Clause(_, body) when isLineLeading sourceText body.Start -> inFrontOfLine body.Start + | Anchor.Clause(keywordEnd, body) when keywordEnd.Line = body.StartLine && restOfLineIsClosers sourceText body.End -> + let keywordLine = lines[Line.toZ keywordEnd.Line] + let keywordEndOffset = keywordLine.Start + keywordEnd.Column + let bodySpan = textSpanOf sourceText body + let newIndent = leadingSpaces sourceText keywordLine + indentSize + let shift = newIndent - body.StartColumn + + let continuation = + [ + for lineNumber in Line.toZ body.StartLine + 1 .. Line.toZ body.EndLine do + let line = lines[lineNumber] + + let keep = + literalLines.Contains lineNumber + || String.IsNullOrWhiteSpace(line.ToString()) + || (line.Start >= target.Replaced.Start && line.Start <= target.Replaced.End) + + if not keep then + line + ] + + let movable = + continuation + |> List.forall (fun line -> shift >= 0 || leadingSpaces sourceText line >= -shift) + + let separatedByWhitespace = + String.IsNullOrWhiteSpace(sourceText.ToString(TextSpan.FromBounds(keywordEndOffset, bodySpan.Start))) + + if movable && separatedByWhitespace then + tryDeclaration sourceText target.Content header (newIndent + indentSize) literalLines lineBreak + |> ValueOption.map (fun declaration -> + [ + TextChange( + TextSpan.FromBounds(keywordEndOffset, bodySpan.Start), + $"{lineBreak}{padding newIndent}{declaration}{lineBreak}{padding newIndent}" + ) + + for line in continuation do + if shift > 0 then + TextChange(TextSpan(line.Start, 0), padding shift) + elif shift < 0 then + TextChange(TextSpan(line.Start, -shift), "") + + replacementChange + ] + |> List.sortBy _.Span.Start) + else + ValueNone + | _ -> ValueNone + +/// Changes that declare `header = `, preceded by the attribute lines, in front of the module-level let +/// containing the selection, and put replacement where the selection was. +let tryDeclareInFrontOfModuleLet + (sourceText: SourceText) + (target: ExtractionTarget) + (attributes: string list) + (header: string) + (replacement: string) + (indentSize: int) + (literalLines: HashSet) + = + match tryEnclosingModuleLet target.Path with + | Some(struct (SynBinding(xmlDoc = xmlDoc), declaration)) -> + let firstLine = + if xmlDoc.IsEmpty then + declaration.StartLine + else + min declaration.StartLine xmlDoc.Range.StartLine + + let line = sourceText.Lines[Line.toZ firstLine] + let indent = leadingSpaces sourceText line + let lineBreak = lineBreakOf sourceText + + tryDeclaration sourceText target.Content header (indent + indentSize) literalLines lineBreak + |> ValueOption.map (fun declaration -> + let attributeLines = + attributes + |> List.map (fun attribute -> $"{padding indent}{attribute}{lineBreak}") + |> String.concat "" + + [ + TextChange(TextSpan(line.Start, 0), $"{attributeLines}{padding indent}{declaration}{lineBreak}{lineBreak}") + TextChange(target.Replaced, replacement) + ]) + | None -> ValueNone diff --git a/vsintegration/src/FSharp.Editor/Telemetry/TelemetryReporter.fs b/vsintegration/src/FSharp.Editor/Telemetry/TelemetryReporter.fs index 230f4988438..a5bf5e6b4ef 100644 --- a/vsintegration/src/FSharp.Editor/Telemetry/TelemetryReporter.fs +++ b/vsintegration/src/FSharp.Editor/Telemetry/TelemetryReporter.fs @@ -18,6 +18,9 @@ module TelemetryEvents = [] let CodefixActivated = "codefixactivated" + [] + let RefactoringActivated = "refactoringactivated" + [] let Hints = "hints" diff --git a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.cs.xlf b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.cs.xlf index cd8c46bf705..9b94eafd87e 100644 --- a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.cs.xlf +++ b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.cs.xlf @@ -105,6 +105,16 @@ Navrhnout názvy pro nerozpoznané identifikátory; Použít místo negace odčítání + + Extract to let binding + Extract to let binding + + + + Extract to literal + Extract to literal + + F# Disposable Values (locals) Uvolnitelné hodnoty jazyka F# (místní) diff --git a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.de.xlf b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.de.xlf index bce1941f0b1..8ce1754b360 100644 --- a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.de.xlf +++ b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.de.xlf @@ -105,6 +105,16 @@ Namen für nicht aufgelöste Bezeichner vorschlagen; Subtraktion anstelle von Negation verwenden + + Extract to let binding + Extract to let binding + + + + Extract to literal + Extract to literal + + F# Disposable Values (locals) Disposable-Werte in F# (lokal) diff --git a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.es.xlf b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.es.xlf index fa8cb62c422..501be2d5e7c 100644 --- a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.es.xlf +++ b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.es.xlf @@ -105,6 +105,16 @@ Sugerir nombres para identificadores sin resolver; Usar la resta en lugar de la negación + + Extract to let binding + Extract to let binding + + + + Extract to literal + Extract to literal + + F# Disposable Values (locals) Valores de F# descartables (locales) diff --git a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.fr.xlf b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.fr.xlf index e7ec71e839e..8b8d9a57638 100644 --- a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.fr.xlf +++ b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.fr.xlf @@ -105,6 +105,16 @@ Suggérer des noms pour les identificateurs non résolus ; Utiliser la soustraction à la place de la négation + + Extract to let binding + Extract to let binding + + + + Extract to literal + Extract to literal + + F# Disposable Values (locals) Valeurs F# pouvant être supprimées (variables locales) diff --git a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.it.xlf b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.it.xlf index 327a7ca362f..35e4cca8ad4 100644 --- a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.it.xlf +++ b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.it.xlf @@ -105,6 +105,16 @@ Suggerisci i nomi per gli identificatori non risolti; Usare la sottrazione invece della negazione + + Extract to let binding + Extract to let binding + + + + Extract to literal + Extract to literal + + F# Disposable Values (locals) Valori eliminabili F# (variabili locali) diff --git a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.ja.xlf b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.ja.xlf index d45234c011a..928004aa616 100644 --- a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.ja.xlf +++ b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.ja.xlf @@ -105,6 +105,16 @@ Suggest names for unresolved identifiers; 否定の代わりに減算を使用する + + Extract to let binding + Extract to let binding + + + + Extract to literal + Extract to literal + + F# Disposable Values (locals) F# の破棄可能な値 (ローカル) diff --git a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.ko.xlf b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.ko.xlf index 3248e0641ea..c104880c6d9 100644 --- a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.ko.xlf +++ b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.ko.xlf @@ -105,6 +105,16 @@ Suggest names for unresolved identifiers; 부정 대신 빼기 사용 + + Extract to let binding + Extract to let binding + + + + Extract to literal + Extract to literal + + F# Disposable Values (locals) F# 삭제 가능한 값(로컬) diff --git a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.pl.xlf b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.pl.xlf index abc39f15da5..4a27d371123 100644 --- a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.pl.xlf +++ b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.pl.xlf @@ -105,6 +105,16 @@ Sugeruj nazwy dla nierozpoznanych identyfikatorów; Użyj odejmowania zamiast negacji + + Extract to let binding + Extract to let binding + + + + Extract to literal + Extract to literal + + F# Disposable Values (locals) Wartości możliwe do likwidacji języka F# (lokalne) diff --git a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.pt-BR.xlf b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.pt-BR.xlf index dfde43120f5..ee0729dde99 100644 --- a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.pt-BR.xlf +++ b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.pt-BR.xlf @@ -105,6 +105,16 @@ Sugerir nomes para identificadores não resolvidos; Use a subtração em vez da negação + + Extract to let binding + Extract to let binding + + + + Extract to literal + Extract to literal + + F# Disposable Values (locals) Valores F# Descartáveis (locais) diff --git a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.ru.xlf b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.ru.xlf index 47cda215312..69476255afe 100644 --- a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.ru.xlf +++ b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.ru.xlf @@ -105,6 +105,16 @@ Suggest names for unresolved identifiers; Используйте вычитание вместо отрицания. + + Extract to let binding + Extract to let binding + + + + Extract to literal + Extract to literal + + F# Disposable Values (locals) Освобождаемые значения F# (локальные) diff --git a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.tr.xlf b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.tr.xlf index 58aa5d54c43..802990076ba 100644 --- a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.tr.xlf +++ b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.tr.xlf @@ -105,6 +105,16 @@ Kullanılmayan değerleri analiz et ve bunlara düzeltmeler öner; Negatif yapma yerine çıkarmayı kullanın + + Extract to let binding + Extract to let binding + + + + Extract to literal + Extract to literal + + F# Disposable Values (locals) F# Atılabilir Değerleri (yereller) diff --git a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.zh-Hans.xlf b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.zh-Hans.xlf index 4fa703776fb..ace8f6ec72e 100644 --- a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.zh-Hans.xlf +++ b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.zh-Hans.xlf @@ -105,6 +105,16 @@ Suggest names for unresolved identifiers; 使用减法代替求反 + + Extract to let binding + Extract to let binding + + + + Extract to literal + Extract to literal + + F# Disposable Values (locals) F# 可释放值(局部值) diff --git a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.zh-Hant.xlf b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.zh-Hant.xlf index fd46ef9919a..e2191749e38 100644 --- a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.zh-Hant.xlf +++ b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.zh-Hant.xlf @@ -105,6 +105,16 @@ Suggest names for unresolved identifiers; 使用減號代替否定 + + Extract to let binding + Extract to let binding + + + + Extract to literal + Extract to literal + + F# Disposable Values (locals) F# 可處置的值 (區域) diff --git a/vsintegration/tests/FSharp.Editor.Tests/FSharp.Editor.Tests.fsproj b/vsintegration/tests/FSharp.Editor.Tests/FSharp.Editor.Tests.fsproj index ecce1205b8c..67a671126da 100644 --- a/vsintegration/tests/FSharp.Editor.Tests/FSharp.Editor.Tests.fsproj +++ b/vsintegration/tests/FSharp.Editor.Tests/FSharp.Editor.Tests.fsproj @@ -74,6 +74,7 @@ + diff --git a/vsintegration/tests/FSharp.Editor.Tests/Refactors/ExtractLetBindingTests.fs b/vsintegration/tests/FSharp.Editor.Tests/Refactors/ExtractLetBindingTests.fs new file mode 100644 index 00000000000..e30e122c27b --- /dev/null +++ b/vsintegration/tests/FSharp.Editor.Tests/Refactors/ExtractLetBindingTests.fs @@ -0,0 +1,170 @@ +module FSharp.Editor.Tests.Refactors.ExtractLetBindingTests + +open System + +open Microsoft.CodeAnalysis.Text + +open Microsoft.VisualStudio.FSharp.Editor +open Microsoft.VisualStudio.FSharp.Editor.CancellableTasks + +open Xunit + +open FSharp.Editor.Tests.Refactors.RefactorTestFramework + +let private extractToLetBinding = "Extract to let binding" +let private extractToLiteral = "Extract to literal" + +let private selectionOf (code: string) (selected: string) = + TextSpan(code.IndexOf(selected, StringComparison.Ordinal), selected.Length) + +let private extracted (title: string) (code: string) (selected: string) = + use context = TestContext.CreateWithCode code + + let document = + refactorSpan code (selectionOf code selected) title context (new FSharpExtractLetBindingRefactoring()) + + let parseResults = + document.GetFSharpParseResultsAsync "test" + |> CancellableTask.runSynchronouslyWithoutCancellation + + Assert.Empty(parseResults.Diagnostics) + (document.GetTextAsync() |> GetTaskResult).ToString() + +let private titlesFor (code: string) (selected: string) = + use context = TestContext.CreateWithCode code + + tryGetRefactoringActionsForSpan code (selectionOf code selected) context (new FSharpExtractLetBindingRefactoring()) + |> Seq.map _.Title + |> List.ofSeq + +[] +[] +[] +let ``Expression in a statement is bound in front of it`` (selected: string, body: string) = + let code = "module M\n\nlet area w h =\n printfn \"%d\" (w * h + 1)\n" + Assert.Equal($"module M\n\nlet area w h =\n{body}", extracted extractToLetBinding code selected) + +[] +let ``Parentheses of a method call stay`` () = + let code = + "module M\n\nlet append (sb: System.Text.StringBuilder) =\n sb.Append(1 + 2)\n" + + let expected = + "module M\n\nlet append (sb: System.Text.StringBuilder) =\n let extracted = 1 + 2\n sb.Append(extracted)\n" + + Assert.Equal(expected, extracted extractToLetBinding code "(1 + 2)") + +[] +let ``Multi-line right-hand side is bound in front of its let`` () = + let code = + "module M\n\nlet run items =\n let mutable acc = 0\n let total =\n items\n |> List.filter (fun i -> i > acc)\n |> List.sum\n total\n" + + let expected = + "module M\n\nlet run items =\n let mutable acc = 0\n let extracted =\n items\n |> List.filter (fun i -> i > acc)\n |> List.sum\n let total =\n extracted\n total\n" + + Assert.Equal(expected, extracted extractToLetBinding code "items\n |> List.filter (fun i -> i > acc)\n |> List.sum") + +[] +let ``Match clause body on the arrow line moves to its own lines`` () = + let code = + "module M\n\nlet f x offset =\n match x with\n | Some v -> v * 2 + offset\n | None -> 0\n" + + let expected = + "module M\n\nlet f x offset =\n match x with\n | Some v ->\n let extracted = v * 2 + offset\n extracted\n | None -> 0\n" + + Assert.Equal(expected, extracted extractToLetBinding code "v * 2 + offset") + +[] +let ``Right-hand side on the equals line keeps its trailing comment`` () = + let code = "module M\n\nlet r = compute a b // slow\n" + + let expected = + "module M\n\nlet r =\n let extracted = compute a b\n extracted // slow\n" + + Assert.Equal(expected, extracted extractToLetBinding code "compute a b") + +[] +let ``Expression in a computation expression is bound in front of its statement`` () = + let code = + "module M\n\nlet load id = async {\n let! raw = fetch id\n return parse raw |> List.length }\n" + + let expected = + "module M\n\nlet load id = async {\n let! raw = fetch id\n let extracted = parse raw |> List.length\n return extracted }\n" + + Assert.Equal(expected, extracted extractToLetBinding code "parse raw |> List.length") + +[] +let ``Lambda body on the arrow line moves to its own lines`` () = + let code = "module M\n\nlet r = xs |> List.map (fun x -> x + 1)\n" + + let expected = + "module M\n\nlet r = xs |> List.map (fun x ->\n let extracted = x + 1\n extracted)\n" + + Assert.Equal(expected, extracted extractToLetBinding code "x + 1") + +[] +let ``Name does not collide with an existing identifier`` () = + let code = + "module M\n\nlet extracted = 0\n\nlet f x =\n printfn \"%d\" (x + 1)\n" + + let expected = + "module M\n\nlet extracted = 0\n\nlet f x =\n let extracted1 = x + 1\n printfn \"%d\" (extracted1)\n" + + Assert.Equal(expected, extracted extractToLetBinding code "x + 1") + +[] +let ``Line breaks of the file are kept`` () = + let code = "module M\r\n\r\nlet f x =\r\n printfn \"%d\" (x + 1)\r\n" + + let expected = + "module M\r\n\r\nlet f x =\r\n let extracted = x + 1\r\n printfn \"%d\" (extracted)\r\n" + + Assert.Equal(expected, extracted extractToLetBinding code "x + 1") + +[] +let ``Lines inside a multi-line string are not re-indented`` () = + let code = + "module M\n\nlet f name =\n printfn \"%s\" (String.Format(\"\"\"Hello\n{0}\"\"\", name))\n" + + let expected = + "module M\n\nlet f name =\n let extracted =\n String.Format(\"\"\"Hello\n{0}\"\"\", name)\n printfn \"%s\" (extracted)\n" + + Assert.Equal(expected, extracted extractToLetBinding code "String.Format(\"\"\"Hello\n{0}\"\"\", name)") + +[] +let ``Constant in a module-level declaration becomes a literal in front of it`` () = + let code = "module M\n\nlet greet name =\n printfn \"Hello %s\" name\n" + + let expected = + "module M\n\n[]\nlet ExtractedConstant = \"Hello %s\"\n\nlet greet name =\n printfn ExtractedConstant name\n" + + Assert.Equal(expected, extracted extractToLiteral code "\"Hello %s\"") + +[] +let ``Negative number becomes a literal`` () = + let code = "module M\n\nlet f () = g -1\n" + + let expected = + "module M\n\n[]\nlet ExtractedConstant = -1\n\nlet f () = g ExtractedConstant\n" + + Assert.Equal(expected, extracted extractToLiteral code "-1") + +[] +let ``Literal is offered only for constants outside types`` () = + Assert.Equal([ extractToLetBinding; extractToLiteral ], titlesFor "module M\n\nlet f () = g 42\n" "42") + Assert.Equal([ extractToLetBinding ], titlesFor "module M\n\ntype T() =\n member _.M() = 42\n" "42") + Assert.Equal([ extractToLetBinding ], titlesFor "module M\n\nlet f x = g (x + 1)\n" "x + 1") + +[] +[] +[] +[] +[] +[ 0 -> v\n | _ -> 0\n", "v > 0")>] +[] +[] +[] +[] +[ 0\n", "s.Length > 0")>] +[] +let ``No action`` (code: string, selected: string) = Assert.Empty(titlesFor code selected) diff --git a/vsintegration/tests/FSharp.Editor.Tests/Refactors/RefactorTestFramework.fs b/vsintegration/tests/FSharp.Editor.Tests/Refactors/RefactorTestFramework.fs index 849da8c84ec..4aad2b130d1 100644 --- a/vsintegration/tests/FSharp.Editor.Tests/Refactors/RefactorTestFramework.fs +++ b/vsintegration/tests/FSharp.Editor.Tests/Refactors/RefactorTestFramework.fs @@ -87,3 +87,30 @@ let tryGetRefactoringActions (code: string) (cursorPosition) (context: TestConte } |> CancellableTask.startWithoutCancellation |> fun task -> task.Result + +let private refactoringActionsAt (code: string) (span: TextSpan) (context: TestContext) (refactorProvider: CodeRefactoringProvider) = + let actions = List() + let existingDocument = RoslynTestHelpers.GetLastDocument context.Solution + context.Solution <- context.Solution.WithDocumentText(existingDocument.Id, SourceText.From(code)) + let document = RoslynTestHelpers.GetLastDocument context.Solution + + let refactoringContext = + CodeRefactoringContext(document, span, (fun action -> actions.Add action), context.CancellationToken) + + refactorProvider.ComputeRefactoringsAsync(refactoringContext).GetAwaiter().GetResult() + actions + +let tryGetRefactoringActionsForSpan (code: string) (span: TextSpan) (context: TestContext) (refactorProvider: #CodeRefactoringProvider) = + refactoringActionsAt code span context refactorProvider + +let refactorSpan (code: string) (span: TextSpan) (title: string) (context: TestContext) (refactorProvider: #CodeRefactoringProvider) = + let action = + refactoringActionsAt code span context refactorProvider + |> Seq.find (fun action -> String.Equals(action.Title, title, StringComparison.Ordinal)) + + for operation in action.GetOperationsAsync(context.CancellationToken) |> GetTaskResult do + let applyChanges = operation :?> ApplyChangesOperation + applyChanges.Apply(context.Solution.Workspace, context.CancellationToken) + context.Solution <- applyChanges.ChangedSolution + + RoslynTestHelpers.GetLastDocument context.Solution From 9859db667906effd5ada9b7a10c73d89f7481fb8 Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Mon, 14 Sep 2026 01:49:33 +0200 Subject: [PATCH 2/9] Add refactorings that extract a selected expression into a local function, a module function or a private member The values the selection reads from the enclosing member or module-level declaration become parameters in the order they are first used; module values, members and 'this' stay where they are. A member that uses 'this' or 'base' can extract into a private member called on its self identifier. Parameter type annotations follow a new Code Fixes option: always, only for receivers of member and indexer accesses and operator operands, or never. A selection that assigns to a captured mutable local, or captures a byref-like value, is not offered. Co-Authored-By: Claude Opus 5 (1M context) --- docs/release-notes/.VisualStudio/18.vNext.md | 1 + .../src/FSharp.Editor/FSharp.Editor.fsproj | 1 + .../src/FSharp.Editor/FSharp.Editor.resx | 9 + .../FSharp.Editor/Options/EditorOptions.fs | 20 +- .../FSharp.Editor/Refactor/ExtractFunction.fs | 352 ++++++++++++++++++ .../Refactor/RefactoringHelpers.fs | 2 +- .../FSharp.Editor/xlf/FSharp.Editor.cs.xlf | 15 + .../FSharp.Editor/xlf/FSharp.Editor.de.xlf | 15 + .../FSharp.Editor/xlf/FSharp.Editor.es.xlf | 15 + .../FSharp.Editor/xlf/FSharp.Editor.fr.xlf | 15 + .../FSharp.Editor/xlf/FSharp.Editor.it.xlf | 15 + .../FSharp.Editor/xlf/FSharp.Editor.ja.xlf | 15 + .../FSharp.Editor/xlf/FSharp.Editor.ko.xlf | 15 + .../FSharp.Editor/xlf/FSharp.Editor.pl.xlf | 15 + .../FSharp.Editor/xlf/FSharp.Editor.pt-BR.xlf | 15 + .../FSharp.Editor/xlf/FSharp.Editor.ru.xlf | 15 + .../FSharp.Editor/xlf/FSharp.Editor.tr.xlf | 15 + .../xlf/FSharp.Editor.zh-Hans.xlf | 15 + .../xlf/FSharp.Editor.zh-Hant.xlf | 15 + .../CodeFixesOptionControl.xaml | 7 + .../FSharp.UIResources/Strings.Designer.cs | 36 ++ .../src/FSharp.UIResources/Strings.resx | 12 + .../src/FSharp.UIResources/xlf/Strings.cs.xlf | 20 + .../src/FSharp.UIResources/xlf/Strings.de.xlf | 20 + .../src/FSharp.UIResources/xlf/Strings.es.xlf | 20 + .../src/FSharp.UIResources/xlf/Strings.fr.xlf | 20 + .../src/FSharp.UIResources/xlf/Strings.it.xlf | 20 + .../src/FSharp.UIResources/xlf/Strings.ja.xlf | 20 + .../src/FSharp.UIResources/xlf/Strings.ko.xlf | 20 + .../src/FSharp.UIResources/xlf/Strings.pl.xlf | 20 + .../FSharp.UIResources/xlf/Strings.pt-BR.xlf | 20 + .../src/FSharp.UIResources/xlf/Strings.ru.xlf | 20 + .../src/FSharp.UIResources/xlf/Strings.tr.xlf | 20 + .../xlf/Strings.zh-Hans.xlf | 20 + .../xlf/Strings.zh-Hant.xlf | 20 + .../FSharp.Editor.Tests.fsproj | 1 + .../Refactors/ExtractFunctionTests.fs | 127 +++++++ 37 files changed, 1021 insertions(+), 2 deletions(-) create mode 100644 vsintegration/src/FSharp.Editor/Refactor/ExtractFunction.fs create mode 100644 vsintegration/tests/FSharp.Editor.Tests/Refactors/ExtractFunctionTests.fs diff --git a/docs/release-notes/.VisualStudio/18.vNext.md b/docs/release-notes/.VisualStudio/18.vNext.md index 8041eeef17d..f237efc3cd5 100644 --- a/docs/release-notes/.VisualStudio/18.vNext.md +++ b/docs/release-notes/.VisualStudio/18.vNext.md @@ -3,6 +3,7 @@ * Code-fixes for FS3888 (compiler-semantic attribute on the `.fs` but not the `.fsi`): copy the attribute into the `.fsi`, or remove it from the `.fs`. ([Issue #19560](https://github.com/dotnet/fsharp/issues/19560), [PR #19880](https://github.com/dotnet/fsharp/pull/19880)) * Expand `` in IDE tooltips, completion, and signature help, inheriting XML documentation from base classes, interfaces, overridden members, and constructors. ([Issue #19175](https://github.com/dotnet/fsharp/issues/19175), [PR #19188](https://github.com/dotnet/fsharp/pull/19188)) * **Extract to let binding** and **Extract to literal** refactorings for a selected expression. The value is bound in front of the statement that uses it, so it is computed before the code that preceded the selection in that statement; a constant in a module-level declaration becomes a `[]` in front of that declaration. +* **Extract to local function**, **Extract to module function** and **Extract to private member** refactorings for a selected expression. Values the selection reads from the enclosing function, member or lambda become parameters in the order they are first used, typed as the new **Parameter types in Extract to function** option under **Code Fixes** asks. A selection that assigns to a captured mutable local, or reads a byref or byref-like value from outside, is not offered. ### Fixed diff --git a/vsintegration/src/FSharp.Editor/FSharp.Editor.fsproj b/vsintegration/src/FSharp.Editor/FSharp.Editor.fsproj index 4759355028a..ec7ad4d768c 100644 --- a/vsintegration/src/FSharp.Editor/FSharp.Editor.fsproj +++ b/vsintegration/src/FSharp.Editor/FSharp.Editor.fsproj @@ -107,6 +107,7 @@ + diff --git a/vsintegration/src/FSharp.Editor/FSharp.Editor.resx b/vsintegration/src/FSharp.Editor/FSharp.Editor.resx index b645ff73aa8..bc67591a010 100644 --- a/vsintegration/src/FSharp.Editor/FSharp.Editor.resx +++ b/vsintegration/src/FSharp.Editor/FSharp.Editor.resx @@ -374,4 +374,13 @@ Use live (unsaved) buffers for analysis Extract to literal + + Extract to local function + + + Extract to module function + + + Extract to private member + \ No newline at end of file diff --git a/vsintegration/src/FSharp.Editor/Options/EditorOptions.fs b/vsintegration/src/FSharp.Editor/Options/EditorOptions.fs index 4b0f885e892..68b507b7170 100644 --- a/vsintegration/src/FSharp.Editor/Options/EditorOptions.fs +++ b/vsintegration/src/FSharp.Editor/Options/EditorOptions.fs @@ -16,6 +16,12 @@ type EnterKeySetting = | NewlineOnCompleteWord | AlwaysNewline +[] +type ParameterAnnotationSetting = + | Always + | WhenNeeded + | Never + // CLIMutable to make the record work also as a view model [] type IntelliSenseOptions = @@ -68,6 +74,7 @@ type CodeFixesOptions = UnusedDeclarations: bool SuggestNamesForErrors: bool RemoveParens: bool + ExtractFunctionParameterAnnotations: ParameterAnnotationSetting } static member Default = @@ -79,6 +86,7 @@ type CodeFixesOptions = UnusedDeclarations = true SuggestNamesForErrors = true RemoveParens = false + ExtractFunctionParameterAnnotations = ParameterAnnotationSetting.Always } [] @@ -213,7 +221,14 @@ module internal OptionsUI = [] type internal CodeFixesOptionPage() = inherit AbstractOptionPage() - override this.CreateView() = upcast CodeFixesOptionControl() + + override this.CreateView() = + let view = CodeFixesOptionControl() + let path = nameof CodeFixesOptions.Default.ExtractFunctionParameterAnnotations + bindRadioButton view.annotateAlways path ParameterAnnotationSetting.Always + bindRadioButton view.annotateWhenNeeded path ParameterAnnotationSetting.WhenNeeded + bindRadioButton view.annotateNever path ParameterAnnotationSetting.Never + upcast view [] type internal LanguageServicePerformanceOptionPage() = @@ -263,6 +278,9 @@ module EditorOptionsExtensions = member this.IsFsharpRemoveParensEnabled = this.EditorOptions.CodeFixes.RemoveParens + member this.FSharpExtractFunctionParameterAnnotations = + this.EditorOptions.CodeFixes.ExtractFunctionParameterAnnotations + member this.IsFSharpCodeFixesSuggestNamesForErrorsEnabled = this.EditorOptions.CodeFixes.SuggestNamesForErrors diff --git a/vsintegration/src/FSharp.Editor/Refactor/ExtractFunction.fs b/vsintegration/src/FSharp.Editor/Refactor/ExtractFunction.fs new file mode 100644 index 00000000000..e2c3ba3858b --- /dev/null +++ b/vsintegration/src/FSharp.Editor/Refactor/ExtractFunction.fs @@ -0,0 +1,352 @@ +// Copyright (c) Microsoft Corporation. All Rights Reserved. See License.txt in the project root for license information. + +namespace Microsoft.VisualStudio.FSharp.Editor + +open System +open System.Collections.Generic +open System.Composition +open System.Threading + +open Microsoft.CodeAnalysis.CodeActions +open Microsoft.CodeAnalysis.CodeRefactorings +open Microsoft.CodeAnalysis.Formatting +open Microsoft.CodeAnalysis.Text +open Microsoft.VisualStudio.FSharp.Editor.Telemetry + +open FSharp.Compiler.CodeAnalysis +open FSharp.Compiler.Symbols +open FSharp.Compiler.Syntax +open FSharp.Compiler.Text + +open RefactoringHelpers +open CancellableTasks + +[] +module private FunctionExtraction = + + [] + type Parameter = + { + Name: string + Type: string + Uses: range list + } + + let private padding (width: int) = String(' ', width) + + /// The declaration whose locals the extracted code may capture: the enclosing member or module-level declaration. + let enclosingScope (path: SyntaxVisitorPath) = + path + |> List.tryPick (function + | SyntaxNode.SynMemberDefn(SynMemberDefn.Member(range = m) | SynMemberDefn.LetBindings(range = m)) + | SyntaxNode.SynModule(SynModuleDecl.Let(range = m) | SynModuleDecl.Expr(range = m)) -> Some m + | _ -> None) + + let private isByRefLike (fullType: FSharpType) = + fullType.HasTypeDefinition + && (fullType.TypeDefinition.IsByRef + || fullType.TypeDefinition.Attributes + |> Seq.exists (fun attribute -> + String.Equals(attribute.AttributeType.CompiledName, "IsByRefLikeAttribute", StringComparison.Ordinal))) + + /// The values the selection reads from its scope, in order of first use, and whether it uses this or base; + /// ValueNone when a captured value has no type or a byref-like one. + let tryCaptures (checkResults: FSharpCheckFileResults) (selection: range) (scope: range) (cancellationToken: CancellationToken) = + let parameters = Dictionary(Range.comparer) + let mutable usesThis = false + let mutable usesBase = false + let mutable capturable = true + + for symbolUse in checkResults.GetAllUsesOfAllSymbolsInFile cancellationToken do + if + not symbolUse.IsFromDefinition + && Range.rangeContainsRange selection symbolUse.Range + then + match symbolUse.Symbol with + | :? FSharpMemberOrFunctionOrValue as value when value.IsMemberThisValue || value.IsConstructorThisValue -> usesThis <- true + | :? FSharpMemberOrFunctionOrValue as value when value.IsBaseValue -> usesBase <- true + | :? FSharpMemberOrFunctionOrValue as value when not value.IsModuleValueOrMember -> + match symbolUse.Symbol.DeclarationLocation with + | Some declaration when + Range.rangeContainsRange scope declaration + && not (Range.rangeContainsRange selection declaration) + -> + match parameters.TryGetValue declaration, value.FullTypeSafe with + | (true, parameter), _ -> + parameters[declaration] <- + { parameter with + Uses = symbolUse.Range :: parameter.Uses + } + | (false, _), Some fullType when not (isByRefLike fullType) -> + parameters[declaration] <- + { + Name = value.DisplayName + Type = fullType.FormatWithConstraints symbolUse.DisplayContext + Uses = [ symbolUse.Range ] + } + | (false, _), _ -> capturable <- false + | _ -> () + | _ -> () + + if capturable then + let firstUse (parameter: Parameter) = + parameter.Uses + |> List.map (fun m -> struct (m.StartLine, m.StartColumn)) + |> List.min + + ValueSome(struct (parameters.Values |> Seq.sortBy firstUse |> List.ofSeq, usesThis, usesBase)) + else + ValueNone + + /// Whether the selection assigns to one of the captured values, which a parameter could not carry back. + let assignsParameter (expr: SynExpr) (parameters: Parameter list) = + let assigned = + (HashSet(), [ SyntaxNode.SynExpr expr ]) + ||> SyntaxNodes.fold (fun positions _ node -> + match node with + | SyntaxNode.SynExpr(SynExpr.LongIdentSet(longDotId = SynLongIdent(id = [ ident ]))) + | SyntaxNode.SynExpr(SynExpr.Set(targetExpr = SynExpr.Ident ident)) -> positions.Add ident.idRange.Start |> ignore + | _ -> () + + positions) + + parameters + |> List.exists (fun parameter -> parameter.Uses |> List.exists (fun m -> assigned.Contains m.Start)) + + /// Positions of identifiers whose type inference cannot tell from their use alone: receivers of a member or indexer + /// access and operands of an operator. + let positionsNeedingType (expr: SynExpr) = + (HashSet(), [ SyntaxNode.SynExpr expr ]) + ||> SyntaxNodes.fold (fun positions _ node -> + match node with + | SyntaxNode.SynExpr(SynExpr.LongIdent(longDotId = SynLongIdent(id = head :: _ :: _))) + | SyntaxNode.SynExpr(SynExpr.DotGet(expr = SynExpr.Ident head)) + | SyntaxNode.SynExpr(SynExpr.DotIndexedGet(objectExpr = SynExpr.Ident head)) + | SyntaxNode.SynExpr(SynExpr.App( + flag = ExprAtomicFlag.Atomic; funcExpr = SynExpr.Ident head; argExpr = SynExpr.ArrayOrListComputed _)) + | SyntaxNode.SynExpr(SynExpr.App(isInfix = false; funcExpr = SynExpr.App(isInfix = true; argExpr = SynExpr.Ident head))) + | SyntaxNode.SynExpr(SynExpr.App(isInfix = false; funcExpr = SynExpr.App(isInfix = true); argExpr = SynExpr.Ident head)) -> + positions.Add head.idRange.Start |> ignore + | _ -> () + + positions) + + let private declared (annotate: Parameter -> bool) (parameter: Parameter) = + if annotate parameter then + $"{parameter.Name}: {parameter.Type}" + else + parameter.Name + + let curriedParameters (parameters: Parameter list) (annotate: Parameter -> bool) = + match parameters with + | [] -> "()" + | _ -> + parameters + |> List.map (fun parameter -> + if annotate parameter then + $"({declared annotate parameter})" + else + parameter.Name) + |> String.concat " " + + let curriedArguments (parameters: Parameter list) = + match parameters with + | [] -> "()" + | _ -> parameters |> List.map _.Name |> String.concat " " + + let tupledParameters (parameters: Parameter list) (annotate: Parameter -> bool) = + let declarations = parameters |> List.map (declared annotate) |> String.concat ", " + $"({declarations})" + + let tupledArguments (parameters: Parameter list) = + let arguments = parameters |> List.map _.Name |> String.concat ", " + $"({arguments})" + + /// Whether a function application put where the selection was needs parentheses to stay one argument. + let needsParentheses (target: ExtractionTarget) = + match target.Expr, target.Path with + | SynExpr.Paren _, _ when target.Replaced = target.Content -> false + | _, SyntaxNode.SynExpr(SynExpr.App _ | SynExpr.DotGet _ | SynExpr.DotIndexedGet _ | SynExpr.TypeApp _) :: _ -> true + | _ -> false + + let isInRecursiveModuleLet (path: SyntaxVisitorPath) = + path + |> List.exists (function + | SyntaxNode.SynModule(SynModuleDecl.Let(isRecursive = true)) -> true + | _ -> false) + + /// The member of a class, record or union that contains the selection, outside interface implementations and + /// object expressions. + let tryEnclosingMember (path: SyntaxVisitorPath) = + let rec loop (path: SyntaxVisitorPath) = + match path with + | SyntaxNode.SynMemberDefn(SynMemberDefn.Member(memberDefn = binding; range = memberRange)) :: SyntaxNode.SynTypeDefn(SynTypeDefn( + typeInfo = info)) :: _ -> ValueSome(struct (binding, memberRange, info)) + | (SyntaxNode.SynMemberDefn(SynMemberDefn.Interface _) | SyntaxNode.SynExpr(SynExpr.ObjExpr _)) :: _ + | [] -> ValueNone + | _ :: rest -> loop rest + + loop path + + /// The start of the new member's declaration and the receiver it is called on: the self identifier of an instance + /// member, or the name of a non-generic type for a static member. + let tryMemberCallee (binding: SynBinding) (info: SynComponentInfo) = + match binding, info with + | SynBinding(headPat = SynPat.LongIdent(longDotId = SynLongIdent(id = [ self; _ ]))), _ when + not (self.idText.StartsWith("_", StringComparison.Ordinal)) + -> + ValueSome(struct ($"member private {self.idText}.", self.idText)) + | SynBinding(headPat = SynPat.LongIdent(longDotId = SynLongIdent(id = [ _ ]))), SynComponentInfo(typeParams = None) -> + ValueSome(struct ("static member private ", (List.last info.LongIdent).idText)) + | _ -> ValueNone + + /// Changes that declare `header = ` right after the member and call it where the selection was. + let tryMemberChanges + (sourceText: SourceText) + (target: ExtractionTarget) + (memberRange: range) + (header: string) + (call: string) + (indentSize: int) + (literalLines: HashSet) + = + let lines = sourceText.Lines + let firstLine = lines[Line.toZ memberRange.StartLine] + let lastLine = lines[Line.toZ memberRange.EndLine] + let indent = leadingSpaces sourceText firstLine + let lineBreak = lineBreakOf sourceText + + tryDeclaration sourceText target.Content header (indent + indentSize) literalLines lineBreak + |> ValueOption.map (fun declaration -> + let insertion = + if lastLine.EndIncludingLineBreak > lastLine.End then + TextChange(TextSpan(lastLine.EndIncludingLineBreak, 0), $"{padding indent}{declaration}{lineBreak}") + else + TextChange(TextSpan(lastLine.End, 0), $"{lineBreak}{padding indent}{declaration}") + + [ TextChange(target.Replaced, call); insertion ]) + +[] +type internal FSharpExtractFunctionRefactoring [] () = + inherit CodeRefactoringProvider() + + static let register + (context: CodeRefactoringContext) + (sourceText: SourceText) + (title: string) + (kind: string) + (changes: TextChange list) + = + let changedDocument = + cancellableTask { + TelemetryReporter.ReportSingleEvent( + TelemetryEvents.RefactoringActivated, + [| "name", box (nameof FSharpExtractFunctionRefactoring); "kind", box kind |] + ) + + return context.Document.WithText(sourceText.WithChanges changes) + } + + context.RegisterRefactoring(CodeAction.Create(title, changedDocument, title)) + + override _.ComputeRefactoringsAsync context = + cancellableTask { + let document = context.Document + + if not (context.Span.IsEmpty || document.IsFSharpSignatureFile) then + let! cancellationToken = CancellableTask.getCancellationToken () + let! sourceText = document.GetTextAsync cancellationToken + let! parseResults = document.GetFSharpParseResultsAsync(nameof FSharpExtractFunctionRefactoring) + + match tryExtractionTarget sourceText parseResults.ParseTree context.Span with + | ValueNone -> () + | ValueSome target -> + match FunctionExtraction.enclosingScope target.Path with + | None -> () + | Some scope -> + let! _, checkResults = document.GetFSharpParseAndCheckResultsAsync(nameof FSharpExtractFunctionRefactoring) + + let selection = + Range.mkRange + document.FilePath + (positionOf sourceText target.Content.Start) + (positionOf sourceText target.Content.End) + + match FunctionExtraction.tryCaptures checkResults selection scope cancellationToken with + | ValueSome(struct (parameters, usesThis, usesBase)) when + not (FunctionExtraction.assignsParameter target.Expr parameters) + -> + let! options = document.GetOptionsAsync cancellationToken + + let indentSize = + options.GetOption(FormattingOptions.IndentationSize, FSharpConstants.FSharpLanguageName) + + let literalLines = linesInsideLiterals parseResults.ParseTree + let names = usedNames parseResults.ParseTree + let needingType = FunctionExtraction.positionsNeedingType target.Expr + let setting = document.Project.FSharpExtractFunctionParameterAnnotations + + let annotate (parameter: FunctionExtraction.Parameter) = + match setting with + | ParameterAnnotationSetting.Always -> true + | ParameterAnnotationSetting.WhenNeeded -> + parameter.Uses |> List.exists (fun m -> needingType.Contains m.Start) + | ParameterAnnotationSetting.Never -> false + + let functionName = uniqueName "extractedFunction" names + + let header = + $"{functionName} {FunctionExtraction.curriedParameters parameters annotate}" + + let application = $"{functionName} {FunctionExtraction.curriedArguments parameters}" + + let call = + if FunctionExtraction.needsParentheses target then + $"({application})" + else + application + + if not usesBase then + match anchorsOf target.Expr target.Path with + | anchor :: _ -> + match tryDeclareInFront sourceText target anchor $"let {header}" call indentSize literalLines with + | ValueSome changes -> register context sourceText (SR.ExtractToLocalFunction()) "local" changes + | ValueNone -> () + | [] -> () + + if not (usesThis || usesBase || FunctionExtraction.isInRecursiveModuleLet target.Path) then + match + tryDeclareInFrontOfModuleLet sourceText target [] $"let private {header}" call indentSize literalLines + with + | ValueSome changes -> register context sourceText (SR.ExtractToModuleFunction()) "module" changes + | ValueNone -> () + + match FunctionExtraction.tryEnclosingMember target.Path with + | ValueSome(struct (binding, memberRange, info)) -> + match FunctionExtraction.tryMemberCallee binding info with + | ValueSome(struct (prefix, receiver)) -> + let memberName = uniqueName "ExtractedMethod" names + + let memberHeader = + $"{prefix}{memberName}{FunctionExtraction.tupledParameters parameters annotate}" + + let memberCall = + $"{receiver}.{memberName}{FunctionExtraction.tupledArguments parameters}" + + match + FunctionExtraction.tryMemberChanges + sourceText + target + memberRange + memberHeader + memberCall + indentSize + literalLines + with + | ValueSome changes -> register context sourceText (SR.ExtractToPrivateMember()) "member" changes + | ValueNone -> () + | ValueNone -> () + | ValueNone -> () + | _ -> () + } + |> CancellableTask.startAsTask context.CancellationToken diff --git a/vsintegration/src/FSharp.Editor/Refactor/RefactoringHelpers.fs b/vsintegration/src/FSharp.Editor/Refactor/RefactoringHelpers.fs index f69c579a364..1b4cd9681df 100644 --- a/vsintegration/src/FSharp.Editor/Refactor/RefactoringHelpers.fs +++ b/vsintegration/src/FSharp.Editor/Refactor/RefactoringHelpers.fs @@ -381,7 +381,7 @@ let tryEnclosingModuleLet (path: SyntaxVisitorPath) = let private padding (width: int) = String(' ', width) /// `header = `, with a multi-line content starting on its own line at bodyColumn. -let private tryDeclaration (sourceText: SourceText) (content: TextSpan) (header: string) (bodyColumn: int) literalLines lineBreak = +let tryDeclaration (sourceText: SourceText) (content: TextSpan) (header: string) (bodyColumn: int) literalLines lineBreak = tryIndentedText sourceText content bodyColumn literalLines |> ValueOption.map (fun body -> let lines = sourceText.Lines diff --git a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.cs.xlf b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.cs.xlf index 9b94eafd87e..01279701304 100644 --- a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.cs.xlf +++ b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.cs.xlf @@ -115,6 +115,21 @@ Navrhnout názvy pro nerozpoznané identifikátory; Extract to literal + + Extract to local function + Extract to local function + + + + Extract to module function + Extract to module function + + + + Extract to private member + Extract to private member + + F# Disposable Values (locals) Uvolnitelné hodnoty jazyka F# (místní) diff --git a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.de.xlf b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.de.xlf index 8ce1754b360..fa60ff5494c 100644 --- a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.de.xlf +++ b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.de.xlf @@ -115,6 +115,21 @@ Namen für nicht aufgelöste Bezeichner vorschlagen; Extract to literal + + Extract to local function + Extract to local function + + + + Extract to module function + Extract to module function + + + + Extract to private member + Extract to private member + + F# Disposable Values (locals) Disposable-Werte in F# (lokal) diff --git a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.es.xlf b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.es.xlf index 501be2d5e7c..ddfd987090a 100644 --- a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.es.xlf +++ b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.es.xlf @@ -115,6 +115,21 @@ Sugerir nombres para identificadores sin resolver; Extract to literal + + Extract to local function + Extract to local function + + + + Extract to module function + Extract to module function + + + + Extract to private member + Extract to private member + + F# Disposable Values (locals) Valores de F# descartables (locales) diff --git a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.fr.xlf b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.fr.xlf index 8b8d9a57638..c43f29dc65d 100644 --- a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.fr.xlf +++ b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.fr.xlf @@ -115,6 +115,21 @@ Suggérer des noms pour les identificateurs non résolus ; Extract to literal + + Extract to local function + Extract to local function + + + + Extract to module function + Extract to module function + + + + Extract to private member + Extract to private member + + F# Disposable Values (locals) Valeurs F# pouvant être supprimées (variables locales) diff --git a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.it.xlf b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.it.xlf index 35e4cca8ad4..04f030c3df1 100644 --- a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.it.xlf +++ b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.it.xlf @@ -115,6 +115,21 @@ Suggerisci i nomi per gli identificatori non risolti; Extract to literal + + Extract to local function + Extract to local function + + + + Extract to module function + Extract to module function + + + + Extract to private member + Extract to private member + + F# Disposable Values (locals) Valori eliminabili F# (variabili locali) diff --git a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.ja.xlf b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.ja.xlf index 928004aa616..8213f74ef52 100644 --- a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.ja.xlf +++ b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.ja.xlf @@ -115,6 +115,21 @@ Suggest names for unresolved identifiers; Extract to literal + + Extract to local function + Extract to local function + + + + Extract to module function + Extract to module function + + + + Extract to private member + Extract to private member + + F# Disposable Values (locals) F# の破棄可能な値 (ローカル) diff --git a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.ko.xlf b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.ko.xlf index c104880c6d9..2bfd70e9ef2 100644 --- a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.ko.xlf +++ b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.ko.xlf @@ -115,6 +115,21 @@ Suggest names for unresolved identifiers; Extract to literal + + Extract to local function + Extract to local function + + + + Extract to module function + Extract to module function + + + + Extract to private member + Extract to private member + + F# Disposable Values (locals) F# 삭제 가능한 값(로컬) diff --git a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.pl.xlf b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.pl.xlf index 4a27d371123..684851fbed4 100644 --- a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.pl.xlf +++ b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.pl.xlf @@ -115,6 +115,21 @@ Sugeruj nazwy dla nierozpoznanych identyfikatorów; Extract to literal + + Extract to local function + Extract to local function + + + + Extract to module function + Extract to module function + + + + Extract to private member + Extract to private member + + F# Disposable Values (locals) Wartości możliwe do likwidacji języka F# (lokalne) diff --git a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.pt-BR.xlf b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.pt-BR.xlf index ee0729dde99..17354c9587b 100644 --- a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.pt-BR.xlf +++ b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.pt-BR.xlf @@ -115,6 +115,21 @@ Sugerir nomes para identificadores não resolvidos; Extract to literal + + Extract to local function + Extract to local function + + + + Extract to module function + Extract to module function + + + + Extract to private member + Extract to private member + + F# Disposable Values (locals) Valores F# Descartáveis (locais) diff --git a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.ru.xlf b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.ru.xlf index 69476255afe..da38a0f2a3a 100644 --- a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.ru.xlf +++ b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.ru.xlf @@ -115,6 +115,21 @@ Suggest names for unresolved identifiers; Extract to literal + + Extract to local function + Extract to local function + + + + Extract to module function + Extract to module function + + + + Extract to private member + Extract to private member + + F# Disposable Values (locals) Освобождаемые значения F# (локальные) diff --git a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.tr.xlf b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.tr.xlf index 802990076ba..19e2697a3cb 100644 --- a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.tr.xlf +++ b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.tr.xlf @@ -115,6 +115,21 @@ Kullanılmayan değerleri analiz et ve bunlara düzeltmeler öner; Extract to literal + + Extract to local function + Extract to local function + + + + Extract to module function + Extract to module function + + + + Extract to private member + Extract to private member + + F# Disposable Values (locals) F# Atılabilir Değerleri (yereller) diff --git a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.zh-Hans.xlf b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.zh-Hans.xlf index ace8f6ec72e..05b7ab2414d 100644 --- a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.zh-Hans.xlf +++ b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.zh-Hans.xlf @@ -115,6 +115,21 @@ Suggest names for unresolved identifiers; Extract to literal + + Extract to local function + Extract to local function + + + + Extract to module function + Extract to module function + + + + Extract to private member + Extract to private member + + F# Disposable Values (locals) F# 可释放值(局部值) diff --git a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.zh-Hant.xlf b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.zh-Hant.xlf index e2191749e38..f2280d8cfd8 100644 --- a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.zh-Hant.xlf +++ b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.zh-Hant.xlf @@ -115,6 +115,21 @@ Suggest names for unresolved identifiers; Extract to literal + + Extract to local function + Extract to local function + + + + Extract to module function + Extract to module function + + + + Extract to private member + Extract to private member + + F# Disposable Values (locals) F# 可處置的值 (區域) diff --git a/vsintegration/src/FSharp.UIResources/CodeFixesOptionControl.xaml b/vsintegration/src/FSharp.UIResources/CodeFixesOptionControl.xaml index 211e17fd3ce..c69d7074e7b 100644 --- a/vsintegration/src/FSharp.UIResources/CodeFixesOptionControl.xaml +++ b/vsintegration/src/FSharp.UIResources/CodeFixesOptionControl.xaml @@ -35,6 +35,13 @@ Content="{x:Static local:Strings.Remove_parens_code_fix}"/> + + + + + + + diff --git a/vsintegration/src/FSharp.UIResources/Strings.Designer.cs b/vsintegration/src/FSharp.UIResources/Strings.Designer.cs index e47f10e1045..d0621fa2a95 100644 --- a/vsintegration/src/FSharp.UIResources/Strings.Designer.cs +++ b/vsintegration/src/FSharp.UIResources/Strings.Designer.cs @@ -87,6 +87,33 @@ public static string Analyze_full_solution_on_background { } } + /// + /// 查找类似 Annotate every parameter 的本地化字符串。 + /// + public static string Annotate_parameters_always { + get { + return ResourceManager.GetString("Annotate_parameters_always", resourceCulture); + } + } + + /// + /// 查找类似 Leave parameter types to type inference 的本地化字符串。 + /// + public static string Annotate_parameters_never { + get { + return ResourceManager.GetString("Annotate_parameters_never", resourceCulture); + } + } + + /// + /// 查找类似 Annotate parameters whose type inference cannot determine from the extracted code 的本地化字符串。 + /// + public static string Annotate_parameters_when_needed { + get { + return ResourceManager.GetString("Annotate_parameters_when_needed", resourceCulture); + } + } + /// /// 查找类似 Background analysis 的本地化字符串。 /// @@ -249,6 +276,15 @@ public static string Enter_Key_Rule { } } + /// + /// 查找类似 Parameter types in Extract to function 的本地化字符串。 + /// + public static string Extract_function_parameter_annotations { + get { + return ResourceManager.GetString("Extract_function_parameter_annotations", resourceCulture); + } + } + /// /// 查找类似 Find References Performance Options 的本地化字符串。 /// diff --git a/vsintegration/src/FSharp.UIResources/Strings.resx b/vsintegration/src/FSharp.UIResources/Strings.resx index 12e5b87aae8..4040b112bdf 100644 --- a/vsintegration/src/FSharp.UIResources/Strings.resx +++ b/vsintegration/src/FSharp.UIResources/Strings.resx @@ -315,4 +315,16 @@ Generate default implementation body for overridden method + + Parameter types in Extract to function + + + Annotate every parameter + + + Annotate parameters whose type inference cannot determine from the extracted code + + + Leave parameter types to type inference + \ No newline at end of file diff --git a/vsintegration/src/FSharp.UIResources/xlf/Strings.cs.xlf b/vsintegration/src/FSharp.UIResources/xlf/Strings.cs.xlf index e55f176a39b..f106b7f80ae 100644 --- a/vsintegration/src/FSharp.UIResources/xlf/Strings.cs.xlf +++ b/vsintegration/src/FSharp.UIResources/xlf/Strings.cs.xlf @@ -17,6 +17,21 @@ Nadále analyzovat celé řešení pro diagnostiku jako úlohu na pozadí s nízkou prioritou (vyžaduje restartování) + + Annotate every parameter + Annotate every parameter + + + + Leave parameter types to type inference + Leave parameter types to type inference + + + + Annotate parameters whose type inference cannot determine from the extracted code + Annotate parameters whose type inference cannot determine from the extracted code + + Background analysis Analýza na pozadí @@ -42,6 +57,11 @@ Povolit částečnou kontrolu typu + + Parameter types in Extract to function + Parameter types in Extract to function + + Find References Performance Options Najít možnosti výkonu odkazů diff --git a/vsintegration/src/FSharp.UIResources/xlf/Strings.de.xlf b/vsintegration/src/FSharp.UIResources/xlf/Strings.de.xlf index b07e4473e52..4b1c70ae34d 100644 --- a/vsintegration/src/FSharp.UIResources/xlf/Strings.de.xlf +++ b/vsintegration/src/FSharp.UIResources/xlf/Strings.de.xlf @@ -17,6 +17,21 @@ Analyse der gesamten Projektmappe zur Diagnose als Hintergrundaufgabe mit niedriger Priorität fortsetzen (Neustart erforderlich) + + Annotate every parameter + Annotate every parameter + + + + Leave parameter types to type inference + Leave parameter types to type inference + + + + Annotate parameters whose type inference cannot determine from the extracted code + Annotate parameters whose type inference cannot determine from the extracted code + + Background analysis Hintergrundanalyse @@ -42,6 +57,11 @@ Aktivieren der partiellen Typüberprüfung + + Parameter types in Extract to function + Parameter types in Extract to function + + Find References Performance Options Leistungsoptionen für Verweise suchen diff --git a/vsintegration/src/FSharp.UIResources/xlf/Strings.es.xlf b/vsintegration/src/FSharp.UIResources/xlf/Strings.es.xlf index aa652aa8723..3af5c185d32 100644 --- a/vsintegration/src/FSharp.UIResources/xlf/Strings.es.xlf +++ b/vsintegration/src/FSharp.UIResources/xlf/Strings.es.xlf @@ -17,6 +17,21 @@ Siga analizando toda la solución para los diagnósticos como una tarea en segundo plano de prioridad baja (requiere un reinicio) + + Annotate every parameter + Annotate every parameter + + + + Leave parameter types to type inference + Leave parameter types to type inference + + + + Annotate parameters whose type inference cannot determine from the extracted code + Annotate parameters whose type inference cannot determine from the extracted code + + Background analysis Análisis en segundo plano @@ -42,6 +57,11 @@ Habilitar la comprobación parcial de tipos + + Parameter types in Extract to function + Parameter types in Extract to function + + Find References Performance Options Buscar opciones de rendimiento de referencias diff --git a/vsintegration/src/FSharp.UIResources/xlf/Strings.fr.xlf b/vsintegration/src/FSharp.UIResources/xlf/Strings.fr.xlf index db51d37fb99..0203ff89e6f 100644 --- a/vsintegration/src/FSharp.UIResources/xlf/Strings.fr.xlf +++ b/vsintegration/src/FSharp.UIResources/xlf/Strings.fr.xlf @@ -17,6 +17,21 @@ Continuez à analyser l'intégralité de la solution pour les diagnostics en tant que tâche en arrière-plan de basse priorité (nécessite un redémarrage) + + Annotate every parameter + Annotate every parameter + + + + Leave parameter types to type inference + Leave parameter types to type inference + + + + Annotate parameters whose type inference cannot determine from the extracted code + Annotate parameters whose type inference cannot determine from the extracted code + + Background analysis Analyse de fond @@ -42,6 +57,11 @@ Activer la vérification de type partielle + + Parameter types in Extract to function + Parameter types in Extract to function + + Find References Performance Options Options de performances de recherche de références diff --git a/vsintegration/src/FSharp.UIResources/xlf/Strings.it.xlf b/vsintegration/src/FSharp.UIResources/xlf/Strings.it.xlf index d220a899907..dcb8eddf184 100644 --- a/vsintegration/src/FSharp.UIResources/xlf/Strings.it.xlf +++ b/vsintegration/src/FSharp.UIResources/xlf/Strings.it.xlf @@ -17,6 +17,21 @@ Continua ad analizzare l'intera soluzione per la diagnostica come attività in background a priorità bassa (richiede il riavvio) + + Annotate every parameter + Annotate every parameter + + + + Leave parameter types to type inference + Leave parameter types to type inference + + + + Annotate parameters whose type inference cannot determine from the extracted code + Annotate parameters whose type inference cannot determine from the extracted code + + Background analysis Analisi in background @@ -42,6 +57,11 @@ Abilita il controllo parziale dei tipi + + Parameter types in Extract to function + Parameter types in Extract to function + + Find References Performance Options Trovare opzioni prestazioni riferimenti diff --git a/vsintegration/src/FSharp.UIResources/xlf/Strings.ja.xlf b/vsintegration/src/FSharp.UIResources/xlf/Strings.ja.xlf index 9328453eba8..0f360088933 100644 --- a/vsintegration/src/FSharp.UIResources/xlf/Strings.ja.xlf +++ b/vsintegration/src/FSharp.UIResources/xlf/Strings.ja.xlf @@ -17,6 +17,21 @@ 診断のソリューション全体を低優先度のバックグラウンド タスクとして分析し続ける (再起動が必要) + + Annotate every parameter + Annotate every parameter + + + + Leave parameter types to type inference + Leave parameter types to type inference + + + + Annotate parameters whose type inference cannot determine from the extracted code + Annotate parameters whose type inference cannot determine from the extracted code + + Background analysis バックグラウンド分析 @@ -42,6 +57,11 @@ 部分型チェックを有効にする + + Parameter types in Extract to function + Parameter types in Extract to function + + Find References Performance Options 参照の検索のパフォーマンス オプション diff --git a/vsintegration/src/FSharp.UIResources/xlf/Strings.ko.xlf b/vsintegration/src/FSharp.UIResources/xlf/Strings.ko.xlf index a31cd3ee3e2..1e770d500d8 100644 --- a/vsintegration/src/FSharp.UIResources/xlf/Strings.ko.xlf +++ b/vsintegration/src/FSharp.UIResources/xlf/Strings.ko.xlf @@ -17,6 +17,21 @@ 진단에 대한 전체 솔루션을 낮은 우선 순위 백그라운드 작업으로 계속 분석(다시 시작해야 함) + + Annotate every parameter + Annotate every parameter + + + + Leave parameter types to type inference + Leave parameter types to type inference + + + + Annotate parameters whose type inference cannot determine from the extracted code + Annotate parameters whose type inference cannot determine from the extracted code + + Background analysis 백그라운드 분석 @@ -42,6 +57,11 @@ 부분 형식 검사 사용 + + Parameter types in Extract to function + Parameter types in Extract to function + + Find References Performance Options 참조 성능 옵션 찾기 diff --git a/vsintegration/src/FSharp.UIResources/xlf/Strings.pl.xlf b/vsintegration/src/FSharp.UIResources/xlf/Strings.pl.xlf index af20566c256..02641c0560a 100644 --- a/vsintegration/src/FSharp.UIResources/xlf/Strings.pl.xlf +++ b/vsintegration/src/FSharp.UIResources/xlf/Strings.pl.xlf @@ -17,6 +17,21 @@ Analizuj całe rozwiązanie pod kątem diagnostyki jako zadanie w tle o niskim priorytecie (wymaga ponownego uruchomienia) + + Annotate every parameter + Annotate every parameter + + + + Leave parameter types to type inference + Leave parameter types to type inference + + + + Annotate parameters whose type inference cannot determine from the extracted code + Annotate parameters whose type inference cannot determine from the extracted code + + Background analysis Analiza w tle @@ -42,6 +57,11 @@ Włącz kontrolę typu częściowego + + Parameter types in Extract to function + Parameter types in Extract to function + + Find References Performance Options Opcje wydajności znajdowania odwołań diff --git a/vsintegration/src/FSharp.UIResources/xlf/Strings.pt-BR.xlf b/vsintegration/src/FSharp.UIResources/xlf/Strings.pt-BR.xlf index d0bc2aebb17..735afa156ec 100644 --- a/vsintegration/src/FSharp.UIResources/xlf/Strings.pt-BR.xlf +++ b/vsintegration/src/FSharp.UIResources/xlf/Strings.pt-BR.xlf @@ -17,6 +17,21 @@ Continuar analisando toda a solução para diagnóstico como uma tarefa em segundo plano de baixa prioridade (requer reinicialização) + + Annotate every parameter + Annotate every parameter + + + + Leave parameter types to type inference + Leave parameter types to type inference + + + + Annotate parameters whose type inference cannot determine from the extracted code + Annotate parameters whose type inference cannot determine from the extracted code + + Background analysis Análise em segundo plano @@ -42,6 +57,11 @@ Habilitar verificação parcial de tipo + + Parameter types in Extract to function + Parameter types in Extract to function + + Find References Performance Options Opções de Localizar Referências de Desempenho diff --git a/vsintegration/src/FSharp.UIResources/xlf/Strings.ru.xlf b/vsintegration/src/FSharp.UIResources/xlf/Strings.ru.xlf index 170dff1df34..92519c3bc9c 100644 --- a/vsintegration/src/FSharp.UIResources/xlf/Strings.ru.xlf +++ b/vsintegration/src/FSharp.UIResources/xlf/Strings.ru.xlf @@ -17,6 +17,21 @@ Продолжать анализ всего решения для диагностики в качестве низкоприоритетной фоновой задачи (требуется перезагрузка) + + Annotate every parameter + Annotate every parameter + + + + Leave parameter types to type inference + Leave parameter types to type inference + + + + Annotate parameters whose type inference cannot determine from the extracted code + Annotate parameters whose type inference cannot determine from the extracted code + + Background analysis Фоновый анализ @@ -42,6 +57,11 @@ Включить частичную проверку типов + + Parameter types in Extract to function + Parameter types in Extract to function + + Find References Performance Options Параметры производительности поиска ссылок diff --git a/vsintegration/src/FSharp.UIResources/xlf/Strings.tr.xlf b/vsintegration/src/FSharp.UIResources/xlf/Strings.tr.xlf index 1a8bea80cc1..47b5c829181 100644 --- a/vsintegration/src/FSharp.UIResources/xlf/Strings.tr.xlf +++ b/vsintegration/src/FSharp.UIResources/xlf/Strings.tr.xlf @@ -17,6 +17,21 @@ Düşük öncelikli arka plan görevi olarak tanılama için çözümün tamamını analiz etmeye devam et (yeniden başlatma gerekir) + + Annotate every parameter + Annotate every parameter + + + + Leave parameter types to type inference + Leave parameter types to type inference + + + + Annotate parameters whose type inference cannot determine from the extracted code + Annotate parameters whose type inference cannot determine from the extracted code + + Background analysis Arka plan analizi @@ -42,6 +57,11 @@ Kısmi tür denetlemeyi etkinleştir + + Parameter types in Extract to function + Parameter types in Extract to function + + Find References Performance Options Başvuruları Bul Performans Seçenekleri diff --git a/vsintegration/src/FSharp.UIResources/xlf/Strings.zh-Hans.xlf b/vsintegration/src/FSharp.UIResources/xlf/Strings.zh-Hans.xlf index 7e573f64315..5ac83524b10 100644 --- a/vsintegration/src/FSharp.UIResources/xlf/Strings.zh-Hans.xlf +++ b/vsintegration/src/FSharp.UIResources/xlf/Strings.zh-Hans.xlf @@ -17,6 +17,21 @@ 继续分析整个解决方案以将诊断作为低优先级后台任务(需要重启) + + Annotate every parameter + Annotate every parameter + + + + Leave parameter types to type inference + Leave parameter types to type inference + + + + Annotate parameters whose type inference cannot determine from the extracted code + Annotate parameters whose type inference cannot determine from the extracted code + + Background analysis 后台分析 @@ -42,6 +57,11 @@ 启用分部类型检查 + + Parameter types in Extract to function + Parameter types in Extract to function + + Find References Performance Options 查找引用性能选项 diff --git a/vsintegration/src/FSharp.UIResources/xlf/Strings.zh-Hant.xlf b/vsintegration/src/FSharp.UIResources/xlf/Strings.zh-Hant.xlf index 868be56e015..28a4a3af1cd 100644 --- a/vsintegration/src/FSharp.UIResources/xlf/Strings.zh-Hant.xlf +++ b/vsintegration/src/FSharp.UIResources/xlf/Strings.zh-Hant.xlf @@ -17,6 +17,21 @@ 將分析整個解決方案以進行診斷保持為低優先順序背景工作 (需要重新開機) + + Annotate every parameter + Annotate every parameter + + + + Leave parameter types to type inference + Leave parameter types to type inference + + + + Annotate parameters whose type inference cannot determine from the extracted code + Annotate parameters whose type inference cannot determine from the extracted code + + Background analysis 背景分析 @@ -42,6 +57,11 @@ 啟用部分型別檢查 + + Parameter types in Extract to function + Parameter types in Extract to function + + Find References Performance Options 尋找參考效能選項 diff --git a/vsintegration/tests/FSharp.Editor.Tests/FSharp.Editor.Tests.fsproj b/vsintegration/tests/FSharp.Editor.Tests/FSharp.Editor.Tests.fsproj index 67a671126da..763b7981941 100644 --- a/vsintegration/tests/FSharp.Editor.Tests/FSharp.Editor.Tests.fsproj +++ b/vsintegration/tests/FSharp.Editor.Tests/FSharp.Editor.Tests.fsproj @@ -75,6 +75,7 @@ + diff --git a/vsintegration/tests/FSharp.Editor.Tests/Refactors/ExtractFunctionTests.fs b/vsintegration/tests/FSharp.Editor.Tests/Refactors/ExtractFunctionTests.fs new file mode 100644 index 00000000000..b53b0753241 --- /dev/null +++ b/vsintegration/tests/FSharp.Editor.Tests/Refactors/ExtractFunctionTests.fs @@ -0,0 +1,127 @@ +module FSharp.Editor.Tests.Refactors.ExtractFunctionTests + +open System + +open Microsoft.CodeAnalysis.Text + +open Microsoft.VisualStudio.FSharp.Editor +open Microsoft.VisualStudio.FSharp.Editor.CancellableTasks + +open Xunit + +open FSharp.Editor.Tests.Helpers +open FSharp.Editor.Tests.Refactors.RefactorTestFramework + +let private extractToLocalFunction = "Extract to local function" +let private extractToModuleFunction = "Extract to module function" +let private extractToPrivateMember = "Extract to private member" + +let private selectionOf (code: string) (selected: string) = + TextSpan(code.IndexOf(selected, StringComparison.Ordinal), selected.Length) + +let private contextFor (setting: ParameterAnnotationSetting) (code: string) = + let options = + { CodeFixesOptions.Default with + ExtractFunctionParameterAnnotations = setting + } + + new TestContext(RoslynTestHelpers.CreateSolution(code, editorOptions = options)) + +let private extractedWith (setting: ParameterAnnotationSetting) (title: string) (code: string) (selected: string) = + use context = contextFor setting code + + let document = + refactorSpan code (selectionOf code selected) title context (new FSharpExtractFunctionRefactoring()) + + let parseResults = + document.GetFSharpParseResultsAsync "test" + |> CancellableTask.runSynchronouslyWithoutCancellation + + Assert.Empty(parseResults.Diagnostics) + (document.GetTextAsync() |> GetTaskResult).ToString() + +let private extracted = extractedWith ParameterAnnotationSetting.Always + +let private titlesFor (code: string) (selected: string) = + use context = contextFor ParameterAnnotationSetting.Always code + + tryGetRefactoringActionsForSpan code (selectionOf code selected) context (new FSharpExtractFunctionRefactoring()) + |> Seq.map _.Title + |> List.ofSeq + +let private area = + "module M\n\nlet area (w: int) (h: int) =\n printfn \"%d\" (w * h + 1)\n" + +[] +let ``Captured parameters become parameters of a local function`` () = + let expected = + "module M\n\nlet area (w: int) (h: int) =\n let extractedFunction (w: int) (h: int) = w * h + 1\n printfn \"%d\" (extractedFunction w h)\n" + + Assert.Equal(expected, extracted extractToLocalFunction area "w * h + 1") + +[] +let ``Module function is declared in front of the declaration using it`` () = + let expected = + "module M\n\nlet private extractedFunction (w: int) (h: int) = w * h + 1\n\nlet area (w: int) (h: int) =\n printfn \"%d\" (extractedFunction w h)\n" + + Assert.Equal(expected, extracted extractToModuleFunction area "w * h + 1") + +[] +let ``Call keeps its parentheses where the selection lost them`` () = + let expected = + "module M\n\nlet area (w: int) (h: int) =\n let extractedFunction (w: int) (h: int) = w * h + 1\n printfn \"%d\" (extractedFunction w h)\n" + + Assert.Equal(expected, extracted extractToLocalFunction area "(w * h + 1)") + +[] +let ``Selection without captures becomes a function of unit`` () = + let code = "module M\n\nlet f () =\n printfn \"%d\" (1 + 2)\n" + + let expected = + "module M\n\nlet f () =\n let extractedFunction () = 1 + 2\n printfn \"%d\" (extractedFunction ())\n" + + Assert.Equal(expected, extracted extractToLocalFunction code "1 + 2") + +[] +let ``Selection using this becomes a private member`` () = + let code = + "module M\n\ntype Order(lines: int list) =\n member this.Rate = 3\n member this.Total = lines |> List.sumBy (fun l -> l * this.Rate)\n" + + let expected = + "module M\n\ntype Order(lines: int list) =\n member this.Rate = 3\n member this.Total = lines |> List.sumBy (fun l -> this.ExtractedMethod(l))\n member private this.ExtractedMethod(l: int) = l * this.Rate\n" + + Assert.Equal(expected, extracted extractToPrivateMember code "l * this.Rate") + +[] +[ ParameterAnnotationSetting.Always + | "WhenNeeded" -> ParameterAnnotationSetting.WhenNeeded + | _ -> ParameterAnnotationSetting.Never + + let code = + "module M\n\nlet shout (s: string) (n: int) =\n printfn \"%s\" (s.ToUpper() + string n)\n" + + let expected = + $"module M\n\nlet shout (s: string) (n: int) =\n {header} s.ToUpper() + string n\n printfn \"%%s\" (extractedFunction s n)\n" + + Assert.Equal(expected, extractedWith setting extractToLocalFunction code "s.ToUpper() + string n") + +[] +let ``Variants follow what the selection uses`` () = + Assert.Equal([ extractToLocalFunction; extractToModuleFunction ], titlesFor area "w * h + 1") + + let derived = + "module M\n\ntype Derived() =\n inherit System.Object()\n override this.ToString() = base.ToString() + \"!\"\n" + + Assert.Equal([ extractToPrivateMember ], titlesFor derived "base.ToString() + \"!\"") + +[] +[] +[) =\n x <- x + 1\n", "x + 1")>] +let ``No action`` (code: string, selected: string) = Assert.Empty(titlesFor code selected) From 9b4587017a9aeea209c1a72dd35e38ca86690fdc Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Mon, 14 Sep 2026 13:23:10 +0200 Subject: [PATCH 3/9] Offer Extract to literal on a caret inside a constant, as C# does 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) --- .../Refactor/ExtractLetBinding.fs | 25 +++++---- .../Refactor/RefactoringHelpers.fs | 30 +++++++++++ .../Refactors/ExtractLetBindingTests.fs | 54 +++++++++++++++++-- 3 files changed, 96 insertions(+), 13 deletions(-) diff --git a/vsintegration/src/FSharp.Editor/Refactor/ExtractLetBinding.fs b/vsintegration/src/FSharp.Editor/Refactor/ExtractLetBinding.fs index 947e5c4871a..8233a79ae43 100644 --- a/vsintegration/src/FSharp.Editor/Refactor/ExtractLetBinding.fs +++ b/vsintegration/src/FSharp.Editor/Refactor/ExtractLetBinding.fs @@ -42,12 +42,18 @@ type internal FSharpExtractLetBindingRefactoring [] () = cancellableTask { let document = context.Document - if not (context.Span.IsEmpty || document.IsFSharpSignatureFile) then + if not document.IsFSharpSignatureFile then let! cancellationToken = CancellableTask.getCancellationToken () let! sourceText = document.GetTextAsync cancellationToken let! parseResults = document.GetFSharpParseResultsAsync(nameof FSharpExtractLetBindingRefactoring) - match tryExtractionTarget sourceText parseResults.ParseTree context.Span with + let target = + if context.Span.IsEmpty then + tryConstantAtCaret sourceText parseResults.ParseTree context.Span.Start + else + tryExtractionTarget sourceText parseResults.ParseTree context.Span + + match target with | ValueNone -> () | ValueSome target -> let! options = document.GetOptionsAsync cancellationToken @@ -58,14 +64,15 @@ type internal FSharpExtractLetBindingRefactoring [] () = let names = usedNames parseResults.ParseTree let literalLines = linesInsideLiterals parseResults.ParseTree - match anchorsOf target.Expr target.Path with - | anchor :: _ -> - let name = uniqueName "extracted" names + if not context.Span.IsEmpty then + match anchorsOf target.Expr target.Path with + | anchor :: _ -> + let name = uniqueName "extracted" names - match tryDeclareInFront sourceText target anchor $"let {name}" name indentSize literalLines with - | ValueSome changes -> register context sourceText (SR.ExtractToLetBinding()) "let" changes - | ValueNone -> () - | [] -> () + match tryDeclareInFront sourceText target anchor $"let {name}" name indentSize literalLines with + | ValueSome changes -> register context sourceText (SR.ExtractToLetBinding()) "let" changes + | ValueNone -> () + | [] -> () let constant = match target.Expr with diff --git a/vsintegration/src/FSharp.Editor/Refactor/RefactoringHelpers.fs b/vsintegration/src/FSharp.Editor/Refactor/RefactoringHelpers.fs index 1b4cd9681df..0a06acfc773 100644 --- a/vsintegration/src/FSharp.Editor/Refactor/RefactoringHelpers.fs +++ b/vsintegration/src/FSharp.Editor/Refactor/RefactoringHelpers.fs @@ -371,6 +371,36 @@ let isLiteralConstant (expr: SynExpr) = _) -> true | _ -> false +/// The literal constant the caret is in or touches. +let tryConstantAtCaret (sourceText: SourceText) (parseTree: ParsedInput) (caret: int) = + let position = positionOf sourceText caret + + let constant = + (position, parseTree) + ||> ParsedInput.tryPickLast (fun path node -> + match node with + | SyntaxNode.SynExpr(SynExpr.Const(range = m) as expr) when + isLiteralConstant expr + && Position.posGeq position m.Start + && Position.posGeq m.End position + && not (isInExcludedContext expr path) + -> + Some(expr, path) + | _ -> None) + + match constant with + | Some(expr, path) -> + let span = textSpanOf sourceText expr.Range + + ValueSome + { + Expr = expr + Path = path + Content = span + Replaced = span + } + | None -> ValueNone + /// The module-level let declaration containing the path, with its first binding. let tryEnclosingModuleLet (path: SyntaxVisitorPath) = path diff --git a/vsintegration/tests/FSharp.Editor.Tests/Refactors/ExtractLetBindingTests.fs b/vsintegration/tests/FSharp.Editor.Tests/Refactors/ExtractLetBindingTests.fs index e30e122c27b..bb356ad5fa5 100644 --- a/vsintegration/tests/FSharp.Editor.Tests/Refactors/ExtractLetBindingTests.fs +++ b/vsintegration/tests/FSharp.Editor.Tests/Refactors/ExtractLetBindingTests.fs @@ -17,11 +17,14 @@ let private extractToLiteral = "Extract to literal" let private selectionOf (code: string) (selected: string) = TextSpan(code.IndexOf(selected, StringComparison.Ordinal), selected.Length) -let private extracted (title: string) (code: string) (selected: string) = +let private caretAt (code: string) (marker: string) = + TextSpan(code.IndexOf(marker, StringComparison.Ordinal), 0) + +let private extractedAt (title: string) (code: string) (span: TextSpan) = use context = TestContext.CreateWithCode code let document = - refactorSpan code (selectionOf code selected) title context (new FSharpExtractLetBindingRefactoring()) + refactorSpan code span title context (new FSharpExtractLetBindingRefactoring()) let parseResults = document.GetFSharpParseResultsAsync "test" @@ -30,13 +33,19 @@ let private extracted (title: string) (code: string) (selected: string) = Assert.Empty(parseResults.Diagnostics) (document.GetTextAsync() |> GetTaskResult).ToString() -let private titlesFor (code: string) (selected: string) = +let private extracted (title: string) (code: string) (selected: string) = + extractedAt title code (selectionOf code selected) + +let private titlesAt (code: string) (span: TextSpan) = use context = TestContext.CreateWithCode code - tryGetRefactoringActionsForSpan code (selectionOf code selected) context (new FSharpExtractLetBindingRefactoring()) + tryGetRefactoringActionsForSpan code span context (new FSharpExtractLetBindingRefactoring()) |> Seq.map _.Title |> List.ofSeq +let private titlesFor (code: string) (selected: string) = + titlesAt code (selectionOf code selected) + [] [] [] @@ -64,6 +73,19 @@ let ``Multi-line right-hand side is bound in front of its let`` () = Assert.Equal(expected, extracted extractToLetBinding code "items\n |> List.filter (fun i -> i > acc)\n |> List.sum") +[] +let ``Whole lines selected with their indentation and line break are extracted`` () = + let code = + "module M\n\nlet run items =\n let mutable acc = 0\n\n let total =\n items\n |> List.filter (fun i -> i > acc)\n |> List.sum\n\n total\n" + + let expected = + "module M\n\nlet run items =\n let mutable acc = 0\n\n let extracted =\n items\n |> List.filter (fun i -> i > acc)\n |> List.sum\n let total =\n extracted\n\n total\n" + + Assert.Equal( + expected, + extracted extractToLetBinding code " items\n |> List.filter (fun i -> i > acc)\n |> List.sum\n" + ) + [] let ``Match clause body on the arrow line moves to its own lines`` () = let code = @@ -155,6 +177,30 @@ let ``Literal is offered only for constants outside types`` () = Assert.Equal([ extractToLetBinding ], titlesFor "module M\n\ntype T() =\n member _.M() = 42\n" "42") Assert.Equal([ extractToLetBinding ], titlesFor "module M\n\nlet f x = g (x + 1)\n" "x + 1") +[] +[] +[] +[] +let ``Constant at the caret becomes a literal without a selection`` (marker: string) = + let code = "module M\n\nlet greet name =\n printfn \"Hello %s\" name\n" + + let expected = + "module M\n\n[]\nlet ExtractedConstant = \"Hello %s\"\n\nlet greet name =\n printfn ExtractedConstant name\n" + + Assert.Equal(expected, extractedAt extractToLiteral code (caretAt code marker)) + +[] +let ``Only the literal is offered without a selection`` () = + let code = "module M\n\nlet f () = g 42\n" + Assert.Equal([ extractToLiteral ], titlesAt code (caretAt code "2\n")) + +[] +[] +[] +[] +let ``No action without a selection`` (code: string, marker: string) = + Assert.Empty(titlesAt code (caretAt code marker)) + [] [] [] From e1ef9e57d173cd88031a688534b2acb765044c70 Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Mon, 14 Sep 2026 13:24:19 +0200 Subject: [PATCH 4/9] Mention the caret in the Extract to literal release note Co-Authored-By: Claude Opus 5 (1M context) --- docs/release-notes/.VisualStudio/18.vNext.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/release-notes/.VisualStudio/18.vNext.md b/docs/release-notes/.VisualStudio/18.vNext.md index f237efc3cd5..03d77d90256 100644 --- a/docs/release-notes/.VisualStudio/18.vNext.md +++ b/docs/release-notes/.VisualStudio/18.vNext.md @@ -2,7 +2,7 @@ * Code-fixes for FS3888 (compiler-semantic attribute on the `.fs` but not the `.fsi`): copy the attribute into the `.fsi`, or remove it from the `.fs`. ([Issue #19560](https://github.com/dotnet/fsharp/issues/19560), [PR #19880](https://github.com/dotnet/fsharp/pull/19880)) * Expand `` in IDE tooltips, completion, and signature help, inheriting XML documentation from base classes, interfaces, overridden members, and constructors. ([Issue #19175](https://github.com/dotnet/fsharp/issues/19175), [PR #19188](https://github.com/dotnet/fsharp/pull/19188)) -* **Extract to let binding** and **Extract to literal** refactorings for a selected expression. The value is bound in front of the statement that uses it, so it is computed before the code that preceded the selection in that statement; a constant in a module-level declaration becomes a `[]` in front of that declaration. +* **Extract to let binding** and **Extract to literal** refactorings for a selected expression. The value is bound in front of the statement that uses it, so it is computed before the code that preceded the selection in that statement; a constant in a module-level declaration becomes a `[]` in front of that declaration, also with just the caret inside the constant. * **Extract to local function**, **Extract to module function** and **Extract to private member** refactorings for a selected expression. Values the selection reads from the enclosing function, member or lambda become parameters in the order they are first used, typed as the new **Parameter types in Extract to function** option under **Code Fixes** asks. A selection that assigns to a captured mutable local, or reads a byref or byref-like value from outside, is not offered. ### Fixed From 63409f8697b634a4a53d4f7c2f48be9a4da42ffe Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Mon, 14 Sep 2026 13:33:14 +0200 Subject: [PATCH 5/9] Extract a parenthesized lambda from a caret in its fun ... -> header 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) --- .../Refactor/ExtractLetBinding.fs | 16 ++++++++++---- .../Refactor/RefactoringHelpers.fs | 20 +++++++++++++++++ .../Refactors/ExtractLetBindingTests.fs | 22 +++++++++++++++++++ 3 files changed, 54 insertions(+), 4 deletions(-) diff --git a/vsintegration/src/FSharp.Editor/Refactor/ExtractLetBinding.fs b/vsintegration/src/FSharp.Editor/Refactor/ExtractLetBinding.fs index 8233a79ae43..4bc595bf49d 100644 --- a/vsintegration/src/FSharp.Editor/Refactor/ExtractLetBinding.fs +++ b/vsintegration/src/FSharp.Editor/Refactor/ExtractLetBinding.fs @@ -47,11 +47,19 @@ type internal FSharpExtractLetBindingRefactoring [] () = let! sourceText = document.GetTextAsync cancellationToken let! parseResults = document.GetFSharpParseResultsAsync(nameof FSharpExtractLetBindingRefactoring) - let target = + let lambdaAtCaret = if context.Span.IsEmpty then - tryConstantAtCaret sourceText parseResults.ParseTree context.Span.Start + tryParenthesizedLambdaAtCaret sourceText parseResults.ParseTree context.Span.Start else - tryExtractionTarget sourceText parseResults.ParseTree context.Span + ValueNone + + let isSelected = not context.Span.IsEmpty || lambdaAtCaret.IsSome + + let target = + match lambdaAtCaret with + | ValueSome _ -> lambdaAtCaret + | ValueNone when context.Span.IsEmpty -> tryConstantAtCaret sourceText parseResults.ParseTree context.Span.Start + | ValueNone -> tryExtractionTarget sourceText parseResults.ParseTree context.Span match target with | ValueNone -> () @@ -64,7 +72,7 @@ type internal FSharpExtractLetBindingRefactoring [] () = let names = usedNames parseResults.ParseTree let literalLines = linesInsideLiterals parseResults.ParseTree - if not context.Span.IsEmpty then + if isSelected then match anchorsOf target.Expr target.Path with | anchor :: _ -> let name = uniqueName "extracted" names diff --git a/vsintegration/src/FSharp.Editor/Refactor/RefactoringHelpers.fs b/vsintegration/src/FSharp.Editor/Refactor/RefactoringHelpers.fs index 0a06acfc773..736e1328a61 100644 --- a/vsintegration/src/FSharp.Editor/Refactor/RefactoringHelpers.fs +++ b/vsintegration/src/FSharp.Editor/Refactor/RefactoringHelpers.fs @@ -401,6 +401,26 @@ let tryConstantAtCaret (sourceText: SourceText) (parseTree: ParsedInput) (caret: } | None -> ValueNone +/// The parenthesized lambda whose `fun ->` the caret is in, as if it were selected with its parentheses. +let tryParenthesizedLambdaAtCaret (sourceText: SourceText) (parseTree: ParsedInput) (caret: int) = + let position = positionOf sourceText caret + + let parenthesized = + (position, parseTree) + ||> ParsedInput.tryPickLast (fun _ node -> + match node with + | SyntaxNode.SynExpr(SynExpr.Paren( + expr = SynExpr.Lambda(parsedData = Some _; trivia = { ArrowRange = Some arrow }) as lambda; range = m)) when + Position.posGeq position lambda.Range.Start + && Position.posGeq arrow.End position + -> + Some m + | _ -> None) + + match parenthesized with + | Some m -> tryExtractionTarget sourceText parseTree (textSpanOf sourceText m) + | None -> ValueNone + /// The module-level let declaration containing the path, with its first binding. let tryEnclosingModuleLet (path: SyntaxVisitorPath) = path diff --git a/vsintegration/tests/FSharp.Editor.Tests/Refactors/ExtractLetBindingTests.fs b/vsintegration/tests/FSharp.Editor.Tests/Refactors/ExtractLetBindingTests.fs index bb356ad5fa5..7974f2ba4b8 100644 --- a/vsintegration/tests/FSharp.Editor.Tests/Refactors/ExtractLetBindingTests.fs +++ b/vsintegration/tests/FSharp.Editor.Tests/Refactors/ExtractLetBindingTests.fs @@ -177,6 +177,28 @@ let ``Literal is offered only for constants outside types`` () = Assert.Equal([ extractToLetBinding ], titlesFor "module M\n\ntype T() =\n member _.M() = 42\n" "42") Assert.Equal([ extractToLetBinding ], titlesFor "module M\n\nlet f x = g (x + 1)\n" "x + 1") +[] +[] +[")>] +[ x")>] +[] +let ``Caret in the header of a parenthesized lambda extracts the lambda`` (marker: string) = + let code = "module M\n\nlet f xs =\n xs |> List.map (fun x -> x + 1)\n" + + let expected = + "module M\n\nlet f xs =\n let extracted = fun x -> x + 1\n xs |> List.map extracted\n" + + Assert.Equal([ extractToLetBinding ], titlesAt code (caretAt code marker)) + Assert.Equal(expected, extractedAt extractToLetBinding code (caretAt code marker)) + +[] +[ List.map (fun x -> x + 1)\n", "x + 1)")>] +[ List.map (fun x -> x + 1)\n", "+ 1)")>] +[ List.map (fun x -> x + 1)\n", "(fun")>] +[ x + 1\n", "fun")>] +let ``No action without a selection outside a parenthesized lambda header`` (code: string, marker: string) = + Assert.Empty(titlesAt code (caretAt code marker)) + [] [] [] From 133dc366e77223e4e0eca9a1b641b69831d6e2b7 Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Mon, 14 Sep 2026 13:36:55 +0200 Subject: [PATCH 6/9] Offer Extract to function from a caret in a parenthesized lambda's header Co-Authored-By: Claude Opus 5 (1M context) --- .../FSharp.Editor/Refactor/ExtractFunction.fs | 10 ++++-- .../Refactors/ExtractFunctionTests.fs | 33 ++++++++++++++++--- 2 files changed, 37 insertions(+), 6 deletions(-) diff --git a/vsintegration/src/FSharp.Editor/Refactor/ExtractFunction.fs b/vsintegration/src/FSharp.Editor/Refactor/ExtractFunction.fs index e2c3ba3858b..e804e2c8da2 100644 --- a/vsintegration/src/FSharp.Editor/Refactor/ExtractFunction.fs +++ b/vsintegration/src/FSharp.Editor/Refactor/ExtractFunction.fs @@ -253,12 +253,18 @@ type internal FSharpExtractFunctionRefactoring [] () = cancellableTask { let document = context.Document - if not (context.Span.IsEmpty || document.IsFSharpSignatureFile) then + if not document.IsFSharpSignatureFile then let! cancellationToken = CancellableTask.getCancellationToken () let! sourceText = document.GetTextAsync cancellationToken let! parseResults = document.GetFSharpParseResultsAsync(nameof FSharpExtractFunctionRefactoring) - match tryExtractionTarget sourceText parseResults.ParseTree context.Span with + let target = + if context.Span.IsEmpty then + tryParenthesizedLambdaAtCaret sourceText parseResults.ParseTree context.Span.Start + else + tryExtractionTarget sourceText parseResults.ParseTree context.Span + + match target with | ValueNone -> () | ValueSome target -> match FunctionExtraction.enclosingScope target.Path with diff --git a/vsintegration/tests/FSharp.Editor.Tests/Refactors/ExtractFunctionTests.fs b/vsintegration/tests/FSharp.Editor.Tests/Refactors/ExtractFunctionTests.fs index b53b0753241..e5eefe8bb3a 100644 --- a/vsintegration/tests/FSharp.Editor.Tests/Refactors/ExtractFunctionTests.fs +++ b/vsintegration/tests/FSharp.Editor.Tests/Refactors/ExtractFunctionTests.fs @@ -27,11 +27,14 @@ let private contextFor (setting: ParameterAnnotationSetting) (code: string) = new TestContext(RoslynTestHelpers.CreateSolution(code, editorOptions = options)) -let private extractedWith (setting: ParameterAnnotationSetting) (title: string) (code: string) (selected: string) = +let private caretAt (code: string) (marker: string) = + TextSpan(code.IndexOf(marker, StringComparison.Ordinal), 0) + +let private extractedAtWith (setting: ParameterAnnotationSetting) (title: string) (code: string) (span: TextSpan) = use context = contextFor setting code let document = - refactorSpan code (selectionOf code selected) title context (new FSharpExtractFunctionRefactoring()) + refactorSpan code span title context (new FSharpExtractFunctionRefactoring()) let parseResults = document.GetFSharpParseResultsAsync "test" @@ -40,15 +43,37 @@ let private extractedWith (setting: ParameterAnnotationSetting) (title: string) Assert.Empty(parseResults.Diagnostics) (document.GetTextAsync() |> GetTaskResult).ToString() +let private extractedWith (setting: ParameterAnnotationSetting) (title: string) (code: string) (selected: string) = + extractedAtWith setting title code (selectionOf code selected) + let private extracted = extractedWith ParameterAnnotationSetting.Always -let private titlesFor (code: string) (selected: string) = +let private titlesAt (code: string) (span: TextSpan) = use context = contextFor ParameterAnnotationSetting.Always code - tryGetRefactoringActionsForSpan code (selectionOf code selected) context (new FSharpExtractFunctionRefactoring()) + tryGetRefactoringActionsForSpan code span context (new FSharpExtractFunctionRefactoring()) |> Seq.map _.Title |> List.ofSeq +let private titlesFor (code: string) (selected: string) = + titlesAt code (selectionOf code selected) + +[] +let ``Caret in the header of a parenthesized lambda extracts it as if it were selected`` () = + let code = + "module M\n\nlet f (xs: int list) (n: int) =\n xs |> List.map (fun x -> x + n)\n" + + let lambda = "(fun x -> x + n)" + let titles = titlesFor code lambda + + Assert.NotEmpty(titles) + Assert.Equal(titles, titlesAt code (caretAt code "x ->")) + + for title in titles do + Assert.Equal(extracted title code lambda, extractedAtWith ParameterAnnotationSetting.Always title code (caretAt code "x ->")) + + Assert.Empty(titlesAt code (caretAt code "x + n")) + let private area = "module M\n\nlet area (w: int) (h: int) =\n printfn \"%d\" (w * h + 1)\n" From 7afa0cb9bcd9b13d311c73a94dec7251bcc74abd Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Mon, 14 Sep 2026 15:30:50 +0200 Subject: [PATCH 7/9] Write the refactoring test code as multi-line strings Co-Authored-By: Claude Opus 5 (1M context) --- .../Refactors/ExtractLetBindingTests.fs | 452 +++++++++++++++--- 1 file changed, 377 insertions(+), 75 deletions(-) diff --git a/vsintegration/tests/FSharp.Editor.Tests/Refactors/ExtractLetBindingTests.fs b/vsintegration/tests/FSharp.Editor.Tests/Refactors/ExtractLetBindingTests.fs index 7974f2ba4b8..50078b08db0 100644 --- a/vsintegration/tests/FSharp.Editor.Tests/Refactors/ExtractLetBindingTests.fs +++ b/vsintegration/tests/FSharp.Editor.Tests/Refactors/ExtractLetBindingTests.fs @@ -46,91 +46,246 @@ let private titlesAt (code: string) (span: TextSpan) = let private titlesFor (code: string) (selected: string) = titlesAt code (selectionOf code selected) +let private area = + """ +module M + +let area w h = + printfn "%d" (w * h + 1) +""" + [] -[] -[] -let ``Expression in a statement is bound in front of it`` (selected: string, body: string) = - let code = "module M\n\nlet area w h =\n printfn \"%d\" (w * h + 1)\n" - Assert.Equal($"module M\n\nlet area w h =\n{body}", extracted extractToLetBinding code selected) +[] +[] +let ``Expression in a statement is bound in front of it`` (selected: string, expected: string) = + Assert.Equal(expected, extracted extractToLetBinding area selected) [] let ``Parentheses of a method call stay`` () = let code = - "module M\n\nlet append (sb: System.Text.StringBuilder) =\n sb.Append(1 + 2)\n" + """ +module M + +let append (sb: System.Text.StringBuilder) = + sb.Append(1 + 2) +""" let expected = - "module M\n\nlet append (sb: System.Text.StringBuilder) =\n let extracted = 1 + 2\n sb.Append(extracted)\n" + """ +module M + +let append (sb: System.Text.StringBuilder) = + let extracted = 1 + 2 + sb.Append(extracted) +""" Assert.Equal(expected, extracted extractToLetBinding code "(1 + 2)") +let private run = + """ +module M + +let run items = + let mutable acc = 0 + let total = + items + |> List.filter (fun i -> i > acc) + |> List.sum + total +""" + [] let ``Multi-line right-hand side is bound in front of its let`` () = - let code = - "module M\n\nlet run items =\n let mutable acc = 0\n let total =\n items\n |> List.filter (fun i -> i > acc)\n |> List.sum\n total\n" - let expected = - "module M\n\nlet run items =\n let mutable acc = 0\n let extracted =\n items\n |> List.filter (fun i -> i > acc)\n |> List.sum\n let total =\n extracted\n total\n" + """ +module M - Assert.Equal(expected, extracted extractToLetBinding code "items\n |> List.filter (fun i -> i > acc)\n |> List.sum") +let run items = + let mutable acc = 0 + let extracted = + items + |> List.filter (fun i -> i > acc) + |> List.sum + let total = + extracted + total +""" + + let start = run.LastIndexOf("items", StringComparison.Ordinal) + + let finish = + run.IndexOf("|> List.sum", start, StringComparison.Ordinal) + + "|> List.sum".Length + + Assert.Equal(expected, extractedAt extractToLetBinding run (TextSpan.FromBounds(start, finish))) [] let ``Whole lines selected with their indentation and line break are extracted`` () = let code = - "module M\n\nlet run items =\n let mutable acc = 0\n\n let total =\n items\n |> List.filter (fun i -> i > acc)\n |> List.sum\n\n total\n" + """ +module M + +let run items = + let mutable acc = 0 + + let total = + items + |> List.filter (fun i -> i > acc) + |> List.sum + + total +""" let expected = - "module M\n\nlet run items =\n let mutable acc = 0\n\n let extracted =\n items\n |> List.filter (fun i -> i > acc)\n |> List.sum\n let total =\n extracted\n\n total\n" + """ +module M + +let run items = + let mutable acc = 0 + + let extracted = + items + |> List.filter (fun i -> i > acc) + |> List.sum + let total = + extracted - Assert.Equal( - expected, - extracted extractToLetBinding code " items\n |> List.filter (fun i -> i > acc)\n |> List.sum\n" - ) + total +""" + + let lineStart = code.LastIndexOf(" items", StringComparison.Ordinal) + + let afterLineBreak = + code.IndexOf('\n', code.IndexOf("|> List.sum", lineStart, StringComparison.Ordinal)) + + 1 + + Assert.Equal(expected, extractedAt extractToLetBinding code (TextSpan.FromBounds(lineStart, afterLineBreak))) [] let ``Match clause body on the arrow line moves to its own lines`` () = let code = - "module M\n\nlet f x offset =\n match x with\n | Some v -> v * 2 + offset\n | None -> 0\n" + """ +module M + +let f x offset = + match x with + | Some v -> v * 2 + offset + | None -> 0 +""" let expected = - "module M\n\nlet f x offset =\n match x with\n | Some v ->\n let extracted = v * 2 + offset\n extracted\n | None -> 0\n" + """ +module M + +let f x offset = + match x with + | Some v -> + let extracted = v * 2 + offset + extracted + | None -> 0 +""" Assert.Equal(expected, extracted extractToLetBinding code "v * 2 + offset") [] let ``Right-hand side on the equals line keeps its trailing comment`` () = - let code = "module M\n\nlet r = compute a b // slow\n" + let code = + """ +module M + +let r = compute a b // slow +""" let expected = - "module M\n\nlet r =\n let extracted = compute a b\n extracted // slow\n" + """ +module M + +let r = + let extracted = compute a b + extracted // slow +""" Assert.Equal(expected, extracted extractToLetBinding code "compute a b") [] let ``Expression in a computation expression is bound in front of its statement`` () = let code = - "module M\n\nlet load id = async {\n let! raw = fetch id\n return parse raw |> List.length }\n" + """ +module M + +let load id = async { + let! raw = fetch id + return parse raw |> List.length } +""" let expected = - "module M\n\nlet load id = async {\n let! raw = fetch id\n let extracted = parse raw |> List.length\n return extracted }\n" + """ +module M + +let load id = async { + let! raw = fetch id + let extracted = parse raw |> List.length + return extracted } +""" Assert.Equal(expected, extracted extractToLetBinding code "parse raw |> List.length") [] let ``Lambda body on the arrow line moves to its own lines`` () = - let code = "module M\n\nlet r = xs |> List.map (fun x -> x + 1)\n" + let code = + """ +module M + +let r = xs |> List.map (fun x -> x + 1) +""" let expected = - "module M\n\nlet r = xs |> List.map (fun x ->\n let extracted = x + 1\n extracted)\n" + """ +module M + +let r = xs |> List.map (fun x -> + let extracted = x + 1 + extracted) +""" Assert.Equal(expected, extracted extractToLetBinding code "x + 1") [] let ``Name does not collide with an existing identifier`` () = let code = - "module M\n\nlet extracted = 0\n\nlet f x =\n printfn \"%d\" (x + 1)\n" + """ +module M + +let extracted = 0 + +let f x = + printfn "%d" (x + 1) +""" let expected = - "module M\n\nlet extracted = 0\n\nlet f x =\n let extracted1 = x + 1\n printfn \"%d\" (extracted1)\n" + """ +module M + +let extracted = 0 + +let f x = + let extracted1 = x + 1 + printfn "%d" (extracted1) +""" Assert.Equal(expected, extracted extractToLetBinding code "x + 1") @@ -145,6 +300,7 @@ let ``Line breaks of the file are kept`` () = [] let ``Lines inside a multi-line string are not re-indented`` () = + // The code contains a triple-quoted string, which a triple-quoted literal cannot hold. let code = "module M\n\nlet f name =\n printfn \"%s\" (String.Format(\"\"\"Hello\n{0}\"\"\", name))\n" @@ -153,29 +309,85 @@ let ``Lines inside a multi-line string are not re-indented`` () = Assert.Equal(expected, extracted extractToLetBinding code "String.Format(\"\"\"Hello\n{0}\"\"\", name)") -[] -let ``Constant in a module-level declaration becomes a literal in front of it`` () = - let code = "module M\n\nlet greet name =\n printfn \"Hello %s\" name\n" +let private greet = + """ +module M - let expected = - "module M\n\n[]\nlet ExtractedConstant = \"Hello %s\"\n\nlet greet name =\n printfn ExtractedConstant name\n" +let greet name = + printfn "Hello %s" name +""" - Assert.Equal(expected, extracted extractToLiteral code "\"Hello %s\"") +let private greetWithLiteral = + """ +module M + +[] +let ExtractedConstant = "Hello %s" + +let greet name = + printfn ExtractedConstant name +""" + +[] +let ``Constant in a module-level declaration becomes a literal in front of it`` () = + Assert.Equal(greetWithLiteral, extracted extractToLiteral greet "\"Hello %s\"") [] let ``Negative number becomes a literal`` () = - let code = "module M\n\nlet f () = g -1\n" + let code = + """ +module M + +let f () = g -1 +""" let expected = - "module M\n\n[]\nlet ExtractedConstant = -1\n\nlet f () = g ExtractedConstant\n" + """ +module M + +[] +let ExtractedConstant = -1 + +let f () = g ExtractedConstant +""" Assert.Equal(expected, extracted extractToLiteral code "-1") +let private moduleConstant = + """ +module M + +let f () = g 42 +""" + [] let ``Literal is offered only for constants outside types`` () = - Assert.Equal([ extractToLetBinding; extractToLiteral ], titlesFor "module M\n\nlet f () = g 42\n" "42") - Assert.Equal([ extractToLetBinding ], titlesFor "module M\n\ntype T() =\n member _.M() = 42\n" "42") - Assert.Equal([ extractToLetBinding ], titlesFor "module M\n\nlet f x = g (x + 1)\n" "x + 1") + let memberConstant = + """ +module M + +type T() = + member _.M() = 42 +""" + + let expression = + """ +module M + +let f x = g (x + 1) +""" + + Assert.Equal([ extractToLetBinding; extractToLiteral ], titlesFor moduleConstant "42") + Assert.Equal([ extractToLetBinding ], titlesFor memberConstant "42") + Assert.Equal([ extractToLetBinding ], titlesFor expression "x + 1") + +let private mapIncrement = + """ +module M + +let f xs = + xs |> List.map (fun x -> x + 1) +""" [] [] @@ -183,56 +395,146 @@ let ``Literal is offered only for constants outside types`` () = [ x")>] [] let ``Caret in the header of a parenthesized lambda extracts the lambda`` (marker: string) = - let code = "module M\n\nlet f xs =\n xs |> List.map (fun x -> x + 1)\n" - let expected = - "module M\n\nlet f xs =\n let extracted = fun x -> x + 1\n xs |> List.map extracted\n" + """ +module M - Assert.Equal([ extractToLetBinding ], titlesAt code (caretAt code marker)) - Assert.Equal(expected, extractedAt extractToLetBinding code (caretAt code marker)) +let f xs = + let extracted = fun x -> x + 1 + xs |> List.map extracted +""" -[] -[ List.map (fun x -> x + 1)\n", "x + 1)")>] -[ List.map (fun x -> x + 1)\n", "+ 1)")>] -[ List.map (fun x -> x + 1)\n", "(fun")>] -[ x + 1\n", "fun")>] -let ``No action without a selection outside a parenthesized lambda header`` (code: string, marker: string) = - Assert.Empty(titlesAt code (caretAt code marker)) + Assert.Equal([ extractToLetBinding ], titlesAt mapIncrement (caretAt mapIncrement marker)) + Assert.Equal(expected, extractedAt extractToLetBinding mapIncrement (caretAt mapIncrement marker)) [] -[] -[] -[] -let ``Constant at the caret becomes a literal without a selection`` (marker: string) = - let code = "module M\n\nlet greet name =\n printfn \"Hello %s\" name\n" +[] +[] +[] +let ``No action without a selection outside a parenthesized lambda header`` (marker: string) = + Assert.Empty(titlesAt mapIncrement (caretAt mapIncrement marker)) - let expected = - "module M\n\n[]\nlet ExtractedConstant = \"Hello %s\"\n\nlet greet name =\n printfn ExtractedConstant name\n" +[] +let ``No action without a selection on a lambda without parentheses`` () = + let code = + """ +module M + +let f = fun x -> x + 1 +""" - Assert.Equal(expected, extractedAt extractToLiteral code (caretAt code marker)) + Assert.Empty(titlesAt code (caretAt code "fun")) + +[] +[] +[] +[] +let ``Constant at the caret becomes a literal without a selection`` (marker: string, offset: int) = + let caret = TextSpan(greet.IndexOf(marker, StringComparison.Ordinal) + offset, 0) + Assert.Equal(greetWithLiteral, extractedAt extractToLiteral greet caret) [] let ``Only the literal is offered without a selection`` () = - let code = "module M\n\nlet f () = g 42\n" - Assert.Equal([ extractToLiteral ], titlesAt code (caretAt code "2\n")) + let caret = TextSpan(moduleConstant.IndexOf("42", StringComparison.Ordinal) + 1, 0) + + Assert.Equal([ extractToLiteral ], titlesAt moduleConstant caret) [] -[] -[] -[] +[] +[] +[] let ``No action without a selection`` (code: string, marker: string) = Assert.Empty(titlesAt code (caretAt code marker)) [] -[] -[] -[] -[] -[ 0 -> v\n | _ -> 0\n", "v > 0")>] -[] -[] -[] -[] -[ 0\n", "s.Length > 0")>] -[] +[] +[] +[] +[] +[ 0 -> v + | _ -> 0 +""", + "v > 0")>] +[] +[] +[] +[] +[ 0 +""", + "s.Length > 0")>] +[] let ``No action`` (code: string, selected: string) = Assert.Empty(titlesFor code selected) From dc118dfe919ac0c85c0aa51986b8202ee3056e2b Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Mon, 14 Sep 2026 15:33:57 +0200 Subject: [PATCH 8/9] Write the extract function test code as multi-line strings Co-Authored-By: Claude Opus 5 (1M context) --- .../Refactors/ExtractFunctionTests.fs | 135 +++++++++++++++--- 1 file changed, 112 insertions(+), 23 deletions(-) diff --git a/vsintegration/tests/FSharp.Editor.Tests/Refactors/ExtractFunctionTests.fs b/vsintegration/tests/FSharp.Editor.Tests/Refactors/ExtractFunctionTests.fs index e5eefe8bb3a..5cc166c0a99 100644 --- a/vsintegration/tests/FSharp.Editor.Tests/Refactors/ExtractFunctionTests.fs +++ b/vsintegration/tests/FSharp.Editor.Tests/Refactors/ExtractFunctionTests.fs @@ -61,7 +61,12 @@ let private titlesFor (code: string) (selected: string) = [] let ``Caret in the header of a parenthesized lambda extracts it as if it were selected`` () = let code = - "module M\n\nlet f (xs: int list) (n: int) =\n xs |> List.map (fun x -> x + n)\n" + """ +module M + +let f (xs: int list) (n: int) = + xs |> List.map (fun x -> x + n) +""" let lambda = "(fun x -> x + n)" let titles = titlesFor code lambda @@ -75,45 +80,85 @@ let ``Caret in the header of a parenthesized lambda extracts it as if it were se Assert.Empty(titlesAt code (caretAt code "x + n")) let private area = - "module M\n\nlet area (w: int) (h: int) =\n printfn \"%d\" (w * h + 1)\n" + """ +module M + +let area (w: int) (h: int) = + printfn "%d" (w * h + 1) +""" + +let private areaWithLocalFunction = + """ +module M + +let area (w: int) (h: int) = + let extractedFunction (w: int) (h: int) = w * h + 1 + printfn "%d" (extractedFunction w h) +""" [] let ``Captured parameters become parameters of a local function`` () = - let expected = - "module M\n\nlet area (w: int) (h: int) =\n let extractedFunction (w: int) (h: int) = w * h + 1\n printfn \"%d\" (extractedFunction w h)\n" - - Assert.Equal(expected, extracted extractToLocalFunction area "w * h + 1") + Assert.Equal(areaWithLocalFunction, extracted extractToLocalFunction area "w * h + 1") [] let ``Module function is declared in front of the declaration using it`` () = let expected = - "module M\n\nlet private extractedFunction (w: int) (h: int) = w * h + 1\n\nlet area (w: int) (h: int) =\n printfn \"%d\" (extractedFunction w h)\n" + """ +module M + +let private extractedFunction (w: int) (h: int) = w * h + 1 + +let area (w: int) (h: int) = + printfn "%d" (extractedFunction w h) +""" Assert.Equal(expected, extracted extractToModuleFunction area "w * h + 1") [] let ``Call keeps its parentheses where the selection lost them`` () = - let expected = - "module M\n\nlet area (w: int) (h: int) =\n let extractedFunction (w: int) (h: int) = w * h + 1\n printfn \"%d\" (extractedFunction w h)\n" - - Assert.Equal(expected, extracted extractToLocalFunction area "(w * h + 1)") + Assert.Equal(areaWithLocalFunction, extracted extractToLocalFunction area "(w * h + 1)") [] let ``Selection without captures becomes a function of unit`` () = - let code = "module M\n\nlet f () =\n printfn \"%d\" (1 + 2)\n" + let code = + """ +module M + +let f () = + printfn "%d" (1 + 2) +""" let expected = - "module M\n\nlet f () =\n let extractedFunction () = 1 + 2\n printfn \"%d\" (extractedFunction ())\n" + """ +module M + +let f () = + let extractedFunction () = 1 + 2 + printfn "%d" (extractedFunction ()) +""" Assert.Equal(expected, extracted extractToLocalFunction code "1 + 2") [] let ``Selection using this becomes a private member`` () = let code = - "module M\n\ntype Order(lines: int list) =\n member this.Rate = 3\n member this.Total = lines |> List.sumBy (fun l -> l * this.Rate)\n" + """ +module M + +type Order(lines: int list) = + member this.Rate = 3 + member this.Total = lines |> List.sumBy (fun l -> l * this.Rate) +""" let expected = - "module M\n\ntype Order(lines: int list) =\n member this.Rate = 3\n member this.Total = lines |> List.sumBy (fun l -> this.ExtractedMethod(l))\n member private this.ExtractedMethod(l: int) = l * this.Rate\n" + """ +module M + +type Order(lines: int list) = + member this.Rate = 3 + member this.Total = lines |> List.sumBy (fun l -> this.ExtractedMethod(l)) + member private this.ExtractedMethod(l: int) = l * this.Rate +""" Assert.Equal(expected, extracted extractToPrivateMember code "l * this.Rate") @@ -129,10 +174,21 @@ let ``Parameter annotations follow the option`` (setting: string, header: string | _ -> ParameterAnnotationSetting.Never let code = - "module M\n\nlet shout (s: string) (n: int) =\n printfn \"%s\" (s.ToUpper() + string n)\n" + """ +module M + +let shout (s: string) (n: int) = + printfn "%s" (s.ToUpper() + string n) +""" let expected = - $"module M\n\nlet shout (s: string) (n: int) =\n {header} s.ToUpper() + string n\n printfn \"%%s\" (extractedFunction s n)\n" + $""" +module M + +let shout (s: string) (n: int) = + {header} s.ToUpper() + string n + printfn "%%s" (extractedFunction s n) +""" Assert.Equal(expected, extractedWith setting extractToLocalFunction code "s.ToUpper() + string n") @@ -141,12 +197,45 @@ let ``Variants follow what the selection uses`` () = Assert.Equal([ extractToLocalFunction; extractToModuleFunction ], titlesFor area "w * h + 1") let derived = - "module M\n\ntype Derived() =\n inherit System.Object()\n override this.ToString() = base.ToString() + \"!\"\n" + """ +module M + +type Derived() = + inherit System.Object() + override this.ToString() = base.ToString() + "!" +""" Assert.Equal([ extractToPrivateMember ], titlesFor derived "base.ToString() + \"!\"") -[] -[] -[) =\n x <- x + 1\n", "x + 1")>] -let ``No action`` (code: string, selected: string) = Assert.Empty(titlesFor code selected) +[] +let ``No action when the selection assigns to a captured mutable local`` () = + let code = + """ +module M + +let counter () = + let mutable n = 0 + for i in 1 .. 3 do + n <- n + i + n +""" + + let start = code.IndexOf("for i in", StringComparison.Ordinal) + + let finish = + code.IndexOf("n <- n + i", start, StringComparison.Ordinal) + + "n <- n + i".Length + + Assert.Empty(titlesAt code (TextSpan.FromBounds(start, finish))) + +[] +let ``No action when the selection reads a byref parameter`` () = + let code = + """ +module M + +let incr (x: byref) = + x <- x + 1 +""" + + Assert.Empty(titlesFor code "x + 1") From a87f0e78cb7e4b90a472043ae0c4502a6192e239 Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Mon, 14 Sep 2026 21:36:11 +0200 Subject: [PATCH 9/9] Link the release note to the pull request and move it to a random line of its section Co-Authored-By: Claude Opus 5 (1M context) --- docs/release-notes/.VisualStudio/18.vNext.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/release-notes/.VisualStudio/18.vNext.md b/docs/release-notes/.VisualStudio/18.vNext.md index 03d77d90256..9cf38497fd2 100644 --- a/docs/release-notes/.VisualStudio/18.vNext.md +++ b/docs/release-notes/.VisualStudio/18.vNext.md @@ -1,9 +1,9 @@ ### Added +* **Extract to let binding** and **Extract to literal** refactorings for a selected expression. The value is bound in front of the statement that uses it, so it is computed before the code that preceded the selection in that statement; a constant in a module-level declaration becomes a `[]` in front of that declaration, also with just the caret inside the constant. ([PR #20537](https://github.com/dotnet/fsharp/pull/20537)) * Code-fixes for FS3888 (compiler-semantic attribute on the `.fs` but not the `.fsi`): copy the attribute into the `.fsi`, or remove it from the `.fs`. ([Issue #19560](https://github.com/dotnet/fsharp/issues/19560), [PR #19880](https://github.com/dotnet/fsharp/pull/19880)) +* **Extract to local function**, **Extract to module function** and **Extract to private member** refactorings for a selected expression. Values the selection reads from the enclosing function, member or lambda become parameters in the order they are first used, typed as the new **Parameter types in Extract to function** option under **Code Fixes** asks. A selection that assigns to a captured mutable local, or reads a byref or byref-like value from outside, is not offered. ([Issue #14449](https://github.com/dotnet/fsharp/issues/14449), [PR #20538](https://github.com/dotnet/fsharp/pull/20538)) * Expand `` in IDE tooltips, completion, and signature help, inheriting XML documentation from base classes, interfaces, overridden members, and constructors. ([Issue #19175](https://github.com/dotnet/fsharp/issues/19175), [PR #19188](https://github.com/dotnet/fsharp/pull/19188)) -* **Extract to let binding** and **Extract to literal** refactorings for a selected expression. The value is bound in front of the statement that uses it, so it is computed before the code that preceded the selection in that statement; a constant in a module-level declaration becomes a `[]` in front of that declaration, also with just the caret inside the constant. -* **Extract to local function**, **Extract to module function** and **Extract to private member** refactorings for a selected expression. Values the selection reads from the enclosing function, member or lambda become parameters in the order they are first used, typed as the new **Parameter types in Extract to function** option under **Code Fixes** asks. A selection that assigns to a captured mutable local, or reads a byref or byref-like value from outside, is not offered. ### Fixed