From d622c03e4381637311adf13d79f4de9016b6da0a Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 9 Mar 2026 14:39:25 +0000 Subject: [PATCH 1/4] feat: enhance textDocument/implementation for .fsi <-> .fs navigation (fixes #1473) TextDocumentImplementation now supports bi-directional navigation between .fs implementation files and .fsi signature files via FCS-provided properties: - In a .fsi file: navigating to the .fs implementation uses symbol.ImplementationLocation (already available from FCS) - In a .fs file: navigating to the .fsi signature uses symbol.SignatureLocation (filtered to ensure it's actually a .fsi file) When neither location is available (e.g., no paired signature file, or the symbol is abstract/virtual), the existing dispatch-slot implementation lookup runs as a fallback, preserving existing behaviour. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../LspServers/AdaptiveFSharpLspServer.fs | 76 ++++++++++++------- 1 file changed, 48 insertions(+), 28 deletions(-) diff --git a/src/FsAutoComplete/LspServers/AdaptiveFSharpLspServer.fs b/src/FsAutoComplete/LspServers/AdaptiveFSharpLspServer.fs index 7250559ce..739b08360 100644 --- a/src/FsAutoComplete/LspServers/AdaptiveFSharpLspServer.fs +++ b/src/FsAutoComplete/LspServers/AdaptiveFSharpLspServer.fs @@ -1423,40 +1423,60 @@ type AdaptiveFSharpLspServer let! lineStr = tryGetLineStr pos volatileFile.Source |> Result.lineLookupErr and! tyRes = state.GetOpenFileTypeCheckResults filePath |> AsyncResult.ofStringErr - logger.info ( - Log.setMessage "TextDocumentImplementation Request: {params}" - >> Log.addContextDestructured "params" p - ) - - let getProjectOptions file = state.GetProjectOptionsForFile file |> AsyncResult.bimap id failwith //? Should we fail here? + // Check for .fsi <-> .fs navigation first. + // FCS exposes `SignatureLocation` (the .fsi location for a symbol in a .fs file) and + // `ImplementationLocation` (the .fs location for a symbol declared in a .fsi file). + // We use these to provide bi-directional navigation between implementation and signature files. + let sigOrImplLocation = + match tyRes.TryGetSymbolUse pos lineStr with + | Some symbolUse -> + let currentFileIsSignature = + (UMX.untag filePath).EndsWith(".fsi", System.StringComparison.OrdinalIgnoreCase) + + if currentFileIsSignature then + // In a .fsi file: navigate to the .fs implementation + symbolUse.Symbol.ImplementationLocation + else + // In a .fs file: navigate to the .fsi signature (if one exists) + symbolUse.Symbol.SignatureLocation + |> Option.filter (fun loc -> loc.FileName.EndsWith(".fsi", System.StringComparison.OrdinalIgnoreCase)) + | None -> None - let getUsesOfSymbol (filePath, opts: _ list, symbol: FSharpSymbol) = - state.GetUsesOfSymbol(filePath, opts, symbol) + match sigOrImplLocation with + | Some range -> + let loc = fcsRangeToLspLocation range + return Some(U2.C1(U2.C1 loc)) + | None -> - let getAllProjects () = - state.GetFilesToProject() - |> Async.map ( - Array.map (fun (file, proj) -> UMX.untag file, AVal.force proj.FSharpProjectCompilerOptions) - >> Array.toList - ) + let getProjectOptions file = state.GetProjectOptionsForFile file |> AsyncResult.bimap id failwith //? Should we fail here? - let! res = - Commands.symbolImplementationProject getProjectOptions getUsesOfSymbol getAllProjects tyRes pos lineStr - |> AsyncResult.ofCoreResponse + let getUsesOfSymbol (filePath, opts: _ list, symbol: FSharpSymbol) = + state.GetUsesOfSymbol(filePath, opts, symbol) - match res with - | None -> return None - | Some res -> - let ranges: FSharp.Compiler.Text.Range[] = - match res with - | LocationResponse.Use(_, uses) -> uses |> Array.map (fun u -> u.Range) + let getAllProjects () = + state.GetFilesToProject() + |> Async.map ( + Array.map (fun (file, proj) -> UMX.untag file, AVal.force proj.FSharpProjectCompilerOptions) + >> Array.toList + ) - let mappedRanges = ranges |> Array.map fcsRangeToLspLocation + let! res = + Commands.symbolImplementationProject getProjectOptions getUsesOfSymbol getAllProjects tyRes pos lineStr + |> AsyncResult.ofCoreResponse - match mappedRanges with - | [||] -> return None - | [| single |] -> return Some(U2.C1(U2.C1 single)) - | multiple -> return Some(U2.C1(U2.C2 multiple)) + match res with + | None -> return None + | Some res -> + let ranges: FSharp.Compiler.Text.Range[] = + match res with + | LocationResponse.Use(_, uses) -> uses |> Array.map (fun u -> u.Range) + + let mappedRanges = ranges |> Array.map fcsRangeToLspLocation + + match mappedRanges with + | [||] -> return None + | [| single |] -> return Some(U2.C1(U2.C1 single)) + | multiple -> return Some(U2.C1(U2.C2 multiple)) with e -> trace |> Tracing.recordException e From d1080ea19b67e94a6c7ebff51927899bb1a5fdde Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 9 Mar 2026 14:46:14 +0000 Subject: [PATCH 2/4] ci: trigger checks From 53e2c13fc1240433fa46aabf6cd53951abcf2db5 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 31 Mar 2026 04:42:27 +0000 Subject: [PATCH 3/4] Add tests for .fsi/.fs bi-directional navigation via TextDocumentImplementation Adds test cases for the feature introduced in PR #1494: - 'Go-to-implementation from .fsi navigates to .fs file': verifies that calling textDocument/implementation on a symbol in a .fsi file returns a location in the corresponding .fs implementation file. - 'Go-to-implementation from .fs navigates to .fsi signature file': verifies that calling textDocument/implementation on a symbol in a .fs file returns a location in the corresponding .fsi signature file. Test case project SignatureNavigation provides a minimal .fsi/.fs pair. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- test/FsAutoComplete.Tests.Lsp/GoToTests.fs | 87 ++++++++++++++++++- .../SignatureNavigation.fs | 5 ++ .../SignatureNavigation.fsi | 8 ++ .../SignatureNavigation.fsproj | 9 ++ 4 files changed, 108 insertions(+), 1 deletion(-) create mode 100644 test/FsAutoComplete.Tests.Lsp/TestCases/GoToTests/SignatureNavigation/SignatureNavigation.fs create mode 100644 test/FsAutoComplete.Tests.Lsp/TestCases/GoToTests/SignatureNavigation/SignatureNavigation.fsi create mode 100644 test/FsAutoComplete.Tests.Lsp/TestCases/GoToTests/SignatureNavigation/SignatureNavigation.fsproj diff --git a/test/FsAutoComplete.Tests.Lsp/GoToTests.fs b/test/FsAutoComplete.Tests.Lsp/GoToTests.fs index 68e24846e..ba7a1dd28 100644 --- a/test/FsAutoComplete.Tests.Lsp/GoToTests.fs +++ b/test/FsAutoComplete.Tests.Lsp/GoToTests.fs @@ -726,10 +726,95 @@ let private untitledGotoTests state = | Ok(_resultValue) -> failwith "Not Implemented" } ]) +let private signatureNavigationTests state = + let server = + async { + let path = + Path.Combine(__SOURCE_DIRECTORY__, "TestCases", "GoToTests", "SignatureNavigation") + + let! (server, event) = serverInitialize path defaultConfigDto state + do! waitForWorkspaceFinishedParsing event + + let fsiPath = Path.Combine(path, "SignatureNavigation.fsi") + let tdop: DidOpenTextDocumentParams = { TextDocument = loadDocument fsiPath } + do! server.TextDocumentDidOpen tdop + + let fsPath = Path.Combine(path, "SignatureNavigation.fs") + let tdop: DidOpenTextDocumentParams = { TextDocument = loadDocument fsPath } + do! server.TextDocumentDidOpen tdop + + do! + waitForParseResultsForFile "SignatureNavigation.fsi" event + |> AsyncResult.foldResult id (failtestf "%A") + + do! + waitForParseResultsForFile "SignatureNavigation.fs" event + |> AsyncResult.foldResult id (failtestf "%A") + + return server, fsiPath, fsPath + } + |> Async.Cache + + testList + "Signature File Navigation Tests" + [ testCaseAsync + "Go-to-implementation from .fsi navigates to .fs file" + (async { + let! server, fsiPath, _fsPath = server + + // `myFunction` is on line 3 at char 4 in SignatureNavigation.fsi: `val myFunction: ...` + let p: ImplementationParams = + { TextDocument = { Uri = Path.FilePathToUri fsiPath } + Position = { Line = 3u; Character = 4u } + WorkDoneToken = None + PartialResultToken = None } + + let! res = server.TextDocumentImplementation p + + match res with + | Error e -> failtestf "Request failed: %A" e + | Ok None -> failtest "Request returned None" + | Ok(Some(U2.C1(U2.C1 r))) -> + Expect.stringEnds r.Uri "SignatureNavigation.fs" "Should navigate to the .fs implementation file" + | Ok(Some(U2.C1(U2.C2 rs))) -> + let fsResult = + rs |> Array.tryFind (fun r -> r.Uri.EndsWith("SignatureNavigation.fs")) + + Expect.isSome fsResult "At least one result should point to the .fs file" + | Ok(_) -> failwith "Unexpected result format" + }) + testCaseAsync + "Go-to-implementation from .fs navigates to .fsi signature file" + (async { + let! server, _fsiPath, fsPath = server + + // `myFunction` is on line 2 at char 4 in SignatureNavigation.fs: `let myFunction ...` + let p: ImplementationParams = + { TextDocument = { Uri = Path.FilePathToUri fsPath } + Position = { Line = 2u; Character = 4u } + WorkDoneToken = None + PartialResultToken = None } + + let! res = server.TextDocumentImplementation p + + match res with + | Error e -> failtestf "Request failed: %A" e + | Ok None -> failtest "Request returned None" + | Ok(Some(U2.C1(U2.C1 r))) -> + Expect.stringEnds r.Uri "SignatureNavigation.fsi" "Should navigate to the .fsi signature file" + | Ok(Some(U2.C1(U2.C2 rs))) -> + let fsiResult = + rs |> Array.tryFind (fun r -> r.Uri.EndsWith("SignatureNavigation.fsi")) + + Expect.isSome fsiResult "At least one result should point to the .fsi file" + | Ok(_) -> failwith "Unexpected result format" + }) ] + let tests createServer = testSequenced <| testList "Go to definition tests" [ gotoTest createServer scriptGotoTests createServer - untitledGotoTests createServer ] + untitledGotoTests createServer + signatureNavigationTests createServer ] diff --git a/test/FsAutoComplete.Tests.Lsp/TestCases/GoToTests/SignatureNavigation/SignatureNavigation.fs b/test/FsAutoComplete.Tests.Lsp/TestCases/GoToTests/SignatureNavigation/SignatureNavigation.fs new file mode 100644 index 000000000..19688610f --- /dev/null +++ b/test/FsAutoComplete.Tests.Lsp/TestCases/GoToTests/SignatureNavigation/SignatureNavigation.fs @@ -0,0 +1,5 @@ +module SignatureNavigation + +let myFunction (x: int) = x + 1 + +type MyRecord = { Value: int } diff --git a/test/FsAutoComplete.Tests.Lsp/TestCases/GoToTests/SignatureNavigation/SignatureNavigation.fsi b/test/FsAutoComplete.Tests.Lsp/TestCases/GoToTests/SignatureNavigation/SignatureNavigation.fsi new file mode 100644 index 000000000..89792ba2e --- /dev/null +++ b/test/FsAutoComplete.Tests.Lsp/TestCases/GoToTests/SignatureNavigation/SignatureNavigation.fsi @@ -0,0 +1,8 @@ +module SignatureNavigation + +/// A simple function declared in the signature file. +val myFunction: x: int -> int + +/// A simple record type declared in the signature file. +type MyRecord = + { Value: int } diff --git a/test/FsAutoComplete.Tests.Lsp/TestCases/GoToTests/SignatureNavigation/SignatureNavigation.fsproj b/test/FsAutoComplete.Tests.Lsp/TestCases/GoToTests/SignatureNavigation/SignatureNavigation.fsproj new file mode 100644 index 000000000..899344b5a --- /dev/null +++ b/test/FsAutoComplete.Tests.Lsp/TestCases/GoToTests/SignatureNavigation/SignatureNavigation.fsproj @@ -0,0 +1,9 @@ + + + net8.0 + + + + + + From 3d209fb86e1a3f659e0f5c5109aebb155566e243 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 31 Mar 2026 04:42:28 +0000 Subject: [PATCH 4/4] ci: trigger checks