diff --git a/vsintegration/src/FSharp.Editor/CodeFixes/ConvertCSharpUsingToFSharpOpen.fs b/vsintegration/src/FSharp.Editor/CodeFixes/ConvertCSharpUsingToFSharpOpen.fs index 0619a7e021c..38c4f7a1e5a 100644 --- a/vsintegration/src/FSharp.Editor/CodeFixes/ConvertCSharpUsingToFSharpOpen.fs +++ b/vsintegration/src/FSharp.Editor/CodeFixes/ConvertCSharpUsingToFSharpOpen.fs @@ -9,6 +9,8 @@ open System.Collections.Immutable open Microsoft.CodeAnalysis.Text open Microsoft.CodeAnalysis.CodeFixes +open CancellableTasks + [] type internal ConvertCSharpUsingToFSharpOpenCodeFixProvider [] () = inherit CodeFixProvider() @@ -16,66 +18,67 @@ type internal ConvertCSharpUsingToFSharpOpenCodeFixProvider [ 0 && not (Char.IsWhiteSpace(ch)) do pos <- pos - 1 - ch <- sourceText.[pos] + ch <- sourceText[pos] // Walk back whitespace - ch <- sourceText.[pos] + ch <- sourceText[pos] while pos > 0 && Char.IsWhiteSpace(ch) do pos <- pos - 1 - ch <- sourceText.[pos] + ch <- sourceText[pos] // Take 'using' slice and don't forget that offset because computer math is annoying let start = pos - usingLength + 1 - let span = TextSpan(start, usingLength) - let slice = sourceText.GetSubText(span).ToString() - struct (slice = "using", start) - - let registerCodeFix (context: CodeFixContext) (str: string) (span: TextSpan) = - let replacement = - let str = str.Replace("using", "open").Replace(";", "") - TextChange(span, str) - do context.RegisterFsharpFix(CodeFix.ConvertCSharpUsingToFSharpOpen, title, [| replacement |]) + if start < 0 then + false + else + let span = TextSpan(start, usingLength) + let slice = sourceText.GetSubText(span).ToString() + slice = "using" override _.FixableDiagnosticIds = ImmutableArray.Create("FS0039", "FS0201") - override _.RegisterCodeFixesAsync context = - asyncMaybe { - let! sourceText = context.Document.GetTextAsync(context.CancellationToken) - - // TODO: handle single-line case? - let statementWithSemicolonSpan = - TextSpan(context.Span.Start, context.Span.Length + 1) - - do! Option.guard (sourceText.Length >= statementWithSemicolonSpan.End) - - let statementWithSemicolon = - sourceText.GetSubText(statementWithSemicolonSpan).ToString() - - // Top of the file case -- entire line gets a diagnostic - if - (statementWithSemicolon.StartsWith("using") - && statementWithSemicolon.EndsWith(";")) - then - registerCodeFix context statementWithSemicolon statementWithSemicolonSpan - else - // Only the identifier being opened has a diagnostic, so we try to find the rest of the statement - let struct (isCSharpUsingShape, start) = - isCSharpUsingShapeWithPos context sourceText - - if isCSharpUsingShape then - let len = (context.Span.Start - start) + statementWithSemicolonSpan.Length - let fullSpan = TextSpan(start, len) - let str = sourceText.GetSubText(fullSpan).ToString() - registerCodeFix context str fullSpan - } - |> Async.Ignore - |> RoslynHelpers.StartAsyncUnitAsTask(context.CancellationToken) + override this.RegisterCodeFixesAsync context = context.RegisterFsharpFix this + + override this.GetFixAllProvider() = this.RegisterFsharpFixAll() + + interface IFSharpCodeFixProvider with + member _.GetCodeFixIfAppliesAsync context = + cancellableTask { + let diagnostic = context.Diagnostics[0] + let! errorText = context.GetSquigglyTextAsync() + let! sourceText = context.GetSourceTextAsync() + + let isValidCase = + match diagnostic.Id with + // using is included in the squiggly + | "FS0201" when errorText.Contains("using ") -> true + // using is not included in the squiqqly + | "FS0039" when isCSharpUsingShapeWithPos context.Span sourceText -> true + | _ -> false + + if isValidCase then + let lineNumber = sourceText.Lines.GetLinePositionSpan(context.Span).Start.Line + let line = sourceText.Lines[lineNumber] + + let change = + TextChange(line.Span, line.ToString().Replace("using", "open").Replace(";", "")) + + return + ValueSome + { + Name = CodeFix.ConvertCSharpUsingToFSharpOpen + Message = title + Changes = [ change ] + } + else + return ValueNone + } diff --git a/vsintegration/tests/FSharp.Editor.Tests/CodeFixes/ConvertCSharpUsingToFSharpOpenTests.fs b/vsintegration/tests/FSharp.Editor.Tests/CodeFixes/ConvertCSharpUsingToFSharpOpenTests.fs new file mode 100644 index 00000000000..936b0169102 --- /dev/null +++ b/vsintegration/tests/FSharp.Editor.Tests/CodeFixes/ConvertCSharpUsingToFSharpOpenTests.fs @@ -0,0 +1,156 @@ +// Copyright (c) Microsoft Corporation. All Rights Reserved. See License.txt in the project root for license information. + +module FSharp.Editor.Tests.CodeFixes.ConvertCSharpUsingToFSharpOpenTests + +open Microsoft.VisualStudio.FSharp.Editor +open Xunit + +open CodeFixTestFramework + +let private codeFix = ConvertCSharpUsingToFSharpOpenCodeFixProvider() + +[] +[] +[] +let ``Fixes FS0039 - simple namespace`` optionalSemicolon = + let code = + $""" +using System{optionalSemicolon} +""" + + let expected = + Some + { + Message = "Convert C# 'using' to F# 'open'" + FixedCode = + """ +open System +""" + } + + let actual = codeFix |> tryFix code Auto + + Assert.Equal(expected, actual) + +[] +[] +[] +let ``Fixes FS0039 - complex namespace`` optionalSemicolon = + let code = + $""" +using System.IO{optionalSemicolon} +""" + + let expected = + Some + { + Message = "Convert C# 'using' to F# 'open'" + FixedCode = + """ +open System.IO +""" + } + + let actual = codeFix |> tryFix code Auto + + Assert.Equal(expected, actual) + +[] +let ``Doesn't fix random FS0039`` () = + let code = """namespa""" + + let expected = None + + let actual = codeFix |> tryFix code Auto + + Assert.Equal(expected, actual) + +[] +[] +[] +let ``Fixes FS0201 - simple namespace`` optionalSemicolon = + let code = + $""" +namespace Test + +using System{optionalSemicolon} +""" + + let expected = + Some + { + Message = "Convert C# 'using' to F# 'open'" + FixedCode = + """ +namespace Test + +open System +""" + } + + let actual = codeFix |> tryFix code Auto + + Assert.Equal(expected, actual) + +[] +[] +[] +let ``Fixes FS0201 - complex namespace`` optionalSemicolon = + let code = + $""" +namespace Test + +using System.IO{optionalSemicolon} +""" + + let expected = + Some + { + Message = "Convert C# 'using' to F# 'open'" + FixedCode = + """ +namespace Test + +open System.IO +""" + } + + let actual = codeFix |> tryFix code Auto + + Assert.Equal(expected, actual) + +[] +let ``Doesn't fix random FS0201`` () = + let code = + """ +namespace Test + +let x = 42 +""" + + let expected = None + + let actual = codeFix |> tryFix code Auto + + Assert.Equal(expected, actual) + +[] +let ``Handles spaces before semicolons`` () = + let code = + $""" +using System ; +""" + + let expected = + Some + { + Message = "Convert C# 'using' to F# 'open'" + FixedCode = + """ +open System +""" + } + + let actual = codeFix |> tryFix code Auto + + Assert.Equal(expected, actual) diff --git a/vsintegration/tests/FSharp.Editor.Tests/FSharp.Editor.Tests.fsproj b/vsintegration/tests/FSharp.Editor.Tests/FSharp.Editor.Tests.fsproj index 65851ab5b71..52048e72bcc 100644 --- a/vsintegration/tests/FSharp.Editor.Tests/FSharp.Editor.Tests.fsproj +++ b/vsintegration/tests/FSharp.Editor.Tests/FSharp.Editor.Tests.fsproj @@ -59,6 +59,7 @@ +