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