diff --git a/docs/release-notes/.VisualStudio/18.vNext.md b/docs/release-notes/.VisualStudio/18.vNext.md index 2357fbcdff4..a954a908f44 100644 --- a/docs/release-notes/.VisualStudio/18.vNext.md +++ b/docs/release-notes/.VisualStudio/18.vNext.md @@ -2,7 +2,9 @@ * Suggest active-pattern cases from unopened modules and offer valid Add Open and qualification edits. ([PR #20719](https://github.com/dotnet/fsharp/pull/20719)) * Add maintainer-requested F# Apex Visual Studio integration tests on DartLab-1ES CloudTest, using PR pipeline definitions pinned to an explicitly requested commit SHA. ([PR #20625](https://github.com/dotnet/fsharp/pull/20625)) +* **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)) * Added a **Tools > Options > F# Tools > Compiler** option, **"Use the .NET SDK F# compiler for builds"**, on by default. Visual Studio uses the .NET SDK F# compiler when the project supplies SDK paths, matching `dotnet build`. Otherwise, it uses the bundled .NET Framework compiler. Turn the option off to use the bundled compiler for all projects. Changes are saved on Apply or OK; Cancel discards unsaved changes. Command-line builds are unaffected. ([Issue #20484](https://github.com/dotnet/fsharp/issues/20484), [PR #20485](https://github.com/dotnet/fsharp/pull/20485)) * F# types, modules, members and values now appear in the GitHub Copilot Chat `#` mention picker, and attach their declaration source as context. ([PR #20409](https://github.com/dotnet/fsharp/pull/20409)) diff --git a/vsintegration/src/FSharp.Editor/FSharp.Editor.fsproj b/vsintegration/src/FSharp.Editor/FSharp.Editor.fsproj index f5d6a8e0b04..4f04b2bcd08 100644 --- a/vsintegration/src/FSharp.Editor/FSharp.Editor.fsproj +++ b/vsintegration/src/FSharp.Editor/FSharp.Editor.fsproj @@ -105,10 +105,13 @@ + + + diff --git a/vsintegration/src/FSharp.Editor/FSharp.Editor.resx b/vsintegration/src/FSharp.Editor/FSharp.Editor.resx index 8d1a5854269..b3f9c235ef8 100644 --- a/vsintegration/src/FSharp.Editor/FSharp.Editor.resx +++ b/vsintegration/src/FSharp.Editor/FSharp.Editor.resx @@ -373,4 +373,19 @@ Use live (unsaved) buffers for analysis {2} Tooltip of an F# declaration in the Copilot Chat "#" picker, as Copilot shows a C# one. {0} is the kind of declaration (Class, Method, Field...), {1} the file name, {2} the declaration's name. + + 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..2d158442235 --- /dev/null +++ b/vsintegration/src/FSharp.Editor/Refactor/ExtractFunction.fs @@ -0,0 +1,437 @@ +// 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.CodeRefactorings +open Microsoft.CodeAnalysis.Text + +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 + IsMutable: bool + Uses: range list + } + + [] + type Captures = + { + Parameters: Parameter list + UsesThis: bool + UsesStructThis: bool + UsesBase: bool + TypeParameterDeclarations: range list + MethodNameEnds: HashSet + } + + let private tryEnclosingDeclaration (path: SyntaxVisitorPath) = + path + |> Seq.tryPickV (function + | SyntaxNode.SynMemberDefn(SynMemberDefn.Member(range = m) | SynMemberDefn.LetBindings(range = m)) + | SyntaxNode.SynModule(SynModuleDecl.Let(range = m) | SynModuleDecl.Expr(range = m)) -> ValueSome m + | _ -> ValueNone) + + let tryTargetInDeclaration (sourceText: SourceText) (parseTree: ParsedInput) (selection: TextSpan) = + trySelectedTarget sourceText parseTree selection + |> ValueOption.bind (fun target -> + tryEnclosingDeclaration target.Path + |> ValueOption.map (fun declaration -> struct (target, declaration))) + + let private tryEntityOf (fullType: FSharpType) = + let stripped = fullType.StripAbbreviations() + + if stripped.HasTypeDefinition then + ValueSome stripped.TypeDefinition + else + ValueNone + + let private isByRefLike (fullType: FSharpType) = + tryEntityOf fullType + |> ValueOption.exists (fun entity -> + entity.IsByRef + || entity.Attributes + |> Seq.exists (fun attribute -> + String.Equals(attribute.AttributeType.CompiledName, "IsByRefLikeAttribute", StringComparison.Ordinal))) + + let private isStruct (fullType: FSharpType) = + tryEntityOf fullType + |> ValueOption.exists (fun entity -> entity.IsByRef || entity.IsValueType) + + let private (|CapturedLocal|_|) (selection: range) (scope: range) (symbol: FSharpSymbol) = + match symbol with + | :? FSharpMemberOrFunctionOrValue as value when not value.IsModuleValueOrMember -> + match symbol.DeclarationLocation with + | Some declaration when + Range.rangeContainsRange scope declaration + && not (Range.rangeContainsRange selection declaration) + -> + Some(value, declaration) + | _ -> None + | _ -> None + + let private (|TypeParameterDeclaredOutside|_|) (selection: range) (symbol: FSharpSymbol) = + match symbol with + | :? FSharpGenericParameter -> + symbol.DeclarationLocation + |> Option.filter (fun declaration -> not (Range.rangeContainsRange selection declaration)) + | _ -> None + + let private tryAddParameter + (parameters: Dictionary) + (value: FSharpMemberOrFunctionOrValue) + (symbolUse: FSharpSymbolUse) + (declaration: range) + = + match parameters.TryGetValue declaration, value.FullTypeSafe with + | (true, parameter), _ -> + parameters[declaration] <- + { parameter with + Uses = symbolUse.Range :: parameter.Uses + } + + true + | (false, _), Some fullType when not (isByRefLike fullType) && value.GenericParameters.Count = 0 -> + parameters[declaration] <- + { + Name = value.DisplayName + Type = fullType.FormatWithConstraints symbolUse.DisplayContext + IsMutable = value.IsMutable + Uses = [ symbolUse.Range ] + } + + true + | _ -> false + + let private firstUse (parameter: Parameter) = + parameter.Uses + |> List.map (fun m -> struct (m.StartLine, m.StartColumn)) + |> List.min + + let tryCaptures (checkResults: FSharpCheckFileResults) (selection: range) (scope: range) (cancellationToken: CancellationToken) = + let parameters = Dictionary(Range.comparer) + let typeParameterDeclarations = ResizeArray() + let methodNameEnds = HashSet() + let mutable usesThis = false + let mutable usesStructThis = false + let mutable usesBase = false + let mutable capturable = true + + let usesInSelection = + checkResults.GetAllUsesOfAllSymbolsInFile cancellationToken + |> Seq.filter (fun symbolUse -> + not symbolUse.IsFromDefinition + && Range.rangeContainsRange selection symbolUse.Range) + + for symbolUse in usesInSelection do + match symbolUse.Symbol with + | :? FSharpMemberOrFunctionOrValue as value when value.IsMemberThisValue || value.IsConstructorThisValue -> + usesThis <- true + usesStructThis <- usesStructThis || (value.FullTypeSafe |> Option.exists isStruct) + | :? FSharpMemberOrFunctionOrValue as value when value.IsBaseValue -> usesBase <- true + | CapturedLocal selection scope (value, declaration) -> + capturable <- capturable && tryAddParameter parameters value symbolUse declaration + | :? FSharpMemberOrFunctionOrValue as value when value.IsMember -> methodNameEnds.Add symbolUse.Range.End |> ignore + | TypeParameterDeclaredOutside selection declaration -> typeParameterDeclarations.Add declaration + | _ -> () + + if capturable then + ValueSome + { + Parameters = parameters.Values |> Seq.sortBy firstUse |> Seq.toList + UsesThis = usesThis + UsesStructThis = usesStructThis + UsesBase = usesBase + TypeParameterDeclarations = Seq.toList typeParameterDeclarations + MethodNameEnds = methodNameEnds + } + else + ValueNone + + let readsMutableInDelayedCode (expr: SynExpr) (parameters: Parameter list) = + let delayed = + (ResizeArray(), [ SyntaxNode.SynExpr expr ]) + ||> SyntaxNodes.fold (fun delayed _ node -> + match node with + | SyntaxNode.SynExpr(SynExpr.Lambda(range = m) | SynExpr.DotLambda(range = m) | SynExpr.MatchLambda(range = m) | SynExpr.Lazy( + range = m) | SynExpr.ObjExpr(range = m) | SynExpr.ComputationExpr(range = m)) -> delayed.Add m + | SyntaxNode.SynBinding(SynBinding(headPat = SynPat.LongIdent(argPats = SynArgPats.Pats(_ :: _))) as binding) -> + delayed.Add binding.RangeOfBindingWithRhs + | _ -> () + + delayed) + + parameters + |> List.exists (fun parameter -> + parameter.IsMutable + && parameter.Uses + |> List.exists (fun m -> delayed |> Seq.exists (fun code -> Range.rangeContainsRange code m))) + + let assignsCapturedValue (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)) + + let rec private identifierArguments (argument: SynExpr) = + match argument with + | SynExpr.Ident ident -> [ ident ] + | SynExpr.Paren(expr = inner) -> identifierArguments inner + | SynExpr.Tuple(exprs = arguments) -> arguments |> List.collect identifierArguments + | _ -> [] + + let positionsNeedingType (expr: SynExpr) (methodNameEnds: HashSet) = + (HashSet(), [ SyntaxNode.SynExpr expr ]) + ||> SyntaxNodes.fold (fun positions _ node -> + match node with + | SyntaxNode.SynExpr(SynExpr.App(funcExpr = method; argExpr = argument)) when methodNameEnds.Contains method.Range.End -> + positions.UnionWith(identifierArguments argument |> Seq.map _.idRange.Start) + | 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 curried (annotate: Parameter -> bool) (parameter: Parameter) = + if annotate parameter then + $"({parameter.Name}: {parameter.Type})" + else + parameter.Name + + let private tupled (annotate: Parameter -> bool) (parameter: Parameter) = + if annotate parameter then + $"{parameter.Name}: {parameter.Type}" + else + parameter.Name + + let private never (_: Parameter) = false + + let private curriedParameters (annotate: Parameter -> bool) (parameters: Parameter list) = + if parameters.IsEmpty then + "()" + else + parameters |> List.map (curried annotate) |> String.concat " " + + let private tupledParameters (annotate: Parameter -> bool) (parameters: Parameter list) = + let declarations = parameters |> List.map (tupled annotate) |> String.concat ", " + $"({declarations})" + + let private callNeedsParentheses (target: ExtractionTarget) = + match target.Expr, target.Path with + | SynExpr.Paren _, _ when target.Replaced = target.Extracted -> false + | _, SyntaxNode.SynExpr(SynExpr.App _ | SynExpr.DotGet _ | SynExpr.DotIndexedGet _ | SynExpr.TypeApp _) :: _ -> true + | expr, + SyntaxNode.SynExpr(SynExpr.DotSet(targetExpr = receiver) | SynExpr.DotIndexedSet(objectExpr = receiver) | SynExpr.DotNamedIndexedPropertySet( + targetExpr = receiver)) :: _ -> obj.ReferenceEquals(expr, receiver) + | _ -> false + + let private isInRecursiveModuleLet (path: SyntaxVisitorPath) = + path + |> List.exists (function + | SyntaxNode.SynModule(SynModuleDecl.Let(isRecursive = true)) -> true + | _ -> false) + + let private isPrivate (binding: SynBinding) = + match binding with + | SynBinding(accessibility = Some(SynAccess.Private _)) + | SynBinding(headPat = SynPat.LongIdent(accessibility = Some(SynAccess.Private _))) + | SynBinding(headPat = SynPat.Named(accessibility = Some(SynAccess.Private _))) -> true + | _ -> false + + let rec private isInExposedInline (path: SyntaxVisitorPath) = + match path with + | [] -> false + | SyntaxNode.SynBinding(SynBinding(isInline = isInline) as binding) :: (SyntaxNode.SynModule _ | SyntaxNode.SynMemberDefn _) :: _ -> + isInline && not (isPrivate binding) + | _ :: rest -> isInExposedInline rest + + let private bindsName (name: string) (binding: SynBinding) = + (false, [ SyntaxNode.SynBinding binding ]) + ||> SyntaxNodes.fold (fun found _ node -> + match node with + | SyntaxNode.SynPat(SynPat.Named(ident = SynIdent(ident, _))) + | SyntaxNode.SynPat(SynPat.LongIdent(longDotId = SynLongIdent(id = [ ident ]))) -> + found || String.Equals(ident.idText, name, StringComparison.Ordinal) + | _ -> found) + + let private 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 + + let private tryMemberCallee (binding: SynBinding) (info: SynComponentInfo) = + match binding, info with + | SynBinding(headPat = SynPat.LongIdent(longDotId = SynLongIdent(id = [ self; _ ]))), _ when + not (self.idText.StartsWith("_", StringComparison.Ordinal)) + && not (bindsName self.idText binding) + -> + let self = PrettyNaming.NormalizeIdentifierBackticks self.idText + + ValueSome + struct {| + Declaration = $"member private {self}." + Receiver = self + |} + | SynBinding(headPat = SynPat.LongIdent(longDotId = SynLongIdent(id = [ _ ]))), SynComponentInfo(typeParams = None) -> + ValueSome + struct {| + Declaration = "static member private " + Receiver = PrettyNaming.NormalizeIdentifierBackticks (List.last info.LongIdent).idText + |} + | _ -> ValueNone + + let private tryDeclareAfterMember (extraction: Extraction) (memberRange: range) (header: string) (call: string) = + let sourceText = extraction.SourceText + let lastLine = sourceText.Lines[Line.toZ memberRange.EndLine] + + let indent = + leadingSpaces sourceText sourceText.Lines[Line.toZ memberRange.StartLine] + + tryDeclaration extraction header (indent + extraction.IndentSize) + |> ValueOption.map (fun declaration -> + let insertion = + if lastLine.EndIncludingLineBreak > lastLine.End then + TextChange(TextSpan(lastLine.EndIncludingLineBreak, 0), $"{padding indent}{declaration}{extraction.LineBreak}") + else + TextChange(TextSpan(lastLine.End, 0), $"{extraction.LineBreak}{padding indent}{declaration}") + + [ TextChange(extraction.Target.Replaced, call); insertion ]) + + let private closureCannotCapture (captures: Captures) = + captures.UsesBase || captures.UsesStructThis + + let private curriedFunction (extraction: Extraction) (captures: Captures) (annotate: Parameter -> bool) = + let name = uniqueName "extractedFunction" extraction.Names + let application = $"{name} {curriedParameters never captures.Parameters}" + + struct {| + Header = $"{name} {curriedParameters annotate captures.Parameters}" + Call = + if callNeedsParentheses extraction.Target then + $"({application})" + else + application + |} + + let private localFunctionChanges (extraction: Extraction) (captures: Captures) (annotate: Parameter -> bool) = + match anchorsOf extraction.Target.Expr extraction.Target.Path with + | anchor :: _ when not (closureCannotCapture captures) -> + let newFunction = curriedFunction extraction captures annotate + tryDeclareInFront extraction anchor $"let {newFunction.Header}" newFunction.Call + | _ -> ValueNone + + let private moduleFunctionChanges (extraction: Extraction) (captures: Captures) (annotate: Parameter -> bool) = + let path = extraction.Target.Path + + if + captures.UsesThis + || captures.UsesBase + || isInExposedInline path + || not captures.TypeParameterDeclarations.IsEmpty + || isInRecursiveModuleLet path + then + ValueNone + else + let newFunction = curriedFunction extraction captures annotate + tryDeclareInFrontOfModuleLet extraction [] $"let private {newFunction.Header}" newFunction.Call + + let private privateMemberChanges (extraction: Extraction) (captures: Captures) (annotate: Parameter -> bool) = + let declaresNoneOf (memberRange: range) = + captures.TypeParameterDeclarations + |> List.forall (fun declaration -> not (Range.rangeContainsRange memberRange declaration)) + + match tryEnclosingMember extraction.Target.Path with + | ValueSome(struct (binding, memberRange, info)) when not (isInExposedInline extraction.Target.Path) && declaresNoneOf memberRange -> + tryMemberCallee binding info + |> ValueOption.bind (fun callee -> + let name = uniqueName "ExtractedMethod" extraction.Names + + let header = + $"{callee.Declaration}{name}{tupledParameters annotate captures.Parameters}" + + let call = $"{callee.Receiver}.{name}{tupledParameters never captures.Parameters}" + tryDeclareAfterMember extraction memberRange header call) + | _ -> ValueNone + + let private annotationOf (setting: ParameterAnnotationSetting) (needingType: HashSet) (parameter: Parameter) = + match setting with + | ParameterAnnotationSetting.Always -> true + | ParameterAnnotationSetting.WhenNeeded -> parameter.Uses |> List.exists (fun m -> needingType.Contains m.Start) + | ParameterAnnotationSetting.Never -> false + + let offer (extraction: Extraction) (declaration: range) (setting: ParameterAnnotationSetting) = + let target = extraction.Target + + let selection = + RoslynHelpers.TextSpanToFSharpRange(extraction.Context.Document.FilePath, target.Extracted, extraction.SourceText) + + match tryCaptures extraction.CheckResults selection declaration extraction.Context.CancellationToken with + | ValueSome captures when + not (assignsCapturedValue target.Expr captures.Parameters) + && not (readsMutableInDelayedCode target.Expr captures.Parameters) + -> + let annotate = + annotationOf setting (positionsNeedingType target.Expr captures.MethodNameEnds) + + let register = registerRefactoring extraction + register (SR.ExtractToLocalFunction()) "local" (localFunctionChanges extraction captures annotate) + register (SR.ExtractToModuleFunction()) "module" (moduleFunctionChanges extraction captures annotate) + register (SR.ExtractToPrivateMember()) "member" (privateMemberChanges extraction captures annotate) + | _ -> () + +[] +type internal FSharpExtractFunctionRefactoring [] () = + inherit CodeRefactoringProvider() + + override _.ComputeRefactoringsAsync context = + cancellableTask { + let document = context.Document + + if not document.IsFSharpSignatureFile then + let! sourceText = document.GetTextAsync context.CancellationToken + let! parseResults = document.GetFSharpParseResultsAsync(nameof FSharpExtractFunctionRefactoring) + let parseTree = parseResults.ParseTree + + match FunctionExtraction.tryTargetInDeclaration sourceText parseTree context.Span with + | ValueNone -> () + | ValueSome(struct (target, declaration)) -> + let! extraction = prepareExtraction context (nameof FSharpExtractFunctionRefactoring) sourceText parseTree target + FunctionExtraction.offer extraction declaration document.Project.FSharpExtractFunctionParameterAnnotations + } + |> 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..bab5d714b4f --- /dev/null +++ b/vsintegration/src/FSharp.Editor/Refactor/ExtractLetBinding.fs @@ -0,0 +1,73 @@ +// 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.CodeRefactorings + +open FSharp.Compiler.Syntax + +open RefactoringHelpers +open CancellableTasks + +[] +type internal FSharpExtractLetBindingRefactoring [] () = + inherit CodeRefactoringProvider() + + static let letBindingChanges (extraction: Extraction) = + match anchorsOf extraction.Target.Expr extraction.Target.Path with + | [] -> ValueNone + | anchor :: _ -> + let name = uniqueName "extracted" extraction.Names + + let header = + match tryFunctionTypeText extraction.CheckResults extraction.Target with + | ValueSome functionType -> $"let {name}: {functionType}" + | ValueNone -> $"let {name}" + + tryDeclareInFront extraction anchor header name + + static let literalChanges (extraction: Extraction) = + let constant = + match extraction.Target.Expr with + | SynExpr.Paren(expr = inner) -> inner + | expr -> expr + + if isLiteralConstant constant then + let name = uniqueName "ExtractedConstant" extraction.Names + + let attribute = + literalAttributeAt extraction.CheckResults extraction.Target.Expr.Range.Start + + tryDeclareInFrontOfModuleLet extraction [ $"[<{attribute}>]" ] $"let {name}" name + else + ValueNone + + static let offer (isSelected: bool) (extraction: Extraction) = + if isSelected then + registerRefactoring extraction (SR.ExtractToLetBinding()) "let" (letBindingChanges extraction) + + registerRefactoring extraction (SR.ExtractToLiteral()) "literal" (literalChanges extraction) + + override _.ComputeRefactoringsAsync context = + cancellableTask { + let document = context.Document + + if not document.IsFSharpSignatureFile then + let! sourceText = document.GetTextAsync context.CancellationToken + let! parseResults = document.GetFSharpParseResultsAsync(nameof FSharpExtractLetBindingRefactoring) + let parseTree = parseResults.ParseTree + let selected = trySelectedTarget sourceText parseTree context.Span + + let target = + selected + |> ValueOption.orElseWith (fun () -> tryConstantAtCaret sourceText parseTree context.Span) + + match target with + | ValueNone -> () + | ValueSome target -> + let! extraction = prepareExtraction context (nameof FSharpExtractLetBindingRefactoring) sourceText parseTree target + offer selected.IsSome extraction + } + |> 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..8158df27329 --- /dev/null +++ b/vsintegration/src/FSharp.Editor/Refactor/RefactoringHelpers.fs @@ -0,0 +1,731 @@ +// 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.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.SyntaxTrivia +open FSharp.Compiler.Text +open FSharp.Compiler.Tokenization + +open CancellableTasks + +[] +type Anchor = + | Statement of start: pos + | Clause of keywordEnd: pos * body: range * offsideColumn: int + +[] +type ExtractionTarget = + { + Expr: SynExpr + Path: SyntaxVisitorPath + Extracted: TextSpan + Replaced: TextSpan + } + +[] +type Extraction = + { + Context: CodeRefactoringContext + Provider: string + SourceText: SourceText + Target: ExtractionTarget + CheckResults: FSharpCheckFileResults + IndentSize: int + LiteralLines: HashSet + LineBreak: string + Names: HashSet + } + +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 padding (width: int) = String(' ', width) + +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.tryFindV (fun line -> line.EndIncludingLineBreak > line.End) + |> ValueOption.map (fun line -> sourceText.ToString(TextSpan.FromBounds(line.End, line.EndIncludingLineBreak))) + |> ValueOption.defaultValue Environment.NewLine + +let private isLineLeading (sourceText: SourceText) (position: pos) = + let line = sourceText.Lines[Line.toZ position.Line] + leadingSpaces sourceText line = position.Column + +let private onlyClosersFollow (sourceText: SourceText) (position: pos) = + let line = sourceText.Lines[Line.toZ position.Line] + + let isCloser c = + Char.IsWhiteSpace c || c = ')' || c = ']' || c = '}' || c = '|' + + let mutable offset = line.Start + position.Column + + while offset < line.End && isCloser (sourceText[offset]) do + offset <- offset + 1 + + offset = line.End + || (offset + 1 < line.End + && sourceText[offset] = '/' + && sourceText[offset + 1] = '/') + +let private startsInsideString (state: FSharpTokenizerLexState) = + match FSharpLineTokenizer.ColorStateOfLexState state with + | FSharpTokenizerColorState.String + | FSharpTokenizerColorState.VerbatimString + | FSharpTokenizerColorState.TripleQuoteString -> true + // The public enum does not name the interpolated string states. + | color -> not (Enum.IsDefined(typeof, color)) + +let private isSkipped (state: FSharpTokenizerLexState) = + match FSharpLineTokenizer.ColorStateOfLexState state with + | FSharpTokenizerColorState.IfDefSkip + | FSharpTokenizerColorState.EndLineThenSkip -> true + | _ -> false + +let private startsWithDirective (code: ReadOnlySpan) (keyword: string) = + code.StartsWith(keyword.AsSpan(), StringComparison.Ordinal) + && (code.Length = keyword.Length + || Char.IsWhiteSpace code[keyword.Length] + || code[keyword.Length] = '/') + +let private isSkippedDirective (line: string) = + let code = line.AsSpan().TrimStart() + + startsWithDirective code "#if" + || startsWithDirective code "#elif" + || startsWithDirective code "#else" + || startsWithDirective code "#endif" + +let private scanLine (tokenizer: FSharpSourceTokenizer) (line: string) (state: FSharpTokenizerLexState) = + let lineTokenizer = tokenizer.CreateLineTokenizer line + + let rec loop state = + match lineTokenizer.ScanToken state with + | Some _, next -> loop next + | None, next -> next + + loop state + +let private linesInsideInactiveLiterals (sourceText: SourceText) (defines: string list) (langVersion: string) (lines: HashSet) = + let fileTokenizer = FSharpSourceTokenizer(defines, None, Some langVersion) + let branchTokenizer = FSharpSourceTokenizer([], None, Some langVersion) + let mutable fileState = FSharpTokenizerLexState.Initial + let mutable branchState = FSharpTokenizerLexState.Initial + + for line in sourceText.Lines do + let text = line.ToString() + let inInactiveBranch = isSkipped fileState && not (isSkippedDirective text) + + if inInactiveBranch && startsInsideString branchState then + lines.Add line.LineNumber |> ignore + + branchState <- + if inInactiveBranch then + scanLine branchTokenizer text branchState + else + FSharpTokenizerLexState.Initial + + fileState <- scanLine fileTokenizer text fileState + +let private linesAfterFirst (m: range) = + seq { for line in m.StartLine + 1 .. m.EndLine -> Line.toZ line } + +let linesInsideLiterals (sourceText: SourceText) (parseTree: ParsedInput) (defines: string list) (langVersion: string) = + let lines = + (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)) -> lines.UnionWith(linesAfterFirst m) + | _ -> () + + lines) + + let directives = + match parseTree with + | ParsedInput.ImplFile file -> file.Trivia.ConditionalDirectives + | ParsedInput.SigFile file -> file.Trivia.ConditionalDirectives + + if not directives.IsEmpty then + linesInsideInactiveLiterals sourceText defines langVersion lines + + 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.SynExpr(SynExpr.LongIdentSet(longDotId = SynLongIdent(id = ident :: _))) + | SyntaxNode.SynExpr(SynExpr.NamedIndexedPropertySet(longDotId = SynLongIdent(id = ident :: _))) + | SyntaxNode.SynPat(SynPat.Named(ident = SynIdent(ident, _))) -> names.Add ident.idText |> ignore + | SyntaxNode.SynPat(SynPat.LongIdent(longDotId = SynLongIdent(id = idents))) -> names.UnionWith(idents |> Seq.map _.idText) + | _ -> () + + names) + +let uniqueName (baseName: string) (used: HashSet) = + Seq.initInfinite (fun i -> if i = 0 then baseName else $"{baseName}{i}") + |> Seq.find (used.Contains >> not) + +let private tryIndentedText (extraction: Extraction) (column: int) = + let sourceText = extraction.SourceText + let span = extraction.Target.Extracted + 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 extraction.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 rec 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.Paren(expr = inner) -> isExtractableShape inner (SyntaxNode.SynExpr expr :: path) + | 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 _ | SynExpr.Assert _) :: _, _ -> true + | SyntaxNode.SynExpr(SynExpr.While(whileExpr = condition) | SynExpr.WhileBang(whileExpr = condition)) :: _, SyntaxNode.SynExpr expr when + isSame condition expr + -> + true + | SyntaxNode.SynExpr(SynExpr.App(funcExpr = SynExpr.Ident operator; argExpr = operand)) :: _, SyntaxNode.SynExpr expr when + hasName "nameof" operator && isSame operand 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 + +let private hasOrphanedComputationConstruct (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 private targetOf (sourceText: SourceText) (expr: SynExpr) (path: SyntaxVisitorPath) = + let span = textSpanOf sourceText expr.Range + + let extracted = + match expr with + | SynExpr.Paren(expr = inner) -> textSpanOf sourceText inner.Range + | _ -> span + + let replaced = + match expr with + | SynExpr.Paren _ when isCall path -> extracted + | _ -> span + + { + Expr = expr + Path = path + Extracted = extracted + Replaced = replaced + } + +let private exprSpanning (start: pos) (finish: pos) (path: SyntaxVisitorPath) (node: SyntaxNode) = + match node with + | SyntaxNode.SynExpr expr when Position.posEq expr.Range.Start start && Position.posEq expr.Range.End finish -> Some(expr, path) + | _ -> None + +let private crossesDirective (directives: ConditionalDirectiveTrivia list) (start: pos) (finish: pos) = + directives + |> List.exists (fun directive -> + let line = directiveLine directive + line >= start.Line && line <= finish.Line) + +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 + + match (start, parseTree) ||> ParsedInput.tryPickLast (exprSpanning start finish) with + | Some(expr, path) when + not (crossesDirective file.Trivia.ConditionalDirectives start finish) + && isExtractableShape expr path + && not (isInExcludedContext expr path) + && not (hasOrphanedComputationConstruct expr) + -> + ValueSome(targetOf sourceText expr path) + | _ -> ValueNone + | _ -> ValueNone + +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.Single _ | SynConst.Double _ | SynConst.Decimal _ | SynConst.Char _ | SynConst.String _), + _) -> true + | _ -> false + +let private constantAt (position: pos) (path: SyntaxVisitorPath) (node: SyntaxNode) = + 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 + +let tryConstantAtCaret (sourceText: SourceText) (parseTree: ParsedInput) (selection: TextSpan) = + let position = positionOf sourceText selection.Start + + if selection.IsEmpty then + match (position, parseTree) ||> ParsedInput.tryPickLast (constantAt position) with + | Some(expr, path) -> ValueSome(targetOf sourceText expr path) + | None -> ValueNone + else + ValueNone + +let private parenthesizedLambdaHeaderAt (position: pos) (_: SyntaxVisitorPath) (node: SyntaxNode) = + 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 + +let private tryParenthesizedLambdaAtCaret (sourceText: SourceText) (parseTree: ParsedInput) (caret: int) = + let position = positionOf sourceText caret + + match + (position, parseTree) + ||> ParsedInput.tryPickLast (parenthesizedLambdaHeaderAt position) + with + | Some m -> tryExtractionTarget sourceText parseTree (textSpanOf sourceText m) + | None -> ValueNone + +let trySelectedTarget (sourceText: SourceText) (parseTree: ParsedInput) (selection: TextSpan) = + if selection.IsEmpty then + tryParenthesizedLambdaAtCaret sourceText parseTree selection.Start + else + tryExtractionTarget sourceText parseTree selection + +let private isFunctionBinding (binding: SynBinding) = + match binding with + | SynBinding(headPat = SynPat.LongIdent(argPats = SynArgPats.Pats(_ :: _))) -> true + | _ -> false + +let private anchorOf (child: SyntaxNode) (parent: SyntaxNode) (outer: SyntaxVisitorPath) (offsideColumn: int) = + 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.SynExpr(SynExpr.TryFinally(finallyExpr = 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 outer, trivia.EqualsRange with + | 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, offsideColumn)) + | _ -> ValueNone + | SyntaxNode.SynMatchClause(SynMatchClause(resultExpr = body; trivia = { ArrowRange = Some arrow })), SyntaxNode.SynExpr expr when + isSame body expr + -> + ValueSome(Anchor.Clause(arrow.End, expr.Range, offsideColumn)) + | SyntaxNode.SynExpr(SynExpr.IfThenElse(thenExpr = body; trivia = trivia)), SyntaxNode.SynExpr expr when isSame body expr -> + ValueSome(Anchor.Clause(trivia.ThenKeyword.End, expr.Range, offsideColumn)) + | SyntaxNode.SynExpr(SynExpr.IfThenElse(elseExpr = Some body; trivia = { ElseKeyword = Some elseKeyword })), SyntaxNode.SynExpr expr when + isSame body expr + -> + ValueSome(Anchor.Clause(elseKeyword.End, expr.Range, offsideColumn)) + | SyntaxNode.SynExpr(SynExpr.Lambda(parsedData = Some(_, body); trivia = { ArrowRange = Some arrow })), SyntaxNode.SynExpr expr when + isSame body expr + -> + ValueSome(Anchor.Clause(arrow.End, expr.Range, offsideColumn)) + | _ -> ValueNone + +let private (|ElifBranch|_|) (path: SyntaxVisitorPath) = + match path with + | SyntaxNode.SynExpr(SynExpr.IfThenElse _ as nested) :: (SyntaxNode.SynExpr(SynExpr.IfThenElse(elseExpr = Some elseExpr)) :: _ as enclosingIf) when + isSame elseExpr nested + -> + Some enclosingIf + | _ -> None + +let rec private offsideColumnOf (path: SyntaxVisitorPath) = + match path with + | [] -> 0 + | ElifBranch enclosingIf -> offsideColumnOf enclosingIf + | SyntaxNode.SynBinding(SynBinding(trivia = trivia)) :: _ -> trivia.LeadingKeyword.Range.StartColumn + | SyntaxNode.SynExpr(SynExpr.Match(range = m) | SynExpr.MatchBang(range = m) | SynExpr.TryWith(range = m) | SynExpr.TryFinally(range = m) | SynExpr.IfThenElse( + range = m) | SynExpr.For(range = m) | SynExpr.ForEach(range = m) | SynExpr.While(range = m)) :: _ -> m.StartColumn + | _ :: rest -> offsideColumnOf rest + +let anchorsOf (expr: SynExpr) (path: SyntaxVisitorPath) = + let rec loop (child: SyntaxNode) (path: SyntaxVisitorPath) = + match path with + | [] -> [] + | parent :: outer -> + match anchorOf child parent outer (offsideColumnOf path) with + | ValueSome anchor -> anchor :: loop parent outer + | ValueNone -> loop parent outer + + loop (SyntaxNode.SynExpr expr) path + +let rec private hasInferredTypeVariable (ty: FSharpType) = + if ty.IsGenericParameter then + ty.GenericParameter.IsCompilerGenerated + else + not ty.IsMeasureType + && ty.GenericArguments |> Seq.exists hasInferredTypeVariable + +let tryFunctionTypeText (checkResults: FSharpCheckFileResults) (target: ExtractionTarget) = + let parenthesizedFunction = + match target.Expr, target.Path with + | SynExpr.Paren(expr = SynExpr.Lambda _ | SynExpr.MatchLambda _; range = m), _ + | (SynExpr.Lambda _ | SynExpr.MatchLambda _), SyntaxNode.SynExpr(SynExpr.Paren(range = m)) :: _ -> ValueSome m + | _ -> ValueNone + + parenthesizedFunction + |> ValueOption.bind (fun m -> + match checkResults.TryGetCapturedType m, checkResults.TryGetCapturedDisplayContext m with + | Some ty, Some displayContext when ty.StripAbbreviations().IsFunctionType && not (hasInferredTypeVariable ty) -> + ValueSome(ty.Format displayContext) + | _ -> ValueNone) + +let literalAttributeAt (checkResults: FSharpCheckFileResults) (position: pos) = + let attribute = + checkResults.ProjectContext.GetReferencedAssemblies() + |> List.tryPick _.Contents.FindEntityByPath([ "Microsoft"; "FSharp"; "Core"; "LiteralAttribute" ]) + + match attribute with + | Some attribute when not (checkResults.IsRelativeNameResolvableFromSymbol(position, [], attribute)) -> + "global.Microsoft.FSharp.Core.Literal" + | _ -> "Literal" + +let private tryEnclosingModuleLet (path: SyntaxVisitorPath) = + path + |> List.tryPick (function + | SyntaxNode.SynModule(SynModuleDecl.Let(bindings = binding :: _; range = m)) -> Some struct (binding, m) + | _ -> None) + +let tryDeclaration (extraction: Extraction) (header: string) (bodyColumn: int) = + let sourceText = extraction.SourceText + let lines = sourceText.Lines + let extracted = extraction.Target.Extracted + + tryIndentedText extraction bodyColumn + |> ValueOption.map (fun body -> + if lines.GetLineFromPosition(extracted.Start).LineNumber = lines.GetLineFromPosition(extracted.End).LineNumber then + $"{header} = {body}" + else + $"{header} ={extraction.LineBreak}{padding bodyColumn}{body}") + +let private tryInFrontOfLine (extraction: Extraction) (start: pos) (header: string) (replacement: string) = + let line = extraction.SourceText.Lines[Line.toZ start.Line] + + tryDeclaration extraction header (start.Column + extraction.IndentSize) + |> ValueOption.map (fun declaration -> + [ + TextChange(TextSpan(line.Start, 0), $"{padding start.Column}{declaration}{extraction.LineBreak}") + TextChange(extraction.Target.Replaced, replacement) + ]) + +let private linesMovedWithBody (extraction: Extraction) (body: range) = + let lines = extraction.SourceText.Lines + let replaced = extraction.Target.Replaced + + let stays (line: TextLine) = + extraction.LiteralLines.Contains line.LineNumber + || String.IsNullOrWhiteSpace(line.ToString()) + || (line.Start >= replaced.Start && line.Start <= replaced.End) + + [ + for lineNumber in Line.toZ body.StartLine + 1 .. Line.toZ body.EndLine -> lines[lineNumber] + ] + |> List.filter (stays >> not) + +let private tryUnderKeyword + (extraction: Extraction) + (keywordEnd: pos) + (body: range) + (offsideColumn: int) + (header: string) + (replacement: string) + = + let sourceText = extraction.SourceText + let keywordLine = sourceText.Lines[Line.toZ keywordEnd.Line] + + let gap = + TextSpan.FromBounds(keywordLine.Start + keywordEnd.Column, (textSpanOf sourceText body).Start) + + let newIndent = + max (leadingSpaces sourceText keywordLine) offsideColumn + extraction.IndentSize + + let shift = newIndent - body.StartColumn + let movedLines = linesMovedWithBody extraction body + + let indentation = + [ + for line in movedLines do + if shift > 0 then + TextChange(TextSpan(line.Start, 0), padding shift) + elif shift < 0 then + TextChange(TextSpan(line.Start, -shift), "") + ] + + let canMove = + movedLines + |> List.forall (fun line -> shift >= 0 || leadingSpaces sourceText line >= -shift) + + if canMove && String.IsNullOrWhiteSpace(sourceText.ToString gap) then + tryDeclaration extraction header (newIndent + extraction.IndentSize) + |> ValueOption.map (fun declaration -> + [ + TextChange(gap, $"{extraction.LineBreak}{padding newIndent}{declaration}{extraction.LineBreak}{padding newIndent}") + yield! indentation + TextChange(extraction.Target.Replaced, replacement) + ] + |> List.sortBy _.Span.Start) + else + ValueNone + +let tryDeclareInFront (extraction: Extraction) (anchor: Anchor) (header: string) (replacement: string) = + let sourceText = extraction.SourceText + + match anchor with + | Anchor.Statement start when isLineLeading sourceText start -> tryInFrontOfLine extraction start header replacement + | Anchor.Clause(body = body) when isLineLeading sourceText body.Start -> tryInFrontOfLine extraction body.Start header replacement + | Anchor.Clause(keywordEnd, body, offsideColumn) when keywordEnd.Line = body.StartLine && onlyClosersFollow sourceText body.End -> + tryUnderKeyword extraction keywordEnd body offsideColumn header replacement + | _ -> ValueNone + +let tryDeclareInFrontOfModuleLet (extraction: Extraction) (attributes: string list) (header: string) (replacement: string) = + let sourceText = extraction.SourceText + + match tryEnclosingModuleLet extraction.Target.Path with + | Some(struct (SynBinding(xmlDoc = xmlDoc), declaration)) when isLineLeading sourceText declaration.Start -> + 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 + + tryDeclaration extraction header (indent + extraction.IndentSize) + |> ValueOption.map (fun declaration -> + let attributeLines = + attributes + |> List.map (fun attribute -> $"{padding indent}{attribute}{extraction.LineBreak}") + |> String.concat "" + + [ + TextChange( + TextSpan(line.Start, 0), + $"{attributeLines}{padding indent}{declaration}{extraction.LineBreak}{extraction.LineBreak}" + ) + TextChange(extraction.Target.Replaced, replacement) + ]) + | _ -> ValueNone + +let prepareExtraction + (context: CodeRefactoringContext) + (provider: string) + (sourceText: SourceText) + (parseTree: ParsedInput) + (target: ExtractionTarget) + = + cancellableTask { + let document = context.Document + let! options = document.GetOptionsAsync context.CancellationToken + let! defines, langVersion = document.GetFsharpParsingOptionsAsync provider + let! _, checkResults = document.GetFSharpParseAndCheckResultsAsync provider + + return + { + Context = context + Provider = provider + SourceText = sourceText + Target = target + CheckResults = checkResults + IndentSize = options.GetOption(FormattingOptions.IndentationSize, FSharpConstants.FSharpLanguageName) + LiteralLines = linesInsideLiterals sourceText parseTree defines langVersion + LineBreak = lineBreakOf sourceText + Names = usedNames parseTree + } + } + +let registerRefactoring (extraction: Extraction) (title: string) (kind: string) (changes: TextChange list voption) = + match changes with + | ValueSome changes -> + let changedDocument = + cancellableTask { + TelemetryReporter.ReportSingleEvent( + TelemetryEvents.RefactoringActivated, + [| "name", box extraction.Provider; "kind", box kind |] + ) + + return extraction.Context.Document.WithText(extraction.SourceText.WithChanges changes) + } + + extraction.Context.RegisterRefactoring(CodeAction.Create(title, changedDocument, title)) + | 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 5af8dfaab66..71f5b51cb2b 100644 --- a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.cs.xlf +++ b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.cs.xlf @@ -440,6 +440,31 @@ Zobrazit poznámky v Rychlých informacích Uzavřít výraz do závorek + + 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 + + \ No newline at end of file diff --git a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.de.xlf b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.de.xlf index e7cf1e34711..bfd4d5f6add 100644 --- a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.de.xlf +++ b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.de.xlf @@ -440,6 +440,31 @@ Hinweise in QuickInfo anzeigen Ausdruck in Klammern einschließen + + 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 + + \ No newline at end of file diff --git a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.es.xlf b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.es.xlf index a763e64aca8..900ee0ec482 100644 --- a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.es.xlf +++ b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.es.xlf @@ -440,6 +440,31 @@ Mostrar comentarios en Información rápida Encapsular la expresión entre paréntesis + + 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 + + \ No newline at end of file diff --git a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.fr.xlf b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.fr.xlf index 6fd805187e5..811ede1106a 100644 --- a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.fr.xlf +++ b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.fr.xlf @@ -440,6 +440,31 @@ Afficher les notes dans Info express Mettre l'expression entre parenthèses + + 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 + + \ No newline at end of file diff --git a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.it.xlf b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.it.xlf index 766483153f0..ef7ca7af878 100644 --- a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.it.xlf +++ b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.it.xlf @@ -440,6 +440,31 @@ Mostra i commenti in Informazioni rapide Racchiudere l'espressione tra parentesi + + 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 + + \ No newline at end of file diff --git a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.ja.xlf b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.ja.xlf index 30834f89e84..a45f15bbf09 100644 --- a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.ja.xlf +++ b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.ja.xlf @@ -440,6 +440,31 @@ F# 構文規則に準拠した改行を追加して、署名を指定された 式をかっこで囲む + + 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 + + \ No newline at end of file diff --git a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.ko.xlf b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.ko.xlf index 1ef22e3047d..f5a379a170a 100644 --- a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.ko.xlf +++ b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.ko.xlf @@ -440,6 +440,31 @@ F# 구문 규칙에 맞는 줄 바꿈을 추가하여 지정된 너비에 시그 식을 괄호로 래핑 + + 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 + + \ No newline at end of file diff --git a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.pl.xlf b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.pl.xlf index 1091913daa2..dbccfef8199 100644 --- a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.pl.xlf +++ b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.pl.xlf @@ -440,6 +440,31 @@ Pokaż uwagi w szybkich informacjach Ujmij wyrażenie w nawiasy + + 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 + + \ No newline at end of file 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 28e5ff703d2..da075248b75 100644 --- a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.pt-BR.xlf +++ b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.pt-BR.xlf @@ -440,6 +440,31 @@ Mostrar os comentários nas Informações Rápidas Coloque a expressão entre parênteses + + 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 + + \ No newline at end of file diff --git a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.ru.xlf b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.ru.xlf index 5787d9818f3..5984019f246 100644 --- a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.ru.xlf +++ b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.ru.xlf @@ -440,6 +440,31 @@ Show remarks in Quick Info Заключите выражение в круглые скобки. + + 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 + + \ No newline at end of file diff --git a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.tr.xlf b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.tr.xlf index 933e42779d4..ca968c2398a 100644 --- a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.tr.xlf +++ b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.tr.xlf @@ -440,6 +440,31 @@ Açıklamaları Hızlı Bilgide göster İfadeyi parantez içinde sarmalayı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 + + \ No newline at end of file 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 69381cc377d..4292a58b8a1 100644 --- a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.zh-Hans.xlf +++ b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.zh-Hans.xlf @@ -440,6 +440,31 @@ Show remarks in Quick Info 将表达式用括号括起来 + + 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 + + \ No newline at end of file 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 4bdafbf0121..48cd50dd07e 100644 --- a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.zh-Hant.xlf +++ b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.zh-Hant.xlf @@ -440,6 +440,31 @@ Show remarks in Quick Info 使用括弧包裝運算式 + + 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 + + \ No newline at end of file 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 d5d1a09c79a..4b914390fa1 100644 --- a/vsintegration/tests/FSharp.Editor.Tests/FSharp.Editor.Tests.fsproj +++ b/vsintegration/tests/FSharp.Editor.Tests/FSharp.Editor.Tests.fsproj @@ -79,6 +79,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..cc1ea365116 --- /dev/null +++ b/vsintegration/tests/FSharp.Editor.Tests/Refactors/ExtractFunctionTests.fs @@ -0,0 +1,424 @@ +module FSharp.Editor.Tests.Refactors.ExtractFunctionTests + +open Microsoft.VisualStudio.FSharp.Editor + +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 localAndModule = extractToLocalFunction + ";" + extractToModuleFunction + +[] +let private localAndMember = extractToLocalFunction + ";" + extractToPrivateMember + +[] +let private area = + """ +let area (w: int) (h: int) = + printfn "%d" (w * h + 1) +""" + +[] +let private areaWithLocalFunction = + """ +let area (w: int) (h: int) = + let extractedFunction (w: int) (h: int) = w * h + 1 + printfn "%d" (extractedFunction w h) +""" + +let private settingOf = + function + | "Always" -> ParameterAnnotationSetting.Always + | "WhenNeeded" -> ParameterAnnotationSetting.WhenNeeded + | _ -> ParameterAnnotationSetting.Never + +let private contextFor (setting: ParameterAnnotationSetting) (code: string) = + let options = + { CodeFixesOptions.Default with + ExtractFunctionParameterAnnotations = setting + } + + new TestContext(RoslynTestHelpers.CreateSolution(code, editorOptions = options)) + +let private extractedAtWith setting title code span = + use context = contextFor setting code + refactoredText (new FSharpExtractFunctionRefactoring()) context title code span + +let private titlesAt code span = + use context = contextFor ParameterAnnotationSetting.Always code + offeredTitles (new FSharpExtractFunctionRefactoring()) context code span + +[] +let ``Caret in the header of a parenthesized lambda extracts it as if it were selected`` () = + let code = + fileOf + """ +let f (xs: int list) (n: int) = + xs |> List.map (fun x -> x + n) +""" + + let lambda = selectionOf code "(fun x -> x + n)" + let caret = caretAt code "x ->" + let titles = titlesAt code lambda + + Assert.True(titles.Length > 0) + Assert.Equal(titles, titlesAt code caret) + + for title in titles.Split ';' do + Assert.Equal( + extractedAtWith ParameterAnnotationSetting.Always title code lambda, + extractedAtWith ParameterAnnotationSetting.Always title code caret + ) + + Assert.Empty(titlesAt code (caretAt code "x + n")) + +[] +[] +[] +[] +[] +[ List.sumBy (fun l -> l * this.Rate) +""", + "l * this.Rate", + """ +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 +""")>] +[] +[] +[] +[] +[] +let ``Selection is extracted`` (_scenario: string, title: string, code: string, selected: string, expected: string) = + let code = fileOf code + + Assert.Equal(fileOf expected, extractedAtWith ParameterAnnotationSetting.Always title code (selectionOf code selected)) + +[] +[] +[] +[] +[] +let ``Parameter annotations follow the option`` (setting: string, code: string, selected: string, expected: string) = + let code = fileOf code + + Assert.Equal(fileOf expected, extractedAtWith (settingOf setting) extractToLocalFunction code (selectionOf code selected)) + +[] +[] +[] +[] +[) = + x <- x + 1 +""", + "x + 1", + "")>] +[] +type Window = + val Length: int + +type Buffer = Window + +let size (buffer: Buffer) = + buffer.Length + 1 +""", + "buffer.Length + 1", + "")>] +[] +[ n) + n <- 1 + read () +""", + "(fun () -> n)", + "")>] +[ Seq.map _.Substring(n) + n <- 1 + tails +""", + "xs |> Seq.map _.Substring(n)", + "")>] +[] +[ () = typeof<'T>", "typeof<'T>", extractToLocalFunction)>] +[() = + member this.M<'U>() = (typeof<'T>, typeof<'U>) +""", + "typeof<'T>", + localAndMember)>] +[() = + member this.M<'U>() = (typeof<'T>, typeof<'U>) +""", + "typeof<'U>", + extractToLocalFunction)>] +[] +type S = + val X: int + new(x) = { X = x } + member this.M() = this.X + 1 +""", + "this.X + 1", + extractToPrivateMember)>] +[] +let ``Actions offered for a selection`` (_scenario: string, code: string, selected: string, titles: string) = + let code = fileOf code + Assert.Equal(titles, titlesAt code (selectionOf code selected)) + +[] +[] +[] +[] +[] +[] +[] +[] +let ``Private function or member is offered unless a more accessible inline one would call it`` + (declaration: string, title: string, offered: bool) + = + let code = $"module M\n\n{declaration}\n" + let titles = (titlesAt code (selectionOf code "x + 1")).Split ';' + + Assert.Equal(offered, Array.contains title titles) + Assert.Contains(extractToLocalFunction, titles) + +[] +let private assignedDependency = + """ +module Dependency + +type Box = { mutable Value: int } + +let extractedFunction = { Value = 0 } +let ExtractedMethod = { Value = 0 } +""" + +[] +[] +[] +let ``Name does not collide with an assignment target from another file`` + (_scenario: string, title: string, code: string, expected: string) + = + use context = TestContext.CreateWithCodeAndDependency code assignedDependency + + Assert.Equal(expected, refactoredText (new FSharpExtractFunctionRefactoring()) context title code (selectionOf code "({ Value = 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..b0764e21902 --- /dev/null +++ b/vsintegration/tests/FSharp.Editor.Tests/Refactors/ExtractLetBindingTests.fs @@ -0,0 +1,614 @@ +module FSharp.Editor.Tests.Refactors.ExtractLetBindingTests + +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 extractToLetBinding = "Extract to let binding" + +[] +let private extractToLiteral = "Extract to literal" + +[] +let private bothTitles = extractToLetBinding + ";" + extractToLiteral + +[] +let private area = + """ +let area w h = + printfn "%d" (w * h + 1) +""" + +[] +let private greet = + """ +let greet name = + printfn "Hello %s" name +""" + +[] +let private greetWithLiteral = + """ +[] +let ExtractedConstant = "Hello %s" + +let greet name = + printfn ExtractedConstant name +""" + +[] +let private moduleConstant = + """ +let f () = g 42 +""" + +[] +let private mapIncrement = + """ +let f xs = + xs |> List.map (fun x -> x + 1) +""" + +[] +let private mapIncrementExtracted = + """ +let f xs = + let extracted: int -> int = fun x -> x + 1 + xs |> List.map extracted +""" + +let private extractedAt title code span = + use context = TestContext.CreateWithCode code + refactoredText (new FSharpExtractLetBindingRefactoring()) context title code span + +let private titlesAt code span = + use context = TestContext.CreateWithCode code + offeredTitles (new FSharpExtractLetBindingRefactoring()) context code span + +let private assertTypeChecks (code: string) = + use context = TestContext.CreateWithCode code + + let _, checkResults = + (RoslynTestHelpers.GetLastDocument context.Solution).GetFSharpParseAndCheckResultsAsync "test" + |> CancellableTask.runSynchronouslyWithoutCancellation + + Assert.Empty(checkResults.Diagnostics) + +[] +[] +[] +[] +[ List.filter (fun i -> i > acc) + |> List.sum + total +""", + """items + |> List.filter (fun i -> i > acc) + |> List.sum""", + """ +let run items = + let mutable acc = 0 + let extracted = + items + |> List.filter (fun i -> i > acc) + |> List.sum + let total = + extracted + total +""")>] +[ List.filter (fun i -> i > acc) + |> List.sum + + total +""", + """ items + |> List.filter (fun i -> i > acc) + |> List.sum +""", + """ +let run items = + let mutable acc = 0 + + let extracted = + items + |> List.filter (fun i -> i > acc) + |> List.sum + let total = + extracted + + total +""")>] +[ v * 2 + offset + | None -> 0 +""", + "v * 2 + offset", + """ +let f x offset = + match x with + | Some v -> + let extracted = v * 2 + offset + extracted + | None -> 0 +""")>] +[] +[ List.length } +""", + "parse raw |> List.length", + """ +let load id = async { + let! raw = fetch id + let extracted = parse raw |> List.length + return extracted } +""")>] +[ List.map (fun x -> x + 1) +""", + "x + 1", + """ +let r = xs |> List.map (fun x -> + let extracted = x + 1 + extracted) +""")>] +[ 1 + 2 +""", + "1 + 2", + """ +let f x = match x with _ -> + let extracted = 1 + 2 + extracted +""")>] +[] +[] +[] +[] +[] +[] +[] +[] +[] +let ExtractedConstant = -1 + +let f () = g ExtractedConstant +""")>] +let ``Selection is extracted`` (_scenario: string, title: string, code: string, selected: string, expected: string) = + let code = fileOf code + Assert.Equal(fileOf expected, extractedAt title code (selectionOf code selected)) + +[] +[] +let ExtractedConstant = 1.5m + +let f () = id ExtractedConstant +""")>] +[] +let ExtractedConstant = 42 + +let x = ExtractedConstant +""")>] +[ int = (fun x -> x.Length) +""", + "(fun x -> x.Length)", + """ +let f: string -> int = + let extracted: string -> int = fun x -> x.Length + extracted +""")>] +[ List.map (fun x -> [ x ]) +""", + "(fun x -> [ x ])", + """ +let f (xs: 'T list) = + let extracted: 'T -> 'T list = fun x -> [ x ] + xs |> List.map extracted +""")>] +[ List.map (fun x -> [ x ]) +""", + "(fun x -> [ x ])", + """ +let f xs = + let extracted = fun x -> [ x ] + xs |> List.map extracted +""")>] +[ int = (function Some x -> x.Length | None -> 0) +""", + "(function Some x -> x.Length | None -> 0)", + """ +let f: string option -> int = + let extracted: string option -> int = function Some x -> x.Length | None -> 0 + extracted +""")>] +[ List.map (fun x -> x.Length) +""", + "fun x -> x.Length", + """ +let f (xs: string list) = + let extracted: string -> int = fun x -> x.Length + xs |> List.map (extracted) +""")>] +[) = xs.ConvertAll(fun x -> x.Length) +""", + "(fun x -> x.Length)", + """ +let f (xs: System.Collections.Generic.List) = + let extracted: string -> int = fun x -> x.Length + xs.ConvertAll(extracted) +""")>] +let ``Extracted code type checks`` (_scenario: string, title: string, code: string, selected: string, expected: string) = + let code = fileOf code + let result = extractedAt title code (selectionOf code selected) + + Assert.Equal(fileOf expected, result) + assertTypeChecks result + +[] +[] +[", mapIncrementExtracted)>] +[ x", mapIncrementExtracted)>] +[] +[] +[] +[] +let ``Caret extracts the lambda or constant around it`` (_scenario: string, title: string, code: string, marker: string, expected: string) = + let code = fileOf code + Assert.Equal(fileOf expected, extractedAt title code (caretAt code marker)) + +[] +[] +[] +[] +let ``Name does not collide with an assignment target from another file`` (_scenario: string, declaration: string, assignment: string) = + let dependency = + """ +module Dependency + +type Box = { mutable Value: int } + +DECLARATION +""" + .Replace("DECLARATION", declaration) + + let code = + """ +module M + +open Dependency + +let f () = + ASSIGNMENT({ Value = 1 }).Value +""" + .Replace("ASSIGNMENT", assignment) + + let expected = + """ +module M + +open Dependency + +let f () = + let extracted1 = { Value = 1 } + ASSIGNMENTextracted1.Value +""" + .Replace("ASSIGNMENT", assignment) + + use context = TestContext.CreateWithCodeAndDependency code dependency + + Assert.Equal( + expected, + refactoredText (new FSharpExtractLetBindingRefactoring()) context extractToLetBinding code (selectionOf code "({ Value = 1 })") + ) + +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[ 0 -> v + | _ -> 0 +""", + "v > 0", + "")>] +[] +[] +[] +[] +[ 0", "s.Length > 0", "")>] +[] +[] +type S = + val mutable X: int + new(x) = { X = x } + member s.Inc() = s.X <- s.X + 1 + +let f () = + let mutable s = S(1) + (s).Inc() + s.X +""", + "(s)", + "")>] +[] +[] +let ``Actions offered for a selection`` (_scenario: string, code: string, selected: string, titles: string) = + let code = fileOf code + Assert.Equal(titles, titlesAt code (selectionOf code selected)) + +[] +[] +[", extractToLetBinding)>] +[ x", extractToLetBinding)>] +[] +[] +[] +[] +[] +[ x + 1", "fun", "")>] +[] +[] +[] +[] +[] +[] +let ``Actions offered at a caret`` (_scenario: string, code: string, marker: string, titles: string) = + let code = fileOf code + Assert.Equal(titles, titlesAt code (caretAt code marker)) diff --git a/vsintegration/tests/FSharp.Editor.Tests/Refactors/RefactorTestFramework.fs b/vsintegration/tests/FSharp.Editor.Tests/Refactors/RefactorTestFramework.fs index 849da8c84ec..2a75ccb9085 100644 --- a/vsintegration/tests/FSharp.Editor.Tests/Refactors/RefactorTestFramework.fs +++ b/vsintegration/tests/FSharp.Editor.Tests/Refactors/RefactorTestFramework.fs @@ -1,13 +1,17 @@ -module FSharp.Editor.Tests.Refactors.RefactorTestFramework +module FSharp.Editor.Tests.Refactors.RefactorTestFramework open System -open System.Linq open System.Collections.Generic open Microsoft.CodeAnalysis open Microsoft.CodeAnalysis.Text +open Microsoft.VisualStudio.FSharp.Editor open Microsoft.VisualStudio.FSharp.Editor.CancellableTasks +open Xunit + +open FSharp.Test.ProjectGeneration + open FSharp.Editor.Tests.Helpers open Microsoft.CodeAnalysis.CodeRefactorings open Microsoft.CodeAnalysis.CodeActions @@ -31,11 +35,19 @@ type TestContext(Solution: Solution) = new TestContext(solution) static member CreateWithCodeAndDependency (code: string) (codeForPreviousFile: string) = - let mutable solution = RoslynTestHelpers.CreateSolution(codeForPreviousFile) - - let firstProject = solution.Projects.First() - solution <- solution.AddDocument(DocumentId.CreateNewId(firstProject.Id), "test2.fs", code, filePath = "C:\\test2.fs") - + let project = + { SyntheticProject.Create( + { sourceFile "First" [] with + Source = codeForPreviousFile + }, + { sourceFile "Second" [ "First" ] with + Source = code + } + ) with + AutoAddModules = false + } + + let solution, _ = RoslynTestHelpers.CreateSolution project new TestContext(solution) let tryRefactor (code: string) (cursorPosition) (context: TestContext) (refactorProvider: 'T :> CodeRefactoringProvider) = @@ -87,3 +99,61 @@ let tryGetRefactoringActions (code: string) (cursorPosition) (context: TestConte } |> CancellableTask.startWithoutCancellation |> fun task -> task.Result + +let tryGetRefactoringActionsForSpan (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 refactorSpan (code: string) (span: TextSpan) (title: string) (context: TestContext) (refactorProvider: CodeRefactoringProvider) = + let action = + tryGetRefactoringActionsForSpan 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 + +let selectionOf (code: string) (selected: string) = + TextSpan(code.IndexOf(selected, StringComparison.Ordinal), selected.Length) + +let caretAt (code: string) (marker: string) = + let offset = max 0 (marker.IndexOf '|') + TextSpan(code.IndexOf(marker.Replace("|", ""), StringComparison.Ordinal) + offset, 0) + +let fileOf (declarations: string) = + let text = declarations.Trim('\r', '\n') + + if + text.StartsWith("module ", StringComparison.Ordinal) + || text.StartsWith("namespace ", StringComparison.Ordinal) + then + declarations + else + let lineBreak = if declarations.IndexOf '\r' >= 0 then "\r\n" else "\n" + $"{lineBreak}module M{lineBreak}{lineBreak}{text}{lineBreak}" + +let refactoredText (provider: CodeRefactoringProvider) (context: TestContext) (title: string) (code: string) (span: TextSpan) = + let document = refactorSpan code span title context provider + + let parseResults = + document.GetFSharpParseResultsAsync "test" + |> CancellableTask.runSynchronouslyWithoutCancellation + + Assert.Empty(parseResults.Diagnostics) + (document.GetTextAsync() |> GetTaskResult).ToString() + +let offeredTitles (provider: CodeRefactoringProvider) (context: TestContext) (code: string) (span: TextSpan) = + tryGetRefactoringActionsForSpan code span context provider + |> Seq.map _.Title + |> String.concat ";"