diff --git a/vsintegration/src/FSharp.Editor/FSharp.Editor.fsproj b/vsintegration/src/FSharp.Editor/FSharp.Editor.fsproj index 60e86c9eb96..37bb02d43d2 100644 --- a/vsintegration/src/FSharp.Editor/FSharp.Editor.fsproj +++ b/vsintegration/src/FSharp.Editor/FSharp.Editor.fsproj @@ -99,6 +99,7 @@ + diff --git a/vsintegration/src/FSharp.Editor/FSharp.Editor.resx b/vsintegration/src/FSharp.Editor/FSharp.Editor.resx index 41b80965895..ef5f05b3d24 100644 --- a/vsintegration/src/FSharp.Editor/FSharp.Editor.resx +++ b/vsintegration/src/FSharp.Editor/FSharp.Editor.resx @@ -353,6 +353,9 @@ Use live (unsaved) buffers for analysis Convert C# 'using' to F# 'open' + + Add return type annotation + Remove unnecessary parentheses diff --git a/vsintegration/src/FSharp.Editor/Refactor/AddReturnType.fs b/vsintegration/src/FSharp.Editor/Refactor/AddReturnType.fs new file mode 100644 index 00000000000..b3c586b445e --- /dev/null +++ b/vsintegration/src/FSharp.Editor/Refactor/AddReturnType.fs @@ -0,0 +1,118 @@ +namespace Microsoft.VisualStudio.FSharp.Editor + +open System.Composition +open FSharp.Compiler.CodeAnalysis +open FSharp.Compiler.Symbols +open FSharp.Compiler.Text + +open Microsoft.CodeAnalysis.Text +open Microsoft.CodeAnalysis.CodeRefactorings +open Microsoft.CodeAnalysis.CodeActions +open CancellableTasks + +[] +type internal AddReturnType [] () = + inherit CodeRefactoringProvider() + + static member isValidMethodWithoutTypeAnnotation + (symbolUse: FSharpSymbolUse) + (parseFileResults: FSharpParseFileResults) + (funcOrValue: FSharpMemberOrFunctionOrValue) + = + let typeAnnotationRange = + parseFileResults.TryRangeOfReturnTypeHint(symbolUse.Range.Start, false) + + let res = + funcOrValue.CompiledName = funcOrValue.DisplayName + && funcOrValue.IsFunction + && not (parseFileResults.IsBindingALambdaAtPosition symbolUse.Range.Start) + && not (funcOrValue.ReturnParameter.Type.IsUnresolved) + && not (parseFileResults.IsTypeAnnotationGivenAtPosition symbolUse.Range.Start) + && not typeAnnotationRange.IsNone + + match (res, typeAnnotationRange) with + | (true, Some tr) -> Some(funcOrValue, tr) + | (_, _) -> None + + static member refactor + (context: CodeRefactoringContext) + (memberFunc: FSharpMemberOrFunctionOrValue, typeRange: Range, symbolUse: FSharpSymbolUse) + = + let title = SR.AddReturnTypeAnnotation() + + let getChangedText (sourceText: SourceText) = + let returnType = memberFunc.ReturnParameter.Type + + let inferredType = + let res = returnType.Format symbolUse.DisplayContext + + if returnType.HasTypeDefinition then + res + else + $"({res})".Replace(" ", "") + + let textSpan = RoslynHelpers.FSharpRangeToTextSpan(sourceText, typeRange) + let textChange = TextChange(textSpan, $": {inferredType} ") + sourceText.WithChanges(textChange) + + let codeActionFunc = + cancellableTask { + let! cancellationToken = CancellableTask.getCancellationToken () + let! sourceText = context.Document.GetTextAsync(cancellationToken) + let changedText = getChangedText sourceText + + let newDocument = context.Document.WithText(changedText) + return newDocument + } + + let codeAction = CodeAction.Create(title, codeActionFunc, title) + + do context.RegisterRefactoring(codeAction) + + static member ofFSharpMemberOrFunctionOrValue(symbol: FSharpSymbol) = + match symbol with + | :? FSharpMemberOrFunctionOrValue as v -> Some v + | _ -> None + + override _.ComputeRefactoringsAsync context = + cancellableTask { + let document = context.Document + let position = context.Span.Start + let! sourceText = document.GetTextAsync() + let textLine = sourceText.Lines.GetLineFromPosition position + let textLinePos = sourceText.Lines.GetLinePosition position + let fcsTextLineNumber = Line.fromZ textLinePos.Line + + let! lexerSymbol = + document.TryFindFSharpLexerSymbolAsync(position, SymbolLookupKind.Greedy, false, false, nameof (AddReturnType)) + + let! (parseFileResults, checkFileResults) = document.GetFSharpParseAndCheckResultsAsync(nameof (AddReturnType)) + + let symbolUseOpt = + lexerSymbol + |> Option.bind (fun lexer -> + checkFileResults.GetSymbolUseAtLocation( + fcsTextLineNumber, + lexer.Ident.idRange.EndColumn, + textLine.ToString(), + lexer.FullIsland + )) + + let memberFuncOpt = + symbolUseOpt + |> Option.bind (fun sym -> sym.Symbol |> AddReturnType.ofFSharpMemberOrFunctionOrValue) + + match (symbolUseOpt, memberFuncOpt) with + | (Some symbolUse, Some memberFunc) -> + let isValidMethod = + memberFunc + |> AddReturnType.isValidMethodWithoutTypeAnnotation symbolUse parseFileResults + + match isValidMethod with + | Some(memberFunc, typeRange) -> do AddReturnType.refactor context (memberFunc, typeRange, symbolUse) + | None -> () + | _ -> () + + return () + } + |> CancellableTask.startAsTask context.CancellationToken diff --git a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.cs.xlf b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.cs.xlf index 7f90ad6a126..b4ad96704a9 100644 --- a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.cs.xlf +++ b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.cs.xlf @@ -22,6 +22,11 @@ Přidejte klíčové slovo new. + + Add return type annotation + Add return type annotation + + Add type annotation Přidat anotaci typu diff --git a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.de.xlf b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.de.xlf index 5e5261ef993..7796b6a2fe9 100644 --- a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.de.xlf +++ b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.de.xlf @@ -22,6 +22,11 @@ Schlüsselwort "new" hinzufügen + + Add return type annotation + Add return type annotation + + Add type annotation Typanmerkung hinzufügen diff --git a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.es.xlf b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.es.xlf index f130a461216..c8c15573014 100644 --- a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.es.xlf +++ b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.es.xlf @@ -22,6 +22,11 @@ Agregar "nueva" palabra clave + + Add return type annotation + Add return type annotation + + Add type annotation Agregar una anotación de tipo diff --git a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.fr.xlf b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.fr.xlf index a3bc317a6af..c0076a1473e 100644 --- a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.fr.xlf +++ b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.fr.xlf @@ -22,6 +22,11 @@ Ajouter le mot clé 'new' + + Add return type annotation + Add return type annotation + + Add type annotation Ajouter une annotation de type diff --git a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.it.xlf b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.it.xlf index 45fbdd6637c..fc95d16b038 100644 --- a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.it.xlf +++ b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.it.xlf @@ -22,6 +22,11 @@ Aggiungi la parola chiave 'new' + + Add return type annotation + Add return type annotation + + Add type annotation Aggiungere l'annotazione di tipo diff --git a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.ja.xlf b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.ja.xlf index 09a35c432c9..74d787d914b 100644 --- a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.ja.xlf +++ b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.ja.xlf @@ -22,6 +22,11 @@ 'new' キーワードを追加する + + Add return type annotation + Add return type annotation + + Add type annotation 型の注釈の追加 diff --git a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.ko.xlf b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.ko.xlf index 32499ff0771..816da89d7d7 100644 --- a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.ko.xlf +++ b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.ko.xlf @@ -22,6 +22,11 @@ 'new' 키워드 추가 + + Add return type annotation + Add return type annotation + + Add type annotation 형식 주석 추가 diff --git a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.pl.xlf b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.pl.xlf index 589adf994b9..f0645a7a1b3 100644 --- a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.pl.xlf +++ b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.pl.xlf @@ -22,6 +22,11 @@ Dodaj słowo kluczowe „new” + + Add return type annotation + Add return type annotation + + Add type annotation Dodaj adnotację typu 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 8234f5190c7..7c05c537e39 100644 --- a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.pt-BR.xlf +++ b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.pt-BR.xlf @@ -22,6 +22,11 @@ Adicionar a palavra-chave 'new' + + Add return type annotation + Add return type annotation + + Add type annotation Adicionar uma anotação de tipo diff --git a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.ru.xlf b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.ru.xlf index d6447a61572..53d05e963f3 100644 --- a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.ru.xlf +++ b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.ru.xlf @@ -22,6 +22,11 @@ Добавить ключевое слово "new" + + Add return type annotation + Add return type annotation + + Add type annotation Добавить заметку типа diff --git a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.tr.xlf b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.tr.xlf index 56c3ca4e2d4..d8047f179af 100644 --- a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.tr.xlf +++ b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.tr.xlf @@ -22,6 +22,11 @@ 'new' anahtar sözcüğünü ekleme + + Add return type annotation + Add return type annotation + + Add type annotation Tür ek açıklaması ekle 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 80d8c8362cc..7f0aeefc86f 100644 --- a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.zh-Hans.xlf +++ b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.zh-Hans.xlf @@ -22,6 +22,11 @@ 添加“新”关键字 + + Add return type annotation + Add return type annotation + + Add type annotation 添加类型注释 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 31b2ea7cec2..8e5de2d10a5 100644 --- a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.zh-Hant.xlf +++ b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.zh-Hant.xlf @@ -22,6 +22,11 @@ 新增 'new' 關鍵字 + + Add return type annotation + Add return type annotation + + Add type annotation 新增型別註解 diff --git a/vsintegration/tests/FSharp.Editor.Tests/FSharp.Editor.Tests.fsproj b/vsintegration/tests/FSharp.Editor.Tests/FSharp.Editor.Tests.fsproj index 77ce57f27e0..a180accc43f 100644 --- a/vsintegration/tests/FSharp.Editor.Tests/FSharp.Editor.Tests.fsproj +++ b/vsintegration/tests/FSharp.Editor.Tests/FSharp.Editor.Tests.fsproj @@ -12,6 +12,7 @@ + @@ -71,6 +72,8 @@ + + diff --git a/vsintegration/tests/FSharp.Editor.Tests/Helpers/RoslynHelpers.fs b/vsintegration/tests/FSharp.Editor.Tests/Helpers/RoslynHelpers.fs index a0853ed83b9..ca113f72bf3 100644 --- a/vsintegration/tests/FSharp.Editor.Tests/Helpers/RoslynHelpers.fs +++ b/vsintegration/tests/FSharp.Editor.Tests/Helpers/RoslynHelpers.fs @@ -304,6 +304,11 @@ type RoslynTestHelpers private () = let document = project.Documents |> Seq.exactlyOne document + static member GetLastDocument(solution: Solution) = + let project = solution.Projects |> Seq.exactlyOne + let document = project.Documents |> Seq.last + document + static member CreateSolution(syntheticProject: SyntheticProject) = let checker = syntheticProject.SaveAndCheck() diff --git a/vsintegration/tests/FSharp.Editor.Tests/Refactors/AddReturnTypeTests.fs b/vsintegration/tests/FSharp.Editor.Tests/Refactors/AddReturnTypeTests.fs new file mode 100644 index 00000000000..5b81fb996da --- /dev/null +++ b/vsintegration/tests/FSharp.Editor.Tests/Refactors/AddReturnTypeTests.fs @@ -0,0 +1,452 @@ +module FSharp.Editor.Tests.Refactors.AddReturnTypeTests + +open Microsoft.VisualStudio.FSharp.Editor +open Xunit +open FSharp.Editor.Tests.Refactors.RefactorTestFramework +open FSharp.Test.ProjectGeneration +open FSharp.Editor.Tests.Helpers + +[] +[] +[] +[] +[] +let ``Refactor should not trigger`` (shouldNotTrigger: string) = + let symbolName = "sum" + + let code = + $""" +let sum a b {shouldNotTrigger}= a + b + """ + + use context = TestContext.CreateWithCode code + + let spanStart = code.IndexOf symbolName + + let actions = tryGetRefactoringActions code spanStart context (new AddReturnType()) + + do Assert.Empty(actions) + +[] +let ``Refactor should not trigger on values`` () = + let symbolName = "example2" + + let code = + """ +let example2 = 42 // value + """ + + use context = TestContext.CreateWithCode code + + let spanStart = code.IndexOf symbolName + + let actions = tryGetRefactoringActions code spanStart context (new AddReturnType()) + + do Assert.Empty(actions) + +[] +let ``Refactor should not trigger on member values`` () = + let symbolName = "SomeProp" + + let code = + """ +type Example3() = + member _.SomeProp = 42 // property + """ + + use context = TestContext.CreateWithCode code + + let spanStart = code.IndexOf symbolName + + let actions = tryGetRefactoringActions code spanStart context (new AddReturnType()) + + do Assert.Empty(actions) + +[] +let ``Correctly infer int as return type`` () = + let symbolName = "sum" + + let code = + """ +let sum a b = a + b + """ + + use context = TestContext.CreateWithCode code + + let spanStart = code.IndexOf symbolName + + let newDoc = tryRefactor code spanStart context (new AddReturnType()) + + let expectedCode = + $""" +let sum a b : int = a + b + """ + + let resultText = newDoc.GetTextAsync() |> GetTaskResult + Assert.Equal(expectedCode, resultText.ToString()) + +[] +let ``Correctly infer on next line arguments`` () = + let symbolName = "sum" + + let code = + """ +let sum + x y = + x + y + """ + + use context = TestContext.CreateWithCode code + + let spanStart = code.IndexOf symbolName + + let newDoc = tryRefactor code spanStart context (new AddReturnType()) + + let expectedCode = + $""" +let sum + x y : int = + x + y + """ + + let resultText = newDoc.GetTextAsync() |> GetTaskResult + Assert.Equal(expectedCode, resultText.ToString()) + +[] +let ``Should not throw exception when binding another method`` () = + let symbolName = "addThings" + + let code = + """ +let add (x:int) (y:int) = (float(x + y)) + 0.1 +let addThings = add + """ + + use context = TestContext.CreateWithCode code + + let spanStart = code.IndexOf symbolName + + let newDoc = tryRefactor code spanStart context (new AddReturnType()) + + let expectedCode = + $""" +let add (x:int) (y:int) = (float(x + y)) + 0.1 +let addThings : (int->int->float) = add + """ + + let resultText = newDoc.GetTextAsync() |> GetTaskResult + Assert.Equal(expectedCode, resultText.ToString()) + +[] +let ``Handle parantheses on the arguments`` () = + let symbolName = "sum" + + let code = + """ +let sum (a:float) (b:float) = a + b + """ + + use context = TestContext.CreateWithCode code + + let spanStart = code.IndexOf symbolName + + let newDoc = tryRefactor code spanStart context (new AddReturnType()) + + let expectedCode = + """ +let sum (a:float) (b:float) : float = a + b + """ + + let resultText = newDoc.GetTextAsync() |> GetTaskResult + Assert.Equal(expectedCode, resultText.ToString()) + +[] +let ``Infer on rec method`` () = + let symbolName = "fib" + + let code = + $""" +let rec fib n = + if n < 2 then 1 + else fib (n - 1) + fib (n - 2) + """ + + use context = TestContext.CreateWithCode code + + let spanStart = code.IndexOf symbolName + + let newDoc = tryRefactor code spanStart context (new AddReturnType()) + + let expectedCode = + $""" +let rec fib n : int = + if n < 2 then 1 + else fib (n - 1) + fib (n - 2) + """ + + let resultText = newDoc.GetTextAsync() |> GetTaskResult + Assert.Equal(expectedCode, resultText.ToString()) + +[] +let ``Infer with function parameter method`` () = + let symbolName = "apply1" + + let code = + $""" +let apply1 (transform: int -> int) y = transform y + """ + + use context = TestContext.CreateWithCode code + + let spanStart = code.IndexOf symbolName + + let newDoc = tryRefactor code spanStart context (new AddReturnType()) + + let expectedCode = + $""" +let apply1 (transform: int -> int) y : int = transform y + """ + + let resultText = newDoc.GetTextAsync() |> GetTaskResult + Assert.Equal(expectedCode, resultText.ToString()) + +[] +let ``Infer on member function`` () = + let symbolName = "SomeMethod" + + let code = + $""" +type SomeType(factor0: int) = + let factor = factor0 + member this.SomeMethod(a, b, c) = (a + b + c) * factor + """ + + use context = TestContext.CreateWithCode code + + let spanStart = code.IndexOf symbolName + + let newDoc = tryRefactor code spanStart context (new AddReturnType()) + + let expectedCode = + $""" +type SomeType(factor0: int) = + let factor = factor0 + member this.SomeMethod(a, b, c) : int = (a + b + c) * factor + """ + + let resultText = newDoc.GetTextAsync() |> GetTaskResult + Assert.Equal(expectedCode, resultText.ToString()) + +[] +let ``Binding another function doesnt crash`` () = + let symbolName = "getNow" + + let code = + $""" +let getNow() = + System.DateTime.Now + """ + + use context = TestContext.CreateWithCode code + + let spanStart = code.IndexOf symbolName + + let newDoc = tryRefactor code spanStart context (new AddReturnType()) + + let expectedCode = + $""" +let getNow() : System.DateTime = + System.DateTime.Now + """ + + let resultText = newDoc.GetTextAsync() |> GetTaskResult + Assert.Equal(expectedCode, resultText.ToString()) + +[] +let ``Handle already existing opens for DateTime`` () = + let symbolName = "getNow" + + let code = + $""" +open System + +let getNow() = + DateTime.Now + """ + + use context = TestContext.CreateWithCode code + + let spanStart = code.IndexOf symbolName + + let newDoc = tryRefactor code spanStart context (new AddReturnType()) + + let expectedCode = + $""" +open System + +let getNow() : DateTime = + DateTime.Now + """ + + let resultText = newDoc.GetTextAsync() |> GetTaskResult + Assert.Equal(expectedCode, resultText.ToString()) + +[] +let ``Binding linq function doesnt crash`` () = + let symbolName = "skip1" + + let code = + $""" +let skip1 elements = + System.Linq.Enumerable.Skip(elements, 1) + """ + + use context = TestContext.CreateWithCode code + + let spanStart = code.IndexOf symbolName + + let newDoc = tryRefactor code spanStart context (new AddReturnType()) + + let expectedCode = + $""" +let skip1 elements : System.Collections.Generic.IEnumerable<'a> = + System.Linq.Enumerable.Skip(elements, 1) + """ + + let resultText = newDoc.GetTextAsync() |> GetTaskResult + Assert.Equal(expectedCode, resultText.ToString()) + +[] +let ``Handle already existing opens on Linq`` () = + let symbolName = "skip1" + + let code = + $""" +open System + +let skip1 elements = + Linq.Enumerable.Skip(elements, 1) + """ + + use context = TestContext.CreateWithCode code + + let spanStart = code.IndexOf symbolName + + let newDoc = tryRefactor code spanStart context (new AddReturnType()) + + let expectedCode = + $""" +open System + +let skip1 elements : Collections.Generic.IEnumerable<'a> = + Linq.Enumerable.Skip(elements, 1) + """ + + let resultText = newDoc.GetTextAsync() |> GetTaskResult + Assert.Equal(expectedCode, resultText.ToString()) + +[] +let ``Handle already existing opens on Enumerable`` () = + let symbolName = "skip1" + + let code = + $""" +open System +open System.Linq + +let skip1 elements = + Enumerable.Skip(elements, 1) + """ + + use context = TestContext.CreateWithCode code + + let spanStart = code.IndexOf symbolName + + let newDoc = tryRefactor code spanStart context (new AddReturnType()) + + let expectedCode = + $""" +open System +open System.Linq + +let skip1 elements : Collections.Generic.IEnumerable<'a> = + Enumerable.Skip(elements, 1) + """ + + let resultText = newDoc.GetTextAsync() |> GetTaskResult + Assert.Equal(expectedCode, resultText.ToString()) + +[] +let ``Correctly infer custom type that is declared earlier in file`` () = + let symbolName = "sum" + + let code = + """ +type MyType = { Value: int } +let sum a b = {Value=a+b} + """ + + use context = TestContext.CreateWithCode code + + let spanStart = code.IndexOf symbolName + + let newDoc = tryRefactor code spanStart context (new AddReturnType()) + + let expectedCode = + """ +type MyType = { Value: int } +let sum a b : MyType = {Value=a+b} + """ + + let resultText = newDoc.GetTextAsync() |> GetTaskResult + Assert.Equal(expectedCode, resultText.ToString()) + +[] +let ``Correctly infer custom type that is declared earlier in project`` () = + let symbolName = "sum" + + let myModule = + """ +module ModuleFirst +type MyType = { Value: int } + """ + + let code = + """ +module ModuleSecond + +open ModuleFirst + +let sum a b = {Value=a+b} + """ + + let project = + { SyntheticProject.Create( + { sourceFile "First" [] with + Source = myModule + }, + { sourceFile "Second" [ "First" ] with + Source = code + } + ) with + AutoAddModules = false + } + + let solution, _ = RoslynTestHelpers.CreateSolution project + let context = new TestContext(solution) + + let spanStart = code.IndexOf symbolName + + let newDoc = tryRefactor code spanStart context (new AddReturnType()) + + let expectedCode = + """ +module ModuleSecond + +open ModuleFirst + +let sum a b : MyType = {Value=a+b} + """ + + let resultText = newDoc.GetTextAsync() |> GetTaskResult + Assert.Equal(expectedCode.Trim(' ', '\r', '\n'), resultText.ToString().Trim(' ', '\r', '\n')) diff --git a/vsintegration/tests/FSharp.Editor.Tests/Refactors/RefactorTestFramework.fs b/vsintegration/tests/FSharp.Editor.Tests/Refactors/RefactorTestFramework.fs new file mode 100644 index 00000000000..849da8c84ec --- /dev/null +++ b/vsintegration/tests/FSharp.Editor.Tests/Refactors/RefactorTestFramework.fs @@ -0,0 +1,89 @@ +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.CancellableTasks + +open FSharp.Editor.Tests.Helpers +open Microsoft.CodeAnalysis.CodeRefactorings +open Microsoft.CodeAnalysis.CodeActions +open System.Threading + +let GetTaskResult (task: Tasks.Task<'T>) = task.GetAwaiter().GetResult() + +type TestContext(Solution: Solution) = + let mutable _solution = Solution + member _.CancellationToken = CancellationToken.None + + member _.Solution + with set value = _solution <- value + and get () = _solution + + interface IDisposable with + member _.Dispose() = Solution.Workspace.Dispose() + + static member CreateWithCode(code: string) = + let solution = RoslynTestHelpers.CreateSolution(code) + 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") + + new TestContext(solution) + +let tryRefactor (code: string) (cursorPosition) (context: TestContext) (refactorProvider: 'T :> CodeRefactoringProvider) = + cancellableTask { + let mutable action: CodeAction = null + let existingDocument = RoslynTestHelpers.GetLastDocument context.Solution + + context.Solution <- context.Solution.WithDocumentText(existingDocument.Id, SourceText.From(code)) + + let document = RoslynTestHelpers.GetLastDocument context.Solution + + let mutable workspace = context.Solution.Workspace + + let refactoringContext = + CodeRefactoringContext(document, TextSpan(cursorPosition, 1), (fun a -> action <- a), context.CancellationToken) + + do! refactorProvider.ComputeRefactoringsAsync refactoringContext + + let! operations = action.GetOperationsAsync context.CancellationToken + + for operation in operations do + let codeChangeOperation = operation :?> ApplyChangesOperation + codeChangeOperation.Apply(workspace, context.CancellationToken) + context.Solution <- codeChangeOperation.ChangedSolution + () + + let newDocument = context.Solution.GetDocument(document.Id) + return newDocument + + } + |> CancellableTask.startWithoutCancellation + |> GetTaskResult + +let tryGetRefactoringActions (code: string) (cursorPosition) (context: TestContext) (refactorProvider: 'T :> CodeRefactoringProvider) = + cancellableTask { + let refactoringActions = new 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, TextSpan(cursorPosition, 1), (fun a -> refactoringActions.Add a), context.CancellationToken) + + do! refactorProvider.ComputeRefactoringsAsync refactoringContext + + return refactoringActions + } + |> CancellableTask.startWithoutCancellation + |> fun task -> task.Result