diff --git a/docs/release-notes/.VisualStudio/18.vNext.md b/docs/release-notes/.VisualStudio/18.vNext.md index ba03f663967..9cf38497fd2 100644 --- a/docs/release-notes/.VisualStudio/18.vNext.md +++ b/docs/release-notes/.VisualStudio/18.vNext.md @@ -1,6 +1,8 @@ ### 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)) ### Fixed diff --git a/vsintegration/src/FSharp.Editor/FSharp.Editor.fsproj b/vsintegration/src/FSharp.Editor/FSharp.Editor.fsproj index 319bdd5a264..ec7ad4d768c 100644 --- a/vsintegration/src/FSharp.Editor/FSharp.Editor.fsproj +++ b/vsintegration/src/FSharp.Editor/FSharp.Editor.fsproj @@ -101,10 +101,13 @@ + + + diff --git a/vsintegration/src/FSharp.Editor/FSharp.Editor.resx b/vsintegration/src/FSharp.Editor/FSharp.Editor.resx index 1f1f632d770..bc67591a010 100644 --- a/vsintegration/src/FSharp.Editor/FSharp.Editor.resx +++ b/vsintegration/src/FSharp.Editor/FSharp.Editor.resx @@ -368,4 +368,19 @@ Use live (unsaved) buffers for analysis Returns: + + Extract to let binding + + + 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..e804e2c8da2 --- /dev/null +++ b/vsintegration/src/FSharp.Editor/Refactor/ExtractFunction.fs @@ -0,0 +1,358 @@ +// 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 document.IsFSharpSignatureFile then + let! cancellationToken = CancellableTask.getCancellationToken () + let! sourceText = document.GetTextAsync cancellationToken + let! parseResults = document.GetFSharpParseResultsAsync(nameof FSharpExtractFunctionRefactoring) + + 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 + | 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/ExtractLetBinding.fs b/vsintegration/src/FSharp.Editor/Refactor/ExtractLetBinding.fs new file mode 100644 index 00000000000..4bc595bf49d --- /dev/null +++ b/vsintegration/src/FSharp.Editor/Refactor/ExtractLetBinding.fs @@ -0,0 +1,99 @@ +// 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 document.IsFSharpSignatureFile then + let! cancellationToken = CancellableTask.getCancellationToken () + let! sourceText = document.GetTextAsync cancellationToken + let! parseResults = document.GetFSharpParseResultsAsync(nameof FSharpExtractLetBindingRefactoring) + + let lambdaAtCaret = + if context.Span.IsEmpty then + tryParenthesizedLambdaAtCaret sourceText parseResults.ParseTree context.Span.Start + else + 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 -> () + | 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 + + if isSelected 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 -> () + | [] -> () + + 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..736e1328a61 --- /dev/null +++ b/vsintegration/src/FSharp.Editor/Refactor/RefactoringHelpers.fs @@ -0,0 +1,557 @@ +// 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 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 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 + |> 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 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..01279701304 100644 --- a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.cs.xlf +++ b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.cs.xlf @@ -105,6 +105,31 @@ 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 + + + + 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 bce1941f0b1..fa60ff5494c 100644 --- a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.de.xlf +++ b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.de.xlf @@ -105,6 +105,31 @@ 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 + + + + 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 fa8cb62c422..ddfd987090a 100644 --- a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.es.xlf +++ b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.es.xlf @@ -105,6 +105,31 @@ 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 + + + + 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 e7ec71e839e..c43f29dc65d 100644 --- a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.fr.xlf +++ b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.fr.xlf @@ -105,6 +105,31 @@ 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 + + + + 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 327a7ca362f..04f030c3df1 100644 --- a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.it.xlf +++ b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.it.xlf @@ -105,6 +105,31 @@ 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 + + + + 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 d45234c011a..8213f74ef52 100644 --- a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.ja.xlf +++ b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.ja.xlf @@ -105,6 +105,31 @@ Suggest names for unresolved identifiers; 否定の代わりに減算を使用する + + Extract to let binding + Extract to let binding + + + + Extract to literal + 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 3248e0641ea..2bfd70e9ef2 100644 --- a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.ko.xlf +++ b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.ko.xlf @@ -105,6 +105,31 @@ Suggest names for unresolved identifiers; 부정 대신 빼기 사용 + + Extract to let binding + Extract to let binding + + + + Extract to literal + 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 abc39f15da5..684851fbed4 100644 --- a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.pl.xlf +++ b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.pl.xlf @@ -105,6 +105,31 @@ 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 + + + + 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 dfde43120f5..17354c9587b 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,31 @@ 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 + + + + 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 47cda215312..da38a0f2a3a 100644 --- a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.ru.xlf +++ b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.ru.xlf @@ -105,6 +105,31 @@ Suggest names for unresolved identifiers; Используйте вычитание вместо отрицания. + + Extract to let binding + Extract to let binding + + + + Extract to literal + 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 58aa5d54c43..19e2697a3cb 100644 --- a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.tr.xlf +++ b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.tr.xlf @@ -105,6 +105,31 @@ 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 + + + + 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 4fa703776fb..05b7ab2414d 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,31 @@ Suggest names for unresolved identifiers; 使用减法代替求反 + + Extract to let binding + Extract to let binding + + + + Extract to literal + 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 fd46ef9919a..f2280d8cfd8 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,31 @@ Suggest names for unresolved identifiers; 使用減號代替否定 + + Extract to let binding + Extract to let binding + + + + Extract to literal + 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 ecce1205b8c..763b7981941 100644 --- a/vsintegration/tests/FSharp.Editor.Tests/FSharp.Editor.Tests.fsproj +++ b/vsintegration/tests/FSharp.Editor.Tests/FSharp.Editor.Tests.fsproj @@ -74,6 +74,8 @@ + + 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..5cc166c0a99 --- /dev/null +++ b/vsintegration/tests/FSharp.Editor.Tests/Refactors/ExtractFunctionTests.fs @@ -0,0 +1,241 @@ +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 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 span title context (new FSharpExtractFunctionRefactoring()) + + let parseResults = + document.GetFSharpParseResultsAsync "test" + |> CancellableTask.runSynchronouslyWithoutCancellation + + 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 titlesAt (code: string) (span: TextSpan) = + use context = contextFor ParameterAnnotationSetting.Always code + + 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 + +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 + + 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 + +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`` () = + 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 + +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`` () = + Assert.Equal(areaWithLocalFunction, extracted extractToLocalFunction area "(w * h + 1)") + +[] +let ``Selection without captures becomes a function of unit`` () = + let code = + """ +module M + +let f () = + printfn "%d" (1 + 2) +""" + + let expected = + """ +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 + +type Order(lines: int list) = + member this.Rate = 3 + member this.Total = lines |> List.sumBy (fun l -> l * this.Rate) +""" + + let expected = + """ +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") + +[] +[ ParameterAnnotationSetting.Always + | "WhenNeeded" -> ParameterAnnotationSetting.WhenNeeded + | _ -> ParameterAnnotationSetting.Never + + let code = + """ +module M + +let shout (s: string) (n: int) = + printfn "%s" (s.ToUpper() + string n) +""" + + let expected = + $""" +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") + +[] +let ``Variants follow what the selection uses`` () = + Assert.Equal([ extractToLocalFunction; extractToModuleFunction ], titlesFor area "w * h + 1") + + let derived = + """ +module M + +type Derived() = + inherit System.Object() + override this.ToString() = base.ToString() + "!" +""" + + Assert.Equal([ extractToPrivateMember ], titlesFor derived "base.ToString() + \"!\"") + +[] +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") 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..50078b08db0 --- /dev/null +++ b/vsintegration/tests/FSharp.Editor.Tests/Refactors/ExtractLetBindingTests.fs @@ -0,0 +1,540 @@ +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 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 span title context (new FSharpExtractLetBindingRefactoring()) + + let parseResults = + document.GetFSharpParseResultsAsync "test" + |> CancellableTask.runSynchronouslyWithoutCancellation + + Assert.Empty(parseResults.Diagnostics) + (document.GetTextAsync() |> GetTaskResult).ToString() + +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 span context (new FSharpExtractLetBindingRefactoring()) + |> Seq.map _.Title + |> List.ofSeq + +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, expected: string) = + Assert.Equal(expected, extracted extractToLetBinding area selected) + +[] +let ``Parentheses of a method call stay`` () = + let code = + """ +module M + +let append (sb: System.Text.StringBuilder) = + sb.Append(1 + 2) +""" + + let expected = + """ +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 expected = + """ +module M + +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 + +let run items = + let mutable acc = 0 + + let total = + items + |> List.filter (fun i -> i > acc) + |> List.sum + + total +""" + + let expected = + """ +module M + +let run items = + let mutable acc = 0 + + let extracted = + items + |> List.filter (fun i -> i > acc) + |> List.sum + let total = + extracted + + 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 + +let f x offset = + match x with + | Some v -> v * 2 + offset + | None -> 0 +""" + + let expected = + """ +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 + +let r = compute a b // slow +""" + + let expected = + """ +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 + +let load id = async { + let! raw = fetch id + return parse raw |> List.length } +""" + + let expected = + """ +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 + +let r = xs |> List.map (fun x -> x + 1) +""" + + let expected = + """ +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 + +let extracted = 0 + +let f x = + printfn "%d" (x + 1) +""" + + let expected = + """ +module M + +let extracted = 0 + +let f x = + let extracted1 = x + 1 + printfn "%d" (extracted1) +""" + + 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`` () = + // 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" + + 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 private greet = + """ +module M + +let greet name = + printfn "Hello %s" name +""" + +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 + +let f () = g -1 +""" + + let expected = + """ +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`` () = + 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) +""" + +[] +[] +[")>] +[ x")>] +[] +let ``Caret in the header of a parenthesized lambda extracts the lambda`` (marker: string) = + let expected = + """ +module M + +let f xs = + let extracted = fun x -> x + 1 + xs |> List.map extracted +""" + + Assert.Equal([ extractToLetBinding ], titlesAt mapIncrement (caretAt mapIncrement marker)) + Assert.Equal(expected, extractedAt extractToLetBinding mapIncrement (caretAt mapIncrement marker)) + +[] +[] +[] +[] +let ``No action without a selection outside a parenthesized lambda header`` (marker: string) = + Assert.Empty(titlesAt mapIncrement (caretAt mapIncrement marker)) + +[] +let ``No action without a selection on a lambda without parentheses`` () = + let code = + """ +module M + +let f = fun x -> x + 1 +""" + + 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 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 + | _ -> 0 +""", + "v > 0")>] +[] +[] +[] +[] +[ 0 +""", + "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