diff --git a/src/FsAutoComplete/CodeFixes/ToInterpolatedString.fsi b/src/FsAutoComplete/CodeFixes/ToInterpolatedString.fsi new file mode 100644 index 000000000..5adc16bfc --- /dev/null +++ b/src/FsAutoComplete/CodeFixes/ToInterpolatedString.fsi @@ -0,0 +1,12 @@ +module FsAutoComplete.CodeFix.ToInterpolatedString + +open FsAutoComplete.CodeFix.Types +open Ionide.LanguageServerProtocol.Types + +val title: string + +val fix: + getParseResultsForFile: GetParseResultsForFile -> + getLanguageVersion: GetLanguageVersion -> + codeActionParams: CodeActionParams -> + Async> diff --git a/src/FsAutoComplete/CodeFixes/UpdateValueInSignatureFile.fs b/src/FsAutoComplete/CodeFixes/UpdateValueInSignatureFile.fs new file mode 100644 index 000000000..87e7fa4e3 --- /dev/null +++ b/src/FsAutoComplete/CodeFixes/UpdateValueInSignatureFile.fs @@ -0,0 +1,81 @@ +module FsAutoComplete.CodeFix.UpdateValueInSignatureFile + +open FSharp.Compiler.Symbols +open FSharp.Compiler.Syntax +open FsToolkit.ErrorHandling +open Ionide.LanguageServerProtocol.Types +open FsAutoComplete.CodeFix.Types +open FsAutoComplete +open FsAutoComplete.LspHelpers + +let visitSynModuleSigDecl (name: string) (decl: SynModuleSigDecl) = + match decl with + | SynModuleSigDecl.Val(valSig = SynValSig(ident = SynIdent(ident = ident)); range = m) when ident.idText = name -> + Some m + | _ -> None + +let visitSynModuleOrNamespaceSig (name: string) (SynModuleOrNamespaceSig(decls = decls)) = + decls |> List.tryPick (visitSynModuleSigDecl name) + +let visitParsedSigFileInput (name: string) (ParsedSigFileInput(contents = contents)) = + contents |> List.tryPick (visitSynModuleOrNamespaceSig name) + +let visitTree (name: string) (tree: ParsedInput) = + match tree with + | ParsedInput.ImplFile _ -> None + | ParsedInput.SigFile parsedSigFileInput -> visitParsedSigFileInput name parsedSigFileInput + +let title = "Update val in signature file" + +let fix (getParseResultsForFile: GetParseResultsForFile) : CodeFix = + Run.ifDiagnosticByCode (Set.ofList [ "34" ]) (fun diagnostic codeActionParams -> + asyncResult { + let implFilePath = codeActionParams.TextDocument.GetFilePath() + let sigFilePath = $"%s{implFilePath}i" + + let implFileName = Utils.normalizePath implFilePath + let sigFileName = Utils.normalizePath sigFilePath + + let sigTextDocumentIdentifier: TextDocumentIdentifier = + { Uri = $"%s{codeActionParams.TextDocument.Uri}i" } + + let! (implParseAndCheckResults: ParseAndCheckResults, implLine: string, implSourceText: IFSACSourceText) = + getParseResultsForFile implFileName (protocolPosToPos diagnostic.Range.Start) + + let! implBindingName = + implSourceText.GetText(protocolRangeToRange implParseAndCheckResults.GetParseResults.FileName diagnostic.Range) + + let! (sigParseAndCheckResults: ParseAndCheckResults, _sigLine: string, _sigSourceText: IFSACSourceText) = + getParseResultsForFile sigFileName (protocolPosToPos diagnostic.Range.Start) + + match visitTree implBindingName sigParseAndCheckResults.GetParseResults.ParseTree with + | None -> return [] + | Some mVal -> + let endPos = protocolPosToPos diagnostic.Range.End + + let symbolUse = + implParseAndCheckResults.GetCheckResults.GetSymbolUseAtLocation( + endPos.Line, + endPos.Column, + implLine, + [ implBindingName ] + ) + + match symbolUse with + | None -> return [] + | Some symbolUse -> + match symbolUse.Symbol with + | :? FSharpMemberOrFunctionOrValue as mfv -> + match mfv.GetValSignatureText(symbolUse.DisplayContext, symbolUse.Range) with + | None -> return [] + | Some valText -> + return + [ { SourceDiagnostic = None + Title = title + File = sigTextDocumentIdentifier + Edits = + [| { Range = fcsRangeToLsp mVal + NewText = valText } |] + Kind = FixKind.Fix } ] + | _ -> return [] + }) diff --git a/src/FsAutoComplete/CodeFixes/UpdateValueInSignatureFile.fsi b/src/FsAutoComplete/CodeFixes/UpdateValueInSignatureFile.fsi new file mode 100644 index 000000000..ede271327 --- /dev/null +++ b/src/FsAutoComplete/CodeFixes/UpdateValueInSignatureFile.fsi @@ -0,0 +1,6 @@ +module FsAutoComplete.CodeFix.UpdateValueInSignatureFile + +open FsAutoComplete.CodeFix.Types + +val title: string +val fix: getParseResultsForFile: GetParseResultsForFile -> CodeFix diff --git a/src/FsAutoComplete/LspServers/AdaptiveFSharpLspServer.fs b/src/FsAutoComplete/LspServers/AdaptiveFSharpLspServer.fs index 49cd59c15..29da7fc7d 100644 --- a/src/FsAutoComplete/LspServers/AdaptiveFSharpLspServer.fs +++ b/src/FsAutoComplete/LspServers/AdaptiveFSharpLspServer.fs @@ -1835,7 +1835,8 @@ type AdaptiveFSharpLspServer UseTripleQuotedInterpolation.fix tryGetParseResultsForFile getRangeText RenameParamToMatchSignature.fix tryGetParseResultsForFile RemovePatternArgument.fix tryGetParseResultsForFile - ToInterpolatedString.fix tryGetParseResultsForFile getLanguageVersion |]) + ToInterpolatedString.fix tryGetParseResultsForFile getLanguageVersion + UpdateValueInSignatureFile.fix tryGetParseResultsForFile |]) let forgetDocument (uri: DocumentUri) = async { diff --git a/src/FsAutoComplete/LspServers/FsAutoComplete.Lsp.fs b/src/FsAutoComplete/LspServers/FsAutoComplete.Lsp.fs index b11db9eee..08d95f5f7 100644 --- a/src/FsAutoComplete/LspServers/FsAutoComplete.Lsp.fs +++ b/src/FsAutoComplete/LspServers/FsAutoComplete.Lsp.fs @@ -1173,6 +1173,7 @@ type FSharpLspServer(state: State, lspClient: FSharpLspClient, sourceTextFactory RenameParamToMatchSignature.fix tryGetParseResultsForFile RemovePatternArgument.fix tryGetParseResultsForFile ToInterpolatedString.fix tryGetParseResultsForFile tryGetLanguageVersion + UpdateValueInSignatureFile.fix tryGetParseResultsForFile |] diff --git a/test/FsAutoComplete.Tests.Lsp/CodeFixTests/RenameParamToMatchSignatureTests.fs b/test/FsAutoComplete.Tests.Lsp/CodeFixTests/RenameParamToMatchSignatureTests.fs index daa72bc34..2d67e6d79 100644 --- a/test/FsAutoComplete.Tests.Lsp/CodeFixTests/RenameParamToMatchSignatureTests.fs +++ b/test/FsAutoComplete.Tests.Lsp/CodeFixTests/RenameParamToMatchSignatureTests.fs @@ -13,43 +13,43 @@ open Utils.CursorbasedTests.CodeFix let tests state = - let selectCodeFix expectedName = CodeFix.withTitle (RenameParamToMatchSignature.title expectedName) + let selectCodeFix expectedName = + CodeFix.withTitle (RenameParamToMatchSignature.title expectedName) // requires `fsi` and corresponding `fs` file (and a project!) // -> cannot use untitled doc // -> use existing files, but load with text specified in tests - let path = Path.Combine(__SOURCE_DIRECTORY__, @"../TestCases/CodeFixTests/RenameParamToMatchSignature/") + let path = + Path.Combine(__SOURCE_DIRECTORY__, @"../TestCases/CodeFixTests/RenameParamToMatchSignature/") + let (fsiFile, fsFile) = ("Code.fsi", "Code.fs") - serverTestList (nameof RenameParamToMatchSignature) state defaultConfigDto (Some path) (fun server -> [ - let checkWithFsi - fsiSource - fsSourceWithCursor - selectCodeFix - fsSourceExpected - = async { - let fsiSource = fsiSource |> Text.trimTripleQuotation - let (cursor, fsSource) = - fsSourceWithCursor - |> Text.trimTripleQuotation - |> Cursor.assertExtractRange - let! (fsiDoc, diags) = server |> Server.openDocumentWithText fsiFile fsiSource - use fsiDoc = fsiDoc - Expect.isEmpty diags "There should be no diagnostics in fsi doc" - let! (fsDoc, diags) = server |> Server.openDocumentWithText fsFile fsSource - use fsDoc = fsDoc - - do! - checkFixAt - (fsDoc, diags) - (fsSource, cursor) - (Diagnostics.expectCode "3218") - selectCodeFix - (After (fsSourceExpected |> Text.trimTripleQuotation)) - } - - testCaseAsync "can rename parameter in F# function" <| - checkWithFsi + serverTestList (nameof RenameParamToMatchSignature) state defaultConfigDto (Some path) (fun server -> + [ let checkWithFsi fsiSource fsSourceWithCursor selectCodeFix fsSourceExpected = + async { + let fsiSource = fsiSource |> Text.trimTripleQuotation + + let (cursor, fsSource) = + fsSourceWithCursor |> Text.trimTripleQuotation |> Cursor.assertExtractRange + + let! (fsiDoc, diags) = server |> Server.openDocumentWithText fsiFile fsiSource + use fsiDoc = fsiDoc + Expect.isEmpty diags "There should be no diagnostics in fsi doc" + let! (fsDoc, diags) = server |> Server.openDocumentWithText fsFile fsSource + use fsDoc = fsDoc + + do! + checkFixAt + (fsDoc, diags) + fsDoc.VersionedTextDocumentIdentifier + (fsSource, cursor) + (Diagnostics.expectCode "3218") + selectCodeFix + (After(fsSourceExpected |> Text.trimTripleQuotation)) + } + + testCaseAsync "can rename parameter in F# function" + <| checkWithFsi """ module Code @@ -66,8 +66,9 @@ let tests state = let f value1 = value1 + 1 """ - testCaseAsync "can rename parameter with backticks in signature in F# function" <| - checkWithFsi + + testCaseAsync "can rename parameter with backticks in signature in F# function" + <| checkWithFsi """ module Code @@ -84,8 +85,9 @@ let tests state = let f ``my value2`` = ``my value2`` + 1 """ - testCaseAsync "can rename parameter with backticks in implementation in F# function" <| - checkWithFsi + + testCaseAsync "can rename parameter with backticks in implementation in F# function" + <| checkWithFsi """ module Code @@ -102,8 +104,9 @@ let tests state = let f value3 = value3 + 1 """ - testCaseAsync "can rename all usage in F# function" <| - checkWithFsi + + testCaseAsync "can rename all usage in F# function" + <| checkWithFsi """ module Code @@ -128,8 +131,9 @@ let tests state = let v = a + b v + x * y """ - testCaseAsync "can rename parameter with type in F# function" <| - checkWithFsi + + testCaseAsync "can rename parameter with type in F# function" + <| checkWithFsi """ module Code @@ -146,8 +150,9 @@ let tests state = let f (value5: int) = value5 + 1 """ - testCaseAsync "can rename parameter in constructor" <| - checkWithFsi + + testCaseAsync "can rename parameter in constructor" + <| checkWithFsi """ module Code @@ -167,8 +172,9 @@ let tests state = type T(value6: int) = let _ = value6 + 3 """ - testCaseAsync "can rename parameter in member" <| - checkWithFsi + + testCaseAsync "can rename parameter in member" + <| checkWithFsi """ module Code @@ -189,8 +195,9 @@ let tests state = type T() = member _.F(value7) = value7 + 1 """ - testCaseAsync "can rename parameter with ' in signature in F# function" <| - checkWithFsi + + testCaseAsync "can rename parameter with ' in signature in F# function" + <| checkWithFsi """ module Code @@ -207,8 +214,9 @@ let tests state = let f value8' = value8' + 1 """ - testCaseAsync "can rename parameter with ' in implementation in F# function" <| - checkWithFsi + + testCaseAsync "can rename parameter with ' in implementation in F# function" + <| checkWithFsi """ module Code @@ -225,8 +233,9 @@ let tests state = let f value9 = value9 + 1 """ - testCaseAsync "can rename parameter with ' (not in last place) in signature in F# function" <| - checkWithFsi + + testCaseAsync "can rename parameter with ' (not in last place) in signature in F# function" + <| checkWithFsi """ module Code @@ -243,8 +252,9 @@ let tests state = let f v10'2 = v10'2 + 1 """ - testCaseAsync "can rename parameter with ' (not in last place) in implementation in F# function" <| - checkWithFsi + + testCaseAsync "can rename parameter with ' (not in last place) in implementation in F# function" + <| checkWithFsi """ module Code @@ -261,8 +271,9 @@ let tests state = let f value11 = value11 + 1 """ - testCaseAsync "can rename parameter with multiple ' in signature in F# function" <| - checkWithFsi + + testCaseAsync "can rename parameter with multiple ' in signature in F# function" + <| checkWithFsi """ module Code @@ -279,8 +290,9 @@ let tests state = let f value12'v'2 = value12'v'2 + 1 """ - testCaseAsync "can rename parameter with multiple ' in implementation in F# function" <| - checkWithFsi + + testCaseAsync "can rename parameter with multiple ' in implementation in F# function" + <| checkWithFsi """ module Code @@ -297,8 +309,9 @@ let tests state = let f value13 = value13 + 1 """ - itestCaseAsync "can handle `' and implementation '` in impl name" <| - checkWithFsi + + itestCaseAsync "can handle `' and implementation '` in impl name" + <| checkWithFsi """ module Code @@ -315,9 +328,9 @@ let tests state = let f value14 = value14 + 1 """ - //ENHANCEMENT: correctly detect below. Currently: detects sig name `sig` - itestCaseAsync "can handle `' and implementation '` in sig name" <| - checkWithFsi + //ENHANCEMENT: correctly detect below. Currently: detects sig name `sig` + itestCaseAsync "can handle `' and implementation '` in sig name" + <| checkWithFsi """ module Code @@ -333,5 +346,4 @@ let tests state = module Code let f ``sig' and implementation 'impl' do not match`` = ``sig' and implementation 'impl' do not match`` + 1 - """ - ]) + """ ]) diff --git a/test/FsAutoComplete.Tests.Lsp/CodeFixTests/Tests.fs b/test/FsAutoComplete.Tests.Lsp/CodeFixTests/Tests.fs index 9230d36a8..e8386aaf8 100644 --- a/test/FsAutoComplete.Tests.Lsp/CodeFixTests/Tests.fs +++ b/test/FsAutoComplete.Tests.Lsp/CodeFixTests/Tests.fs @@ -3351,4 +3351,5 @@ let tests state = useTripleQuotedInterpolationTests state wrapExpressionInParenthesesTests state removeRedundantAttributeSuffixTests state - removePatternArgumentTests state ] + removePatternArgumentTests state + UpdateValueInSignatureFileTests.tests state ] diff --git a/test/FsAutoComplete.Tests.Lsp/CodeFixTests/UpdateValueInSignatureFileTests.fs b/test/FsAutoComplete.Tests.Lsp/CodeFixTests/UpdateValueInSignatureFileTests.fs new file mode 100644 index 000000000..47fe99699 --- /dev/null +++ b/test/FsAutoComplete.Tests.Lsp/CodeFixTests/UpdateValueInSignatureFileTests.fs @@ -0,0 +1,68 @@ +module private FsAutoComplete.Tests.CodeFixTests.UpdateValueInSignatureFileTests + +open System.IO +open Expecto +open Helpers +open Utils.ServerTests +open Utils.CursorbasedTests +open FsAutoComplete.CodeFix +open Utils.Utils +open Utils.TextEdit +open Utils.Server +open Utils.CursorbasedTests.CodeFix + +let path = Path.Combine(__SOURCE_DIRECTORY__, @"../TestCases/CodeFixTests/RenameParamToMatchSignature/") +let fsiFile, fsFile = ("Code.fsi", "Code.fs") + +let checkWithFsi + server + fsiSource + fsSourceWithCursor + selectCodeFix + fsiSourceExpected + = async { + let fsiSource = fsiSource |> Text.trimTripleQuotation + let cursor, fsSource = + fsSourceWithCursor + |> Text.trimTripleQuotation + |> Cursor.assertExtractRange + let! fsiDoc, diags = server |> Server.openDocumentWithText fsiFile fsiSource + use fsiDoc = fsiDoc + Expect.isEmpty diags "There should be no diagnostics in fsi doc" + let! fsDoc, diags = server |> Server.openDocumentWithText fsFile fsSource + use fsDoc = fsDoc + + do! + checkFixAt + (fsDoc, diags) + fsiDoc.VersionedTextDocumentIdentifier + (fsiSource, cursor) + (Diagnostics.expectCode "34") + selectCodeFix + (After (fsiSourceExpected |> Text.trimTripleQuotation)) + } + +let tests state = + serverTestList (nameof UpdateValueInSignatureFile) state defaultConfigDto (Some path) (fun server -> + [ let selectCodeFix = CodeFix.withTitle UpdateValueInSignatureFile.title + + ftestCaseAsync "first unit test for UpdateValueInSignatureFile" + <| checkWithFsi + server + """ +module A + +val a: b:int -> int +""" +""" +module A + +let a$0 (b:int) (c: string) = 0 +""" + selectCodeFix + """ +module A + +val a: b: int -> c: string -> int +""" + ]) diff --git a/test/FsAutoComplete.Tests.Lsp/Utils/CursorbasedTests.fs b/test/FsAutoComplete.Tests.Lsp/Utils/CursorbasedTests.fs index e861e8746..82e2293c5 100644 --- a/test/FsAutoComplete.Tests.Lsp/Utils/CursorbasedTests.fs +++ b/test/FsAutoComplete.Tests.Lsp/Utils/CursorbasedTests.fs @@ -1,6 +1,6 @@ module Utils.CursorbasedTests + open Expecto -open Expecto.Diff open Ionide.LanguageServerProtocol.Types open FsToolkit.ErrorHandling open Utils.Utils @@ -17,8 +17,7 @@ module CodeFix = let private logger = LogProvider.getLoggerByName "CursorbasedTests.CodeFix" let private diagnosticsIn (range: Range) (diags: Diagnostic[]) = - diags - |> Array.filter (fun diag -> range |> Range.overlapsStrictly diag.Range) + diags |> Array.filter (fun diag -> range |> Range.overlapsStrictly diag.Range) /// Note: Return should be just ONE `CodeAction` (for Applicable) or ZERO `CodeAction` (for Not Applicable). /// But actual return type is an array of `CodeAction`s: @@ -26,6 +25,7 @@ module CodeFix = /// * Returning `CodeAction option` would mean different filters for `check` (exactly one fix) and `checkNotApplicable` (exactly zero fix). /// Both error with multiple matching fixes! type ChooseFix = CodeAction[] -> CodeAction[] + type ExpectedResult = | NotApplicable | Applicable @@ -33,25 +33,27 @@ module CodeFix = let checkFixAt (doc: Document, diagnostics: Diagnostic[]) + (editsFrom: VersionedTextDocumentIdentifier) (beforeWithoutCursor: string, cursorRange: Range) (validateDiagnostics: Diagnostic[] -> unit) (chooseFix: ChooseFix) (expected: ExpectedResult) - = async { + = + async { // filter to only diags matching the cursor range let diags = diagnostics |> diagnosticsIn cursorRange validateDiagnostics diags // get code fixes from server let! res = doc |> Document.codeActionAt diags cursorRange + let allCodeActions = match res, expected with | None, (Applicable | After _) -> - // error here instead of later to return error noting it was `None` instead of empty CodeAction array - Expect.isSome res "No CodeAction returned (`None`)" - failwith "unreachable" - | None, NotApplicable -> - [||] - | Some (Helpers.CodeActions actions), _ -> actions + // error here instead of later to return error noting it was `None` instead of empty CodeAction array + Expect.isSome res "No CodeAction returned (`None`)" + failwith "unreachable" + | None, NotApplicable -> [||] + | Some(Helpers.CodeActions actions), _ -> actions | Some _, _ -> failwith "Expected some code actions from the server" @@ -71,42 +73,41 @@ module CodeFix = match expected with | NotApplicable -> // Expect.isEmpty codeActions "There should be no applicable code action" // doesn't show `actual` when not empty - if not (codeActions |> Array.isEmpty) then + if not (codeActions |> Array.isEmpty) then failtestf "There should be no applicable code action, but was %A" codeActions - | Applicable -> - codeActions - |> getCodeAction - |> ignore - //ENHANCEMENT?: apply edits to check valid? + | Applicable -> codeActions |> getCodeAction |> ignore + //ENHANCEMENT?: apply edits to check valid? | After expected -> - let codeAction = codeActions |> getCodeAction + let codeAction = codeActions |> getCodeAction + + /// Error message is appended by selected `codeAction` + let inline failCodeFixTest (msg: string) = + let msg = + if + System.String.IsNullOrWhiteSpace msg + || System.Char.IsPunctuation(msg, msg.Length - 1) + then + msg + else + msg + "." - /// Error message is appended by selected `codeAction` - let inline failCodeFixTest (msg: string) = - let msg = - if System.String.IsNullOrWhiteSpace msg || System.Char.IsPunctuation(msg, msg.Length-1) then - msg - else - msg + "." - failtest $"{msg} CodeAction was: %A{codeAction}" - - // only text edits supported - if codeAction.Command |> Option.isSome then - failCodeFixTest "Code action contains commands. Commands aren't supported in this test!" - - let edits = - codeAction.Edit - |> Option.defaultWith (fun _ -> failCodeFixTest "Code action doesn't contain any edits") - |> WorkspaceEdit.tryExtractTextEditsInSingleFile doc.VersionedTextDocumentIdentifier - |> Result.valueOr failCodeFixTest - - // apply fix - let actual = - beforeWithoutCursor - |> TextEdits.apply edits - |> Result.valueOr failCodeFixTest - - Expecto.Diff.equals actual expected "Incorrect text after applying the chosen code action" + failtest $"{msg} CodeAction was: %A{codeAction}" + + // only text edits supported + if codeAction.Command |> Option.isSome then + failCodeFixTest "Code action contains commands. Commands aren't supported in this test!" + + let edits = + codeAction.Edit + |> Option.defaultWith (fun _ -> failCodeFixTest "Code action doesn't contain any edits") + |> WorkspaceEdit.tryExtractTextEditsInSingleFile editsFrom + |> Result.valueOr failCodeFixTest + + // apply fix + let actual = + beforeWithoutCursor |> TextEdits.apply edits |> Result.valueOr failCodeFixTest + + Expecto.Diff.equals actual expected "Incorrect text after applying the chosen code action" } let private checkFix @@ -115,16 +116,22 @@ module CodeFix = (validateDiagnostics: Diagnostic[] -> unit) (chooseFix: ChooseFix) (expected: unit -> ExpectedResult) - = async { + = + async { let (range, text) = - beforeWithCursor - |> Text.trimTripleQuotation - |> Cursor.assertExtractRange + beforeWithCursor |> Text.trimTripleQuotation |> Cursor.assertExtractRange // load text file let! (doc, diags) = server |> Server.createUntitledDocument text use doc = doc // ensure doc gets closed (disposed) after test - do! checkFixAt (doc, diags) (text, range) validateDiagnostics chooseFix (expected()) + do! + checkFixAt + (doc, diags) + doc.VersionedTextDocumentIdentifier + (text, range) + validateDiagnostics + chooseFix + (expected ()) } /// Checks a CodeFix (CodeAction) for validity. @@ -158,50 +165,18 @@ module CodeFix = /// Linebreaks from edits in selected CodeFix are all transformed to just `\n` /// -> CodeFix can use `\r` and `\r\n` /// If you want to validate Line Endings of CodeFix, add a validation step to your `chooseFix` - let check - server - beforeWithCursor - validateDiagnostics - chooseFix - expected - = - checkFix - server - beforeWithCursor - validateDiagnostics - chooseFix - (fun () -> After (expected |> Text.trimTripleQuotation)) + let check server beforeWithCursor validateDiagnostics chooseFix expected = + checkFix server beforeWithCursor validateDiagnostics chooseFix (fun () -> + After(expected |> Text.trimTripleQuotation)) /// Note: Doesn't apply Fix! Just checks its existence! - let checkApplicable - server - beforeWithCursor - validateDiagnostics - chooseFix - = - checkFix - server - beforeWithCursor - validateDiagnostics - chooseFix - (fun () -> Applicable) - - let checkNotApplicable - server - beforeWithCursor - validateDiagnostics - chooseFix - = - checkFix - server - beforeWithCursor - validateDiagnostics - chooseFix - (fun () -> NotApplicable) - - let matching cond (fixes: CodeAction array) = - fixes - |> Array.filter cond + let checkApplicable server beforeWithCursor validateDiagnostics chooseFix = + checkFix server beforeWithCursor validateDiagnostics chooseFix (fun () -> Applicable) + + let checkNotApplicable server beforeWithCursor validateDiagnostics chooseFix = + checkFix server beforeWithCursor validateDiagnostics chooseFix (fun () -> NotApplicable) + + let matching cond (fixes: CodeAction array) = fixes |> Array.filter cond let withTitle title = matching (fun f -> f.Title = title) let ofKind kind = matching (fun f -> f.Kind = Some kind) @@ -225,18 +200,29 @@ module CodeFix = (expected: ExpectedResult) = Expect.isNonEmpty cursorRanges "No Range(s) specified" - ServerTests.documentTestList name server (Server.createUntitledDocument beforeWithoutCursor) (fun doc -> [ - for (i, range) in cursorRanges |> Seq.indexed do - let pos = - if range |> Range.isPosition then - range.Start.DebuggerDisplay - else - $"{range.Start.DebuggerDisplay}..{range.End.DebuggerDisplay}" - testCaseAsync $"Cursor {i} at {pos}" (async { - let! (doc, diags) = doc - do! checkFixAt (doc, diags) (beforeWithoutCursor, range) validateDiagnostics chooseFix expected - }) - ]) + + ServerTests.documentTestList name server (Server.createUntitledDocument beforeWithoutCursor) (fun doc -> + [ for (i, range) in cursorRanges |> Seq.indexed do + let pos = + if range |> Range.isPosition then + range.Start.DebuggerDisplay + else + $"{range.Start.DebuggerDisplay}..{range.End.DebuggerDisplay}" + + testCaseAsync + $"Cursor {i} at {pos}" + (async { + let! (doc, diags) = doc + + do! + checkFixAt + (doc, diags) + doc.VersionedTextDocumentIdentifier + (beforeWithoutCursor, range) + validateDiagnostics + chooseFix + expected + }) ]) /// One test for each Cursor. /// @@ -250,51 +236,18 @@ module CodeFix = (chooseFix: ChooseFix) (expected: unit -> ExpectedResult) = - let (beforeWithoutCursor, poss) = beforeWithCursors |> Text.trimTripleQuotation |> Cursors.extract + let (beforeWithoutCursor, poss) = + beforeWithCursors |> Text.trimTripleQuotation |> Cursors.extract + let ranges = poss |> List.map (fun p -> { Start = p; End = p }) - checkFixAll name server beforeWithoutCursor ranges validateDiagnostics chooseFix (expected()) - - let testAllPositions - name - server - beforeWithCursors - validateDiagnostics - chooseFix - expected - = - Test.checkAllPositions - name - server - beforeWithCursors - validateDiagnostics - chooseFix - (fun () -> After (expected |> Text.trimTripleQuotation)) - - let testApplicableAllPositions - name - server - beforeWithCursors - validateDiagnostics - chooseFix - = - Test.checkAllPositions - name - server - beforeWithCursors - validateDiagnostics - chooseFix - (fun () -> Applicable) - let testNotApplicableAllPositions - name - server - beforeWithCursors - validateDiagnostics - chooseFix - = - Test.checkAllPositions - name - server - beforeWithCursors - validateDiagnostics - chooseFix - (fun () -> NotApplicable) + checkFixAll name server beforeWithoutCursor ranges validateDiagnostics chooseFix (expected ()) + + let testAllPositions name server beforeWithCursors validateDiagnostics chooseFix expected = + Test.checkAllPositions name server beforeWithCursors validateDiagnostics chooseFix (fun () -> + After(expected |> Text.trimTripleQuotation)) + + let testApplicableAllPositions name server beforeWithCursors validateDiagnostics chooseFix = + Test.checkAllPositions name server beforeWithCursors validateDiagnostics chooseFix (fun () -> Applicable) + + let testNotApplicableAllPositions name server beforeWithCursors validateDiagnostics chooseFix = + Test.checkAllPositions name server beforeWithCursors validateDiagnostics chooseFix (fun () -> NotApplicable) diff --git a/test/FsAutoComplete.Tests.Lsp/Utils/CursorbasedTests.fsi b/test/FsAutoComplete.Tests.Lsp/Utils/CursorbasedTests.fsi index 901e1e9ad..0812f801e 100644 --- a/test/FsAutoComplete.Tests.Lsp/Utils/CursorbasedTests.fsi +++ b/test/FsAutoComplete.Tests.Lsp/Utils/CursorbasedTests.fsi @@ -1,13 +1,8 @@ module Utils.CursorbasedTests open Expecto -open Expecto.Diff open Ionide.LanguageServerProtocol.Types -open FsToolkit.ErrorHandling -open Utils.Utils open Utils.Server -open Utils.TextEdit -open Ionide.ProjInfo.Logging /// Checks for CodeFixes, CodeActions /// @@ -15,138 +10,139 @@ open Ionide.ProjInfo.Logging /// * `check`: Check to use inside a `testCaseAsync`. Not a Test itself! /// * `test`: Returns Expecto Test. Usually combines multiple tests (like: test all positions). module CodeFix = - /// Note: Return should be just ONE `CodeAction` (for Applicable) or ZERO `CodeAction` (for Not Applicable). - /// But actual return type is an array of `CodeAction`s: - /// * Easier to successive filter CodeActions down with simple pipe and `Array.filter` - /// * Returning `CodeAction option` would mean different filters for `check` (exactly one fix) and `checkNotApplicable` (exactly zero fix). - /// Both error with multiple matching fixes! - type ChooseFix = CodeAction[] -> CodeAction[] + /// Note: Return should be just ONE `CodeAction` (for Applicable) or ZERO `CodeAction` (for Not Applicable). + /// But actual return type is an array of `CodeAction`s: + /// * Easier to successive filter CodeActions down with simple pipe and `Array.filter` + /// * Returning `CodeAction option` would mean different filters for `check` (exactly one fix) and `checkNotApplicable` (exactly zero fix). + /// Both error with multiple matching fixes! + type ChooseFix = CodeAction[] -> CodeAction[] - type ExpectedResult = - | NotApplicable - | Applicable - | After of string + type ExpectedResult = + | NotApplicable + | Applicable + | After of string - val checkFixAt: - doc: Document * diagnostics: Diagnostic[] -> - beforeWithoutCursor: string * cursorRange: Range -> - validateDiagnostics: (Diagnostic[] -> unit) -> - chooseFix: ChooseFix -> - expected: ExpectedResult -> - Async - - /// Checks a CodeFix (CodeAction) for validity. - /// - /// * Extracts cursor position (`$0`) or range (between two `$0`) from `beforeWithCursor` - /// * Opens untitled Doc with source `beforeWithCursor` (with cursor removed) - /// * Note: untitled Document acts as Script file! - /// * Note: untitled Documents doesn't exist on disk! - /// * Waits for Diagnostics in that doc - /// * Filters Diags down to diags matching cursor position/range - /// * Then validates diags with `validateDiagnostics` - /// * Note: Validates filtered diags (-> only diags at cursor pos); not all diags in doc! - /// * Gets CodeFixes (CodeActions) from LSP server (`textDocument/codeAction`) for cursor range - /// * Request includes filtered diags - /// * Selects CodeFix from returned CodeFixes with `chooseFix` - /// * Note: `chooseFix` should return a single CodeFix. No CodeFix or multiple CodeFixes count as Failure! - /// * Use `checkNotApplicable` when there shouldn't be a CodeFix - /// * Note: Though `chooseFix` should return one CodeFix, the function actually returns an array of CodeFixes. - /// Reasons: - /// * Easier to filter down CodeFixes (`CodeFix.ofKind "..." >> CodeFix.withTitle "..."`) - /// * Better error messages: Can differentiate between no CodeFixes and too many CodeFixes - /// * Validates selected CodeFix: - /// * Applies selected CodeFix to source (`beforeWithCursor` with cursor removed) - /// * Compares result with `expected` - /// - /// Note: - /// `beforeWithCursor` as well as `expected` get trimmed with `Text.trimTripleQuotation`: Leading empty line and indentation gets removed. - /// - /// Note: - /// `beforeWithCursor` and `expected` MUST use `\n` for linebreaks -- using `\r` (either alone or as `\r\n`) results in test failure! - /// Linebreaks from edits in selected CodeFix are all transformed to just `\n` - /// -> CodeFix can use `\r` and `\r\n` - /// If you want to validate Line Endings of CodeFix, add a validation step to your `chooseFix` - val check: - server: CachedServer -> - beforeWithCursor: string -> - validateDiagnostics: (Diagnostic array -> unit) -> + val checkFixAt: + doc: Document * diagnostics: Diagnostic[] -> + editsFrom: VersionedTextDocumentIdentifier -> + beforeWithoutCursor: string * cursorRange: Range -> + validateDiagnostics: (Diagnostic[] -> unit) -> chooseFix: ChooseFix -> - expected: string -> - Async + expected: ExpectedResult -> + Async - /// Note: Doesn't apply Fix! Just checks its existence! - val checkApplicable: - server: CachedServer -> - beforeWithCursor: string -> - validateDiagnostics: (Diagnostic array -> unit) -> - chooseFix: ChooseFix -> - Async + /// Checks a CodeFix (CodeAction) for validity. + /// + /// * Extracts cursor position (`$0`) or range (between two `$0`) from `beforeWithCursor` + /// * Opens untitled Doc with source `beforeWithCursor` (with cursor removed) + /// * Note: untitled Document acts as Script file! + /// * Note: untitled Documents doesn't exist on disk! + /// * Waits for Diagnostics in that doc + /// * Filters Diags down to diags matching cursor position/range + /// * Then validates diags with `validateDiagnostics` + /// * Note: Validates filtered diags (-> only diags at cursor pos); not all diags in doc! + /// * Gets CodeFixes (CodeActions) from LSP server (`textDocument/codeAction`) for cursor range + /// * Request includes filtered diags + /// * Selects CodeFix from returned CodeFixes with `chooseFix` + /// * Note: `chooseFix` should return a single CodeFix. No CodeFix or multiple CodeFixes count as Failure! + /// * Use `checkNotApplicable` when there shouldn't be a CodeFix + /// * Note: Though `chooseFix` should return one CodeFix, the function actually returns an array of CodeFixes. + /// Reasons: + /// * Easier to filter down CodeFixes (`CodeFix.ofKind "..." >> CodeFix.withTitle "..."`) + /// * Better error messages: Can differentiate between no CodeFixes and too many CodeFixes + /// * Validates selected CodeFix: + /// * Applies selected CodeFix to source (`beforeWithCursor` with cursor removed) + /// * Compares result with `expected` + /// + /// Note: + /// `beforeWithCursor` as well as `expected` get trimmed with `Text.trimTripleQuotation`: Leading empty line and indentation gets removed. + /// + /// Note: + /// `beforeWithCursor` and `expected` MUST use `\n` for linebreaks -- using `\r` (either alone or as `\r\n`) results in test failure! + /// Linebreaks from edits in selected CodeFix are all transformed to just `\n` + /// -> CodeFix can use `\r` and `\r\n` + /// If you want to validate Line Endings of CodeFix, add a validation step to your `chooseFix` + val check: + server: CachedServer -> + beforeWithCursor: string -> + validateDiagnostics: (Diagnostic array -> unit) -> + chooseFix: ChooseFix -> + expected: string -> + Async - val checkNotApplicable: - server: CachedServer -> - beforeWithCursor: string -> - validateDiagnostics: (Diagnostic array -> unit) -> - chooseFix: ChooseFix -> - Async + /// Note: Doesn't apply Fix! Just checks its existence! + val checkApplicable: + server: CachedServer -> + beforeWithCursor: string -> + validateDiagnostics: (Diagnostic array -> unit) -> + chooseFix: ChooseFix -> + Async - val matching: cond: (CodeAction -> bool) -> fixes: CodeAction array -> CodeAction array - val withTitle: title: string -> (CodeAction array -> CodeAction array) - val ofKind: kind: string -> (CodeAction array -> CodeAction array) + val checkNotApplicable: + server: CachedServer -> + beforeWithCursor: string -> + validateDiagnostics: (Diagnostic array -> unit) -> + chooseFix: ChooseFix -> + Async - /// Bundled tests in Expecto test - module private Test = - /// One `testCaseAsync` for each cursorRange. - /// All test cases use same document (`ServerTests.documentTestList`) with source `beforeWithoutCursor`. - /// - /// Test names: - /// * `name` is name of outer test list. - /// * Each test case: `Cursor {i} at {pos or range}` - /// - /// Note: Sharing a common `Document` is just barely faster than using a new `Document` for each test (at least for simple source in `beforeWithoutCursor`). - val checkFixAll: - name: string -> - server: CachedServer -> - beforeWithoutCursor: string -> - cursorRanges: Range seq -> - validateDiagnostics: (Diagnostic[] -> unit) -> - chooseFix: ChooseFix -> - expected: ExpectedResult -> - Test + val matching: cond: (CodeAction -> bool) -> fixes: CodeAction array -> CodeAction array + val withTitle: title: string -> (CodeAction array -> CodeAction array) + val ofKind: kind: string -> (CodeAction array -> CodeAction array) - /// One test for each Cursor. - /// - /// Note: Tests single positions -> each `$0` gets checked. - /// -> Every test is for single-position range (`Start=End`)! - val checkAllPositions: - name: string -> - server: CachedServer -> - beforeWithCursors: string -> - validateDiagnostics: (Diagnostic[] -> unit) -> - chooseFix: ChooseFix -> - expected: (unit -> ExpectedResult) -> - Test + /// Bundled tests in Expecto test + module private Test = + /// One `testCaseAsync` for each cursorRange. + /// All test cases use same document (`ServerTests.documentTestList`) with source `beforeWithoutCursor`. + /// + /// Test names: + /// * `name` is name of outer test list. + /// * Each test case: `Cursor {i} at {pos or range}` + /// + /// Note: Sharing a common `Document` is just barely faster than using a new `Document` for each test (at least for simple source in `beforeWithoutCursor`). + val checkFixAll: + name: string -> + server: CachedServer -> + beforeWithoutCursor: string -> + cursorRanges: Range seq -> + validateDiagnostics: (Diagnostic[] -> unit) -> + chooseFix: ChooseFix -> + expected: ExpectedResult -> + Test - val testAllPositions: - name: string -> - server: CachedServer -> - beforeWithCursors: string -> - validateDiagnostics: (Diagnostic array -> unit) -> - chooseFix: ChooseFix -> - expected: string -> - Test + /// One test for each Cursor. + /// + /// Note: Tests single positions -> each `$0` gets checked. + /// -> Every test is for single-position range (`Start=End`)! + val checkAllPositions: + name: string -> + server: CachedServer -> + beforeWithCursors: string -> + validateDiagnostics: (Diagnostic[] -> unit) -> + chooseFix: ChooseFix -> + expected: (unit -> ExpectedResult) -> + Test - val testApplicableAllPositions: - name: string -> - server: CachedServer -> - beforeWithCursors: string -> - validateDiagnostics: (Diagnostic array -> unit) -> - chooseFix: ChooseFix -> - Test + val testAllPositions: + name: string -> + server: CachedServer -> + beforeWithCursors: string -> + validateDiagnostics: (Diagnostic array -> unit) -> + chooseFix: ChooseFix -> + expected: string -> + Test - val testNotApplicableAllPositions: - name: string -> - server: CachedServer -> - beforeWithCursors: string -> - validateDiagnostics: (Diagnostic array -> unit) -> - chooseFix: ChooseFix -> - Test + val testApplicableAllPositions: + name: string -> + server: CachedServer -> + beforeWithCursors: string -> + validateDiagnostics: (Diagnostic array -> unit) -> + chooseFix: ChooseFix -> + Test + + val testNotApplicableAllPositions: + name: string -> + server: CachedServer -> + beforeWithCursors: string -> + validateDiagnostics: (Diagnostic array -> unit) -> + chooseFix: ChooseFix -> + Test