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 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 + + + + + +