diff --git a/src/FsAutoComplete.Core/FCSPatches.fs b/src/FsAutoComplete.Core/FCSPatches.fs index 961aac170..b8cd0a499 100644 --- a/src/FsAutoComplete.Core/FCSPatches.fs +++ b/src/FsAutoComplete.Core/FCSPatches.fs @@ -304,6 +304,15 @@ type FSharpParseFileResults with None | _ -> defaultTraverse expr }) + /// Attempts to find the range of the string interpolation that contains a given position. + member scope.TryRangeOfStringInterpolationContainingPos pos = + SyntaxTraversal.Traverse(pos, scope.ParseTree, { new SyntaxVisitorBase<_>() with + member _.VisitExpr(_, _, defaultTraverse, expr) = + match expr with + | SynExpr.InterpolatedString(range = range) when Range.rangeContainsPos range pos -> + Some range + | _ -> defaultTraverse expr }) + module SyntaxTreeOps = open FSharp.Compiler.Syntax let rec synExprContainsError inpExpr = diff --git a/src/FsAutoComplete/CodeFixes/UseTripleQuotedInterpolation.fs b/src/FsAutoComplete/CodeFixes/UseTripleQuotedInterpolation.fs new file mode 100644 index 000000000..de7d344a9 --- /dev/null +++ b/src/FsAutoComplete/CodeFixes/UseTripleQuotedInterpolation.fs @@ -0,0 +1,37 @@ +module FsAutoComplete.CodeFix.UseTripleQuotedInterpolation + +open FsToolkit.ErrorHandling +open FsAutoComplete.CodeFix.Types +open Ionide.LanguageServerProtocol.Types +open FsAutoComplete +open FsAutoComplete.LspHelpers +open FsAutoComplete.FCSPatches + +/// a codefix that replaces erroring single-quoted interpolations with triple-quoted interpolations +let fix (getParseResultsForFile: GetParseResultsForFile) (getRangeText: GetRangeText) : CodeFix = + Run.ifDiagnosticByCode (Set.ofList [ "3373" ]) (fun diagnostic codeActionParams -> + asyncResult { + let pos = protocolPosToPos diagnostic.Range.Start + + let filePath = + codeActionParams.TextDocument.GetFilePath() + |> Utils.normalizePath + + let! tyRes, _, sourceText = getParseResultsForFile filePath pos + + match tyRes.GetParseResults.TryRangeOfStringInterpolationContainingPos pos with + | Some range -> + let! interpolationText = sourceText.GetText range + // skip the leading '$' in the existing single-quoted interpolation + let newText = "$\"\"" + interpolationText.[1..] + "\"\"" + + return + [ { File = codeActionParams.TextDocument + SourceDiagnostic = Some diagnostic + Title = "Use triple-quoted string interpolation" + Edits = + [| { Range = fcsRangeToLsp range + NewText = newText } |] + Kind = FixKind.Fix } ] + | None -> return [] + }) diff --git a/src/FsAutoComplete/FsAutoComplete.Lsp.fs b/src/FsAutoComplete/FsAutoComplete.Lsp.fs index ba6cf8979..ebe3605e9 100644 --- a/src/FsAutoComplete/FsAutoComplete.Lsp.fs +++ b/src/FsAutoComplete/FsAutoComplete.Lsp.fs @@ -868,7 +868,8 @@ type FSharpLspServer(backgroundServiceEnabled: bool, state: State, lspClient: FS ChangeTypeOfNameToNameOf.fix tryGetParseResultsForFile AddMissingInstanceMember.fix AddExplicitTypeToParameter.fix tryGetParseResultsForFile - ConvertPositionalDUToNamed.fix tryGetParseResultsForFile getRangeText |] + ConvertPositionalDUToNamed.fix tryGetParseResultsForFile getRangeText + UseTripleQuotedInterpolation.fix tryGetParseResultsForFile getRangeText |] match p.RootPath, c.AutomaticWorkspaceInit with diff --git a/test/FsAutoComplete.Tests.Lsp/CodeFixTests.fs b/test/FsAutoComplete.Tests.Lsp/CodeFixTests.fs index a1188cb89..483e44d97 100644 --- a/test/FsAutoComplete.Tests.Lsp/CodeFixTests.fs +++ b/test/FsAutoComplete.Tests.Lsp/CodeFixTests.fs @@ -852,6 +852,62 @@ let positionalToNamedDUTests state = expectEdits patternPos edits) ] +let tripleQuotedInterpolationTests state = + let server = + async { + let path = + Path.Combine(__SOURCE_DIRECTORY__, "TestCases", "TripleQuotedInterpolation") + + let cfg = defaultConfigDto + let! (server, events) = serverInitialize path cfg state + do! waitForWorkspaceFinishedParsing events + let path = Path.Combine(path, "Script.fsx") + let tdop: DidOpenTextDocumentParams = { TextDocument = loadDocument path } + do! server.TextDocumentDidOpen tdop + + let! diagnostics = + events + |> waitForParseResultsForFile "Script.fsx" + |> AsyncResult.bimap (fun _ -> failtest "Should have had errors") id + + return (server, path, diagnostics) + } + |> Async.Cache + + testList + "interpolation fixes" + [ testCaseAsync + "converts erroring single-quoted interpolation to triple-quoted" + (async { + let! (server, filePath, diagnostics) = server + + let diagnostic = + diagnostics + |> Array.tryFind (fun d -> d.Code = Some "3373") + |> Option.defaultWith (fun _ -> failtest "Should have gotten an error of type 3373") + + let context: CodeActionParams = + { Context = { Diagnostics = [| diagnostic |] } + Range = + { Start = diagnostic.Range.Start + End = diagnostic.Range.Start } + TextDocument = { Uri = Path.FilePathToUri filePath } } + + match! server.TextDocumentCodeAction context with + | Ok (Some (TextDocumentCodeActionResult.CodeActions [| { Title = "Use triple-quoted string interpolation" + Kind = Some "quickfix" + Edit = Some { DocumentChanges = Some [| { Edits = [| { Range = { Start = { Line = 0 + Character = 8 } + End = { Line = 0 + Character = 44 } } + NewText = "$\"\"\":^) {if true then \"y\" else \"n\"} d\"\"\"" } |] } |] } } |])) -> + () + | Ok other -> + failtestf + $"Should have converted single quoted interpolations to triple quotes, but instead generated %A{other}" + | Error reason -> failtestf $"Should have succeeded, but failed with %A{reason}" + }) ] + let tests state = testList "codefix tests" diff --git a/test/FsAutoComplete.Tests.Lsp/TestCases/TripleQuotedInterpolation/Script.fsx b/test/FsAutoComplete.Tests.Lsp/TestCases/TripleQuotedInterpolation/Script.fsx new file mode 100644 index 000000000..03c245bb7 --- /dev/null +++ b/test/FsAutoComplete.Tests.Lsp/TestCases/TripleQuotedInterpolation/Script.fsx @@ -0,0 +1 @@ +let a = $":^) {if true then "y" else "n"} d"