Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 48 additions & 28 deletions src/FsAutoComplete/LspServers/AdaptiveFSharpLspServer.fs
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
87 changes: 86 additions & 1 deletion test/FsAutoComplete.Tests.Lsp/GoToTests.fs
Original file line number Diff line number Diff line change
Expand Up @@ -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 ]
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
module SignatureNavigation

let myFunction (x: int) = x + 1

type MyRecord = { Value: int }
Original file line number Diff line number Diff line change
@@ -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 }
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<Compile Include="SignatureNavigation.fsi" />
<Compile Include="SignatureNavigation.fs" />
</ItemGroup>
</Project>