From 553b1f686a6288267a7fa80e304ba0d183e959a3 Mon Sep 17 00:00:00 2001 From: nojaf Date: Tue, 5 Sep 2023 14:41:21 +0200 Subject: [PATCH 1/6] POC: update value in signature file. --- .../CodeFixes/ToInterpolatedString.fsi | 12 + .../CodeFixes/UpdateValueInSignatureFile.fs | 81 ++++++ .../CodeFixes/UpdateValueInSignatureFile.fsi | 6 + .../LspServers/AdaptiveServerState.fs | 3 +- .../RenameParamToMatchSignatureTests.fs | 6 +- .../CodeFixTests/Tests.fs | 3 +- .../UpdateValueInSignatureFileTests.fs | 68 +++++ .../Utils/CursorbasedTests.fs | 23 +- .../Utils/CursorbasedTests.fsi | 254 +++++++++--------- 9 files changed, 319 insertions(+), 137 deletions(-) create mode 100644 src/FsAutoComplete/CodeFixes/ToInterpolatedString.fsi create mode 100644 src/FsAutoComplete/CodeFixes/UpdateValueInSignatureFile.fs create mode 100644 src/FsAutoComplete/CodeFixes/UpdateValueInSignatureFile.fsi create mode 100644 test/FsAutoComplete.Tests.Lsp/CodeFixTests/UpdateValueInSignatureFileTests.fs 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/AdaptiveServerState.fs b/src/FsAutoComplete/LspServers/AdaptiveServerState.fs index f5d829512..c97ce7f52 100644 --- a/src/FsAutoComplete/LspServers/AdaptiveServerState.fs +++ b/src/FsAutoComplete/LspServers/AdaptiveServerState.fs @@ -1768,7 +1768,8 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac RenameParamToMatchSignature.fix tryGetParseResultsForFile RemovePatternArgument.fix tryGetParseResultsForFile ToInterpolatedString.fix tryGetParseResultsForFile getLanguageVersion - AdjustConstant.fix tryGetParseResultsForFile |]) + AdjustConstant.fix tryGetParseResultsForFile + UpdateValueInSignatureFile.fix tryGetParseResultsForFile |]) let forgetDocument (uri: DocumentUri) = async { diff --git a/test/FsAutoComplete.Tests.Lsp/CodeFixTests/RenameParamToMatchSignatureTests.fs b/test/FsAutoComplete.Tests.Lsp/CodeFixTests/RenameParamToMatchSignatureTests.fs index 89e4c4e75..2d67e6d79 100644 --- a/test/FsAutoComplete.Tests.Lsp/CodeFixTests/RenameParamToMatchSignatureTests.fs +++ b/test/FsAutoComplete.Tests.Lsp/CodeFixTests/RenameParamToMatchSignatureTests.fs @@ -13,7 +13,8 @@ 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 @@ -32,7 +33,7 @@ let tests state = fsSourceWithCursor |> Text.trimTripleQuotation |> Cursor.assertExtractRange let! (fsiDoc, diags) = server |> Server.openDocumentWithText fsiFile fsiSource - use _fsiDoc = fsiDoc + 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 @@ -40,6 +41,7 @@ let tests state = do! checkFixAt (fsDoc, diags) + fsDoc.VersionedTextDocumentIdentifier (fsSource, cursor) (Diagnostics.expectCode "3218") selectCodeFix diff --git a/test/FsAutoComplete.Tests.Lsp/CodeFixTests/Tests.fs b/test/FsAutoComplete.Tests.Lsp/CodeFixTests/Tests.fs index f18f62585..dfd80ea5a 100644 --- a/test/FsAutoComplete.Tests.Lsp/CodeFixTests/Tests.fs +++ b/test/FsAutoComplete.Tests.Lsp/CodeFixTests/Tests.fs @@ -3351,4 +3351,5 @@ let tests textFactory 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 1c99527f4..00fa8725f 100644 --- a/test/FsAutoComplete.Tests.Lsp/Utils/CursorbasedTests.fs +++ b/test/FsAutoComplete.Tests.Lsp/Utils/CursorbasedTests.fs @@ -1,7 +1,6 @@ module Utils.CursorbasedTests open Expecto -open Expecto.Diff open Ionide.LanguageServerProtocol.Types open FsToolkit.ErrorHandling open Utils.Utils @@ -34,6 +33,7 @@ module CodeFix = let checkFixAt (doc: Document, diagnostics: Diagnostic[]) + (editsFrom: VersionedTextDocumentIdentifier) (beforeWithoutCursor: string, cursorRange: Range) (validateDiagnostics: Diagnostic[] -> unit) (chooseFix: ChooseFix) @@ -100,7 +100,7 @@ module CodeFix = let edits = codeAction.Edit |> Option.defaultWith (fun _ -> failCodeFixTest "Code action doesn't contain any edits") - |> WorkspaceEdit.tryExtractTextEditsInSingleFile doc.VersionedTextDocumentIdentifier + |> WorkspaceEdit.tryExtractTextEditsInSingleFile editsFrom |> Result.valueOr failCodeFixTest // apply fix @@ -124,7 +124,14 @@ module CodeFix = 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. @@ -206,7 +213,15 @@ module CodeFix = $"Cursor {i} at {pos}" (async { let! (doc, diags) = doc - do! checkFixAt (doc, diags) (beforeWithoutCursor, range) validateDiagnostics chooseFix expected + + do! + checkFixAt + (doc, diags) + doc.VersionedTextDocumentIdentifier + (beforeWithoutCursor, range) + validateDiagnostics + chooseFix + expected }) ]) /// One test for each Cursor. 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 From 3f4ff4fc109ac2a9cd0b709f0a1456a635b7ef78 Mon Sep 17 00:00:00 2001 From: nojaf Date: Thu, 9 Nov 2023 17:03:40 +0100 Subject: [PATCH 2/6] Update checkFixAt usage --- .../CodeFixTests/AdjustConstantTests.fs | 18 ++++++++++++++++-- .../RenameParamToMatchSignatureTests.fs | 5 ++--- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/test/FsAutoComplete.Tests.Lsp/CodeFixTests/AdjustConstantTests.fs b/test/FsAutoComplete.Tests.Lsp/CodeFixTests/AdjustConstantTests.fs index fd9ce4a8a..e0f8c6339 100644 --- a/test/FsAutoComplete.Tests.Lsp/CodeFixTests/AdjustConstantTests.fs +++ b/test/FsAutoComplete.Tests.Lsp/CodeFixTests/AdjustConstantTests.fs @@ -47,7 +47,14 @@ module private ConvertIntToOtherBase = else ExpectedResult.After expected - do! checkFixAt (doc, diags) (source, cursor) Diagnostics.acceptAll (selectIntCodeFix base') expected + do! + checkFixAt + (doc, diags) + doc.VersionedTextDocumentIdentifier + (source, cursor) + Diagnostics.acceptAll + (selectIntCodeFix base') + expected }) /// empty `expectedXXX`: there should be no corresponding Fix @@ -941,7 +948,14 @@ module private ConvertCharToOtherForm = else ExpectedResult.After expected - do! checkFixAt (doc, diags) (source, cursor) Diagnostics.acceptAll (selectCharCodeFix (format)) expected + do! + checkFixAt + (doc, diags) + doc.VersionedTextDocumentIdentifier + (source, cursor) + Diagnostics.acceptAll + (selectCharCodeFix (format)) + expected }) let check diff --git a/test/FsAutoComplete.Tests.Lsp/CodeFixTests/RenameParamToMatchSignatureTests.fs b/test/FsAutoComplete.Tests.Lsp/CodeFixTests/RenameParamToMatchSignatureTests.fs index 2d67e6d79..e4f01db14 100644 --- a/test/FsAutoComplete.Tests.Lsp/CodeFixTests/RenameParamToMatchSignatureTests.fs +++ b/test/FsAutoComplete.Tests.Lsp/CodeFixTests/RenameParamToMatchSignatureTests.fs @@ -13,8 +13,7 @@ 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 @@ -33,7 +32,7 @@ let tests state = fsSourceWithCursor |> Text.trimTripleQuotation |> Cursor.assertExtractRange let! (fsiDoc, diags) = server |> Server.openDocumentWithText fsiFile fsiSource - use fsiDoc = fsiDoc + 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 From 57e57c89668b409c479faf5b05040641d3e110d6 Mon Sep 17 00:00:00 2001 From: nojaf Date: Thu, 9 Nov 2023 17:03:59 +0100 Subject: [PATCH 3/6] Use SyntaxVisitorBase to traverse the signature file. --- .../CodeFixes/UpdateValueInSignatureFile.fs | 124 +++++++++++------- 1 file changed, 77 insertions(+), 47 deletions(-) diff --git a/src/FsAutoComplete/CodeFixes/UpdateValueInSignatureFile.fs b/src/FsAutoComplete/CodeFixes/UpdateValueInSignatureFile.fs index 87e7fa4e3..fa0473f27 100644 --- a/src/FsAutoComplete/CodeFixes/UpdateValueInSignatureFile.fs +++ b/src/FsAutoComplete/CodeFixes/UpdateValueInSignatureFile.fs @@ -2,30 +2,31 @@ module FsAutoComplete.CodeFix.UpdateValueInSignatureFile open FSharp.Compiler.Symbols open FSharp.Compiler.Syntax +open FSharp.Compiler.Text 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 title = "Update val in signature file" -let visitParsedSigFileInput (name: string) (ParsedSigFileInput(contents = contents)) = - contents |> List.tryPick (visitSynModuleOrNamespaceSig name) +let assertPaths (sigPath: SyntaxVisitorPath) (implPath: SyntaxVisitorPath) = + let extractPath (path: SyntaxVisitorPath) = + path + |> List.collect (function + | SyntaxNode.SynModuleOrNamespace(SynModuleOrNamespace(longId = lid)) + | SyntaxNode.SynModule(SynModuleDecl.NestedModule(moduleInfo = SynComponentInfo(longId = lid))) + | SyntaxNode.SynModuleOrNamespaceSig(SynModuleOrNamespaceSig(longId = lid)) + | SyntaxNode.SynModuleSigDecl(SynModuleSigDecl.NestedModule(moduleInfo = SynComponentInfo(longId = lid))) -> lid + | _ -> []) + |> List.map (fun ident -> ident.idText) + |> String.concat "." -let visitTree (name: string) (tree: ParsedInput) = - match tree with - | ParsedInput.ImplFile _ -> None - | ParsedInput.SigFile parsedSigFileInput -> visitParsedSigFileInput name parsedSigFileInput + let impl = extractPath implPath + let sign = extractPath sigPath + impl = sign -let title = "Update val in signature file" let fix (getParseResultsForFile: GetParseResultsForFile) : CodeFix = Run.ifDiagnosticByCode (Set.ofList [ "34" ]) (fun diagnostic codeActionParams -> @@ -39,43 +40,72 @@ let fix (getParseResultsForFile: GetParseResultsForFile) : CodeFix = let sigTextDocumentIdentifier: TextDocumentIdentifier = { Uri = $"%s{codeActionParams.TextDocument.Uri}i" } - let! (implParseAndCheckResults: ParseAndCheckResults, implLine: string, implSourceText: IFSACSourceText) = + let! (implParseAndCheckResults: ParseAndCheckResults, implLine: string, _implSourceText: IFSACSourceText) = getParseResultsForFile implFileName (protocolPosToPos diagnostic.Range.Start) - let! implBindingName = - implSourceText.GetText(protocolRangeToRange implParseAndCheckResults.GetParseResults.FileName diagnostic.Range) + let mDiag = + protocolRangeToRange implParseAndCheckResults.GetParseResults.FileName diagnostic.Range - let! (sigParseAndCheckResults: ParseAndCheckResults, _sigLine: string, _sigSourceText: IFSACSourceText) = - getParseResultsForFile sigFileName (protocolPosToPos diagnostic.Range.Start) + // Find the binding name in the implementation file. + let impVisitor = + { new SyntaxVisitorBase<_>() with + override x.VisitBinding(path, defaultTraverse, SynBinding(headPat = pat)) = + match pat with + | SynPat.LongIdent(longDotId = SynLongIdent(id = [ ident ])) when Range.equals mDiag ident.idRange -> + Some(ident, path) + | _ -> None } - match visitTree implBindingName sigParseAndCheckResults.GetParseResults.ParseTree with + match SyntaxTraversal.Traverse(mDiag.Start, implParseAndCheckResults.GetParseResults.ParseTree, impVisitor) 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 + | Some(implBindingIdent, implPath) -> + + // Find a matching val in the signature file. + let sigVisitor = + { new SyntaxVisitorBase<_>() with + override x.VisitValSig(path, defaultTraverse, SynValSig(ident = SynIdent(ident, _); range = mValSig)) = + if ident.idText = implBindingIdent.idText then + Some(mValSig, path) + else + None } + + let! (sigParseAndCheckResults: ParseAndCheckResults, _sigLine: string, _sigSourceText: IFSACSourceText) = + getParseResultsForFile sigFileName (protocolPosToPos diagnostic.Range.Start) + + match + SyntaxTraversal.Traverse(Position.pos0, sigParseAndCheckResults.GetParseResults.ParseTree, sigVisitor) + with | None -> return [] - | Some symbolUse -> - match symbolUse.Symbol with - | :? FSharpMemberOrFunctionOrValue as mfv -> - match mfv.GetValSignatureText(symbolUse.DisplayContext, symbolUse.Range) with + | Some(mValSig, sigPath) -> + + // Verify both nodes share the same path. + if not (assertPaths sigPath implPath) then + return [] + else + let endPos = implBindingIdent.idRange.End + + let symbolUse = + implParseAndCheckResults.GetCheckResults.GetSymbolUseAtLocation( + endPos.Line, + endPos.Column, + implLine, + [ implBindingIdent.idText ] + ) + + match symbolUse with | None -> return [] - | Some valText -> - return - [ { SourceDiagnostic = None - Title = title - File = sigTextDocumentIdentifier - Edits = - [| { Range = fcsRangeToLsp mVal - NewText = valText } |] - Kind = FixKind.Fix } ] - | _ -> 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 mValSig + NewText = valText } |] + Kind = FixKind.Fix } ] + | _ -> return [] }) From 449be942ae4fcfb98e71864356814246b61de18e Mon Sep 17 00:00:00 2001 From: nojaf Date: Fri, 10 Nov 2023 17:14:05 +0100 Subject: [PATCH 4/6] Improve code: find range of val in signature file. Use correct display context. --- .../CodeFixes/UpdateValueInSignatureFile.fs | 120 +++++++++++------- 1 file changed, 74 insertions(+), 46 deletions(-) diff --git a/src/FsAutoComplete/CodeFixes/UpdateValueInSignatureFile.fs b/src/FsAutoComplete/CodeFixes/UpdateValueInSignatureFile.fs index fa0473f27..62eace3dd 100644 --- a/src/FsAutoComplete/CodeFixes/UpdateValueInSignatureFile.fs +++ b/src/FsAutoComplete/CodeFixes/UpdateValueInSignatureFile.fs @@ -58,54 +58,82 @@ let fix (getParseResultsForFile: GetParseResultsForFile) : CodeFix = match SyntaxTraversal.Traverse(mDiag.Start, implParseAndCheckResults.GetParseResults.ParseTree, impVisitor) with | None -> return [] | Some(implBindingIdent, implPath) -> + let endPos = implBindingIdent.idRange.End - // Find a matching val in the signature file. - let sigVisitor = - { new SyntaxVisitorBase<_>() with - override x.VisitValSig(path, defaultTraverse, SynValSig(ident = SynIdent(ident, _); range = mValSig)) = - if ident.idText = implBindingIdent.idText then - Some(mValSig, path) - else - None } - - let! (sigParseAndCheckResults: ParseAndCheckResults, _sigLine: string, _sigSourceText: IFSACSourceText) = - getParseResultsForFile sigFileName (protocolPosToPos diagnostic.Range.Start) - - match - SyntaxTraversal.Traverse(Position.pos0, sigParseAndCheckResults.GetParseResults.ParseTree, sigVisitor) - with + let symbolUse = + implParseAndCheckResults.GetCheckResults.GetSymbolUseAtLocation( + endPos.Line, + endPos.Column, + implLine, + [ implBindingIdent.idText ] + ) + + match symbolUse with | None -> return [] - | Some(mValSig, sigPath) -> - - // Verify both nodes share the same path. - if not (assertPaths sigPath implPath) then - return [] - else - let endPos = implBindingIdent.idRange.End - - let symbolUse = - implParseAndCheckResults.GetCheckResults.GetSymbolUseAtLocation( - endPos.Line, - endPos.Column, - implLine, - [ implBindingIdent.idText ] - ) - - match symbolUse with + | Some implSymbolUse -> + + match implSymbolUse.Symbol with + | :? FSharpMemberOrFunctionOrValue as mfv when mfv.SignatureLocation.IsSome -> + let mSig = mfv.SignatureLocation.Value + + // Find a matching val in the signature file. + let sigVisitor = + { new SyntaxVisitorBase<_>() with + override x.VisitModuleSigDecl + ( + path: SyntaxVisitorPath, + defaultTraverse, + synModuleSigDecl: SynModuleSigDecl + ) = + defaultTraverse synModuleSigDecl + + override x.VisitValSig + ( + path, + defaultTraverse, + SynValSig(ident = SynIdent(ident, _); range = mValSig) + ) = + if ident.idText = implBindingIdent.idText then + Some(mValSig, path) + else + None } + + let! (sigParseAndCheckResults: ParseAndCheckResults, sigLine: string, _sigSourceText: IFSACSourceText) = + getParseResultsForFile sigFileName mSig.End + + match + SyntaxTraversal.Traverse(mSig.End, sigParseAndCheckResults.GetParseResults.ParseTree, sigVisitor) + with | None -> return [] - | Some symbolUse -> - match symbolUse.Symbol with - | :? FSharpMemberOrFunctionOrValue as mfv -> - match mfv.GetValSignatureText(symbolUse.DisplayContext, symbolUse.Range) with + | Some(mValSig, sigPath) -> + + // Verify both nodes share the same path. + if not (assertPaths sigPath implPath) then + return [] + else + // Find matching symbol in signature file, we need it for its DisplayContext + let sigSymbolUseOpt = + sigParseAndCheckResults.GetCheckResults.GetSymbolUseAtLocation( + mSig.End.Line, + mSig.End.Column, + sigLine, + [ implBindingIdent.idText ] + ) + + match sigSymbolUseOpt with | None -> return [] - | Some valText -> - return - [ { SourceDiagnostic = None - Title = title - File = sigTextDocumentIdentifier - Edits = - [| { Range = fcsRangeToLsp mValSig - NewText = valText } |] - Kind = FixKind.Fix } ] - | _ -> return [] + | Some sigSymbolUse -> + + match mfv.GetValSignatureText(sigSymbolUse.DisplayContext, implSymbolUse.Range) with + | None -> return [] + | Some valText -> + return + [ { SourceDiagnostic = None + Title = title + File = sigTextDocumentIdentifier + Edits = + [| { Range = fcsRangeToLsp mValSig + NewText = valText } |] + Kind = FixKind.Fix } ] + | _ -> return [] }) From aeb699fc3392b26c6e4ae4cace88c9e80785efc7 Mon Sep 17 00:00:00 2001 From: nojaf Date: Fri, 10 Nov 2023 17:19:57 +0100 Subject: [PATCH 5/6] Add some more pointers. --- .../CodeFixes/UpdateValueInSignatureFile.fs | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/src/FsAutoComplete/CodeFixes/UpdateValueInSignatureFile.fs b/src/FsAutoComplete/CodeFixes/UpdateValueInSignatureFile.fs index 62eace3dd..00e8c4fc3 100644 --- a/src/FsAutoComplete/CodeFixes/UpdateValueInSignatureFile.fs +++ b/src/FsAutoComplete/CodeFixes/UpdateValueInSignatureFile.fs @@ -79,14 +79,6 @@ let fix (getParseResultsForFile: GetParseResultsForFile) : CodeFix = // Find a matching val in the signature file. let sigVisitor = { new SyntaxVisitorBase<_>() with - override x.VisitModuleSigDecl - ( - path: SyntaxVisitorPath, - defaultTraverse, - synModuleSigDecl: SynModuleSigDecl - ) = - defaultTraverse synModuleSigDecl - override x.VisitValSig ( path, @@ -108,10 +100,15 @@ let fix (getParseResultsForFile: GetParseResultsForFile) : CodeFix = | Some(mValSig, sigPath) -> // Verify both nodes share the same path. + // TODO: not sure how relevant this check still is. + // Using the mfv.SignatureLocation is probably already enough to pinpoint the correct val. + // This check was introduced to verify vals with the same name (but in different nested modules) are not getting mixed up. if not (assertPaths sigPath implPath) then return [] else // Find matching symbol in signature file, we need it for its DisplayContext + // GetValSignatureText will take the open namespaces into account when printing the types. + // The implementation file might have different opens than the signature. let sigSymbolUseOpt = sigParseAndCheckResults.GetCheckResults.GetSymbolUseAtLocation( mSig.End.Line, From 81a3fb9076b17023a7f3a5a49c2fe70161dbb7ab Mon Sep 17 00:00:00 2001 From: nojaf Date: Fri, 22 Dec 2023 09:17:30 +0100 Subject: [PATCH 6/6] Use extended data in codefix! --- .../CodeFixes/UpdateValueInSignatureFile.fs | 158 +++++++----------- .../UpdateValueInSignatureFileTests.fs | 2 +- 2 files changed, 63 insertions(+), 97 deletions(-) diff --git a/src/FsAutoComplete/CodeFixes/UpdateValueInSignatureFile.fs b/src/FsAutoComplete/CodeFixes/UpdateValueInSignatureFile.fs index 00e8c4fc3..f3cddb442 100644 --- a/src/FsAutoComplete/CodeFixes/UpdateValueInSignatureFile.fs +++ b/src/FsAutoComplete/CodeFixes/UpdateValueInSignatureFile.fs @@ -1,6 +1,7 @@ module FsAutoComplete.CodeFix.UpdateValueInSignatureFile -open FSharp.Compiler.Symbols +open FSharp.Compiler.Diagnostics +open FSharp.Compiler.Diagnostics.ExtendedData open FSharp.Compiler.Syntax open FSharp.Compiler.Text open FsToolkit.ErrorHandling @@ -9,24 +10,9 @@ open FsAutoComplete.CodeFix.Types open FsAutoComplete open FsAutoComplete.LspHelpers -let title = "Update val in signature file" - -let assertPaths (sigPath: SyntaxVisitorPath) (implPath: SyntaxVisitorPath) = - let extractPath (path: SyntaxVisitorPath) = - path - |> List.collect (function - | SyntaxNode.SynModuleOrNamespace(SynModuleOrNamespace(longId = lid)) - | SyntaxNode.SynModule(SynModuleDecl.NestedModule(moduleInfo = SynComponentInfo(longId = lid))) - | SyntaxNode.SynModuleOrNamespaceSig(SynModuleOrNamespaceSig(longId = lid)) - | SyntaxNode.SynModuleSigDecl(SynModuleSigDecl.NestedModule(moduleInfo = SynComponentInfo(longId = lid))) -> lid - | _ -> []) - |> List.map (fun ident -> ident.idText) - |> String.concat "." - - let impl = extractPath implPath - let sign = extractPath sigPath - impl = sign +#nowarn "57" +let title = "Update val in signature file" let fix (getParseResultsForFile: GetParseResultsForFile) : CodeFix = Run.ifDiagnosticByCode (Set.ofList [ "34" ]) (fun diagnostic codeActionParams -> @@ -46,91 +32,71 @@ let fix (getParseResultsForFile: GetParseResultsForFile) : CodeFix = let mDiag = protocolRangeToRange implParseAndCheckResults.GetParseResults.FileName diagnostic.Range + let! extendedDiagnosticData = + implParseAndCheckResults.GetCheckResults.Diagnostics + |> Array.tryPick (fun (diag: FSharpDiagnostic) -> + if diag.ErrorNumber <> 34 || not (Range.equals diag.Range mDiag) then + None + else + match diag.ExtendedData with + | Some(:? ValueNotContainedDiagnosticExtendedData as extendedData) -> Some extendedData + | _ -> None) + |> Result.ofOption (fun () -> "No extended data") + // Find the binding name in the implementation file. let impVisitor = { new SyntaxVisitorBase<_>() with override x.VisitBinding(path, defaultTraverse, SynBinding(headPat = pat)) = match pat with | SynPat.LongIdent(longDotId = SynLongIdent(id = [ ident ])) when Range.equals mDiag ident.idRange -> - Some(ident, path) + Some ident | _ -> None } - match SyntaxTraversal.Traverse(mDiag.Start, implParseAndCheckResults.GetParseResults.ParseTree, impVisitor) with - | None -> return [] - | Some(implBindingIdent, implPath) -> - let endPos = implBindingIdent.idRange.End - - let symbolUse = - implParseAndCheckResults.GetCheckResults.GetSymbolUseAtLocation( - endPos.Line, - endPos.Column, - implLine, - [ implBindingIdent.idText ] - ) - - match symbolUse with - | None -> return [] - | Some implSymbolUse -> - - match implSymbolUse.Symbol with - | :? FSharpMemberOrFunctionOrValue as mfv when mfv.SignatureLocation.IsSome -> - let mSig = mfv.SignatureLocation.Value - - // Find a matching val in the signature file. - let sigVisitor = - { new SyntaxVisitorBase<_>() with - override x.VisitValSig - ( - path, - defaultTraverse, - SynValSig(ident = SynIdent(ident, _); range = mValSig) - ) = - if ident.idText = implBindingIdent.idText then - Some(mValSig, path) - else - None } - - let! (sigParseAndCheckResults: ParseAndCheckResults, sigLine: string, _sigSourceText: IFSACSourceText) = - getParseResultsForFile sigFileName mSig.End - - match - SyntaxTraversal.Traverse(mSig.End, sigParseAndCheckResults.GetParseResults.ParseTree, sigVisitor) - with - | None -> return [] - | Some(mValSig, sigPath) -> - - // Verify both nodes share the same path. - // TODO: not sure how relevant this check still is. - // Using the mfv.SignatureLocation is probably already enough to pinpoint the correct val. - // This check was introduced to verify vals with the same name (but in different nested modules) are not getting mixed up. - if not (assertPaths sigPath implPath) then - return [] + let! implBindingIdent = + SyntaxTraversal.Traverse(mDiag.Start, implParseAndCheckResults.GetParseResults.ParseTree, impVisitor) + |> Result.ofOption (fun () -> "No binding name found") + + let endPos = implBindingIdent.idRange.End + + let! symbolUse = + implParseAndCheckResults.GetCheckResults.GetSymbolUseAtLocation( + endPos.Line, + endPos.Column, + implLine, + [ implBindingIdent.idText ] + ) + |> Result.ofOption (fun () -> "No symbolUse found") + + let! valText = + extendedDiagnosticData.ImplementationValue.GetValSignatureText(symbolUse.DisplayContext, symbolUse.Range) + |> Result.ofOption (fun () -> "No val text found.") + + // Find a matching val in the signature file. + let sigVisitor = + { new SyntaxVisitorBase<_>() with + override x.VisitValSig(path, defaultTraverse, SynValSig(range = mValSig)) = + if Range.rangeContainsRange mValSig extendedDiagnosticData.SignatureValue.DeclarationLocation then + Some mValSig else - // Find matching symbol in signature file, we need it for its DisplayContext - // GetValSignatureText will take the open namespaces into account when printing the types. - // The implementation file might have different opens than the signature. - let sigSymbolUseOpt = - sigParseAndCheckResults.GetCheckResults.GetSymbolUseAtLocation( - mSig.End.Line, - mSig.End.Column, - sigLine, - [ implBindingIdent.idText ] - ) - - match sigSymbolUseOpt with - | None -> return [] - | Some sigSymbolUse -> - - match mfv.GetValSignatureText(sigSymbolUse.DisplayContext, implSymbolUse.Range) with - | None -> return [] - | Some valText -> - return - [ { SourceDiagnostic = None - Title = title - File = sigTextDocumentIdentifier - Edits = - [| { Range = fcsRangeToLsp mValSig - NewText = valText } |] - Kind = FixKind.Fix } ] - | _ -> return [] + None } + + let! (sigParseAndCheckResults: ParseAndCheckResults, _sigLine: string, _sigSourceText: IFSACSourceText) = + getParseResultsForFile sigFileName extendedDiagnosticData.SignatureValue.DeclarationLocation.End + + let! mVal = + SyntaxTraversal.Traverse( + extendedDiagnosticData.SignatureValue.DeclarationLocation.End, + sigParseAndCheckResults.GetParseResults.ParseTree, + sigVisitor + ) + |> Result.ofOption (fun () -> "No val range found in signature file") + + return + [ { SourceDiagnostic = None + Title = title + File = sigTextDocumentIdentifier + Edits = + [| { Range = fcsRangeToLsp mVal + NewText = valText } |] + Kind = FixKind.Fix } ] }) diff --git a/test/FsAutoComplete.Tests.Lsp/CodeFixTests/UpdateValueInSignatureFileTests.fs b/test/FsAutoComplete.Tests.Lsp/CodeFixTests/UpdateValueInSignatureFileTests.fs index 47fe99699..4b3a19273 100644 --- a/test/FsAutoComplete.Tests.Lsp/CodeFixTests/UpdateValueInSignatureFileTests.fs +++ b/test/FsAutoComplete.Tests.Lsp/CodeFixTests/UpdateValueInSignatureFileTests.fs @@ -46,7 +46,7 @@ let tests state = serverTestList (nameof UpdateValueInSignatureFile) state defaultConfigDto (Some path) (fun server -> [ let selectCodeFix = CodeFix.withTitle UpdateValueInSignatureFile.title - ftestCaseAsync "first unit test for UpdateValueInSignatureFile" + testCaseAsync "first unit test for UpdateValueInSignatureFile" <| checkWithFsi server """