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/2] 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/2] ci: trigger checks