diff --git a/src/FsAutoComplete/LspServers/AdaptiveFSharpLspServer.fs b/src/FsAutoComplete/LspServers/AdaptiveFSharpLspServer.fs index 4f3d90917..36fb0920b 100644 --- a/src/FsAutoComplete/LspServers/AdaptiveFSharpLspServer.fs +++ b/src/FsAutoComplete/LspServers/AdaptiveFSharpLspServer.fs @@ -358,10 +358,72 @@ type AdaptiveFSharpLspServer | Some false -> None | None -> None + // Stage 1 & 2: Use workspaceFolders instead of deprecated RootPath/RootUri + // When AutomaticWorkspaceInit is enabled, probe workspace folders for F# code let actualRootPath = - match p.RootUri with - | Some rootUri -> Some(Path.FileUriToLocalPath rootUri) - | None -> p.RootPath + // Check if we have deprecated fields (for backward compatibility) + let deprecatedPath = + match p.RootUri with + | Some rootUri -> Some(Path.FileUriToLocalPath rootUri) + | None -> p.RootPath + + match p.WorkspaceFolders, c.AutomaticWorkspaceInit, deprecatedPath with + // When we have workspace folders and AutomaticWorkspaceInit is enabled, probe them + | Some(NonEmptyArray folders), true, _ -> + let folderPaths = + folders |> Array.map (fun f -> Path.FileUriToLocalPath f.Uri) |> Array.toList + + logger.info ( + Log.setMessage "Probing workspace folders for F# code: {folders}" + >> Log.addContextDestructured "folders" folderPaths + ) + + // Stage 2: Find first folder with F# projects + let firstFolderWithFSharp = + folderPaths + |> List.tryFind (fun folderPath -> + let peeks = + WorkspacePeek.peek + folderPath + c.WorkspaceModePeekDeepLevel + (c.ExcludeProjectDirectories |> List.ofArray) + + not (List.isEmpty peeks)) + + match firstFolderWithFSharp with + | Some path -> + logger.info ( + Log.setMessage "Selected workspace folder with F# code: {path}" + >> Log.addContextDestructured "path" path + ) + + Some path + | None -> + // No F# code found, use first folder + logger.info (Log.setMessage "No F# code found in any workspace folder, using first") + Some folderPaths.Head + // When we have workspace folders but AutomaticWorkspaceInit is false, prefer deprecated fields if available + | Some(NonEmptyArray _folders), false, Some deprecatedPath -> + logger.info ( + Log.setMessage + "Using deprecated RootPath/RootUri (AutomaticWorkspaceInit disabled, deprecated fields present)" + ) + + Some deprecatedPath + // When we have workspace folders, AutomaticWorkspaceInit is false, and no deprecated fields, use first workspace folder + | Some(NonEmptyArray folders), false, None -> + let folderPath = Path.FileUriToLocalPath folders.[0].Uri + + logger.info ( + Log.setMessage "Using first workspace folder (AutomaticWorkspaceInit disabled, no deprecated fields)" + ) + + Some folderPath + // No workspace folders, use deprecated fields + | Some EmptyArray, _, _ + | None, _, _ -> + logger.info (Log.setMessage "Using deprecated RootPath/RootUri (no workspace folders)") + deprecatedPath let projs = match actualRootPath, c.AutomaticWorkspaceInit with diff --git a/test/FsAutoComplete.Tests.Lsp/Program.fs b/test/FsAutoComplete.Tests.Lsp/Program.fs index a495a7ff9..2ec86cb0c 100644 --- a/test/FsAutoComplete.Tests.Lsp/Program.fs +++ b/test/FsAutoComplete.Tests.Lsp/Program.fs @@ -138,7 +138,8 @@ let lspTests = CallHierarchy.tests createServer diagnosticsTest createServer - TestExplorer.tests createServer ] ] ] + TestExplorer.tests createServer + WorkspaceFolderTests.tests createServer ] ] ] /// Tests that do not require a LSP server let generalTests = diff --git a/test/FsAutoComplete.Tests.Lsp/WorkspaceFolderTests.fs b/test/FsAutoComplete.Tests.Lsp/WorkspaceFolderTests.fs new file mode 100644 index 000000000..ce8789150 --- /dev/null +++ b/test/FsAutoComplete.Tests.Lsp/WorkspaceFolderTests.fs @@ -0,0 +1,127 @@ +module FsAutoComplete.Tests.WorkspaceFolderTests + +open Expecto +open System.IO +open Ionide.LanguageServerProtocol +open Ionide.LanguageServerProtocol.Types +open FsAutoComplete +open FsAutoComplete.Lsp +open FsAutoComplete.LspHelpers +open Helpers +open Utils.Server + +/// Tests for workspaceFolders initialization (stages 1 & 2) +let tests createServer = + testList + "WorkspaceFolder Tests" + [ testCaseAsync "Single workspace folder is selected correctly" + <| async { + let testDir = Path.Combine(__SOURCE_DIRECTORY__, "TestCases", "ServerTests") + let (server: IFSharpLspServer), _events = createServer () + + let p: InitializeParams = + { ProcessId = Some 1 + RootPath = None + Locale = None + RootUri = None + InitializationOptions = Some(Server.serialize defaultConfigDto) + Capabilities = clientCaps + ClientInfo = Some { Name = "Test"; Version = Some "1.0" } + WorkspaceFolders = Some [| { Uri = (Path.FilePathToUri testDir); Name = "Test" } |] + Trace = None + WorkDoneToken = None } + + match! server.Initialize p with + | Ok _ -> () + | Error e -> failtest $"Initialize failed: {e}" + } + + testCaseAsync "Multiple workspace folders - selects first with F# code" + <| async { + let testDir = Path.Combine(__SOURCE_DIRECTORY__, "TestCases", "ServerTests") + let emptyDir = Path.GetTempPath() + let (server: IFSharpLspServer), _events = createServer () + + // First folder has no F# code, second has F# projects + let p: InitializeParams = + { ProcessId = Some 1 + RootPath = None + Locale = None + RootUri = None + InitializationOptions = Some(Server.serialize defaultConfigDto) + Capabilities = clientCaps + ClientInfo = Some { Name = "Test"; Version = Some "1.0" } + WorkspaceFolders = Some [| { Uri = (Path.FilePathToUri emptyDir); Name = "Empty" }; { Uri = (Path.FilePathToUri testDir); Name = "Test" } |] + Trace = None + WorkDoneToken = None } + + match! server.Initialize p with + | Ok _ -> () + | Error e -> failtest $"Initialize failed: {e}" + } + + testCaseAsync "Backward compatibility - uses RootUri when WorkspaceFolders is None" + <| async { + let testDir = Path.Combine(__SOURCE_DIRECTORY__, "TestCases", "ServerTests") + let (server: IFSharpLspServer), _events = createServer () + + let p: InitializeParams = + { ProcessId = Some 1 + RootPath = None + Locale = None + RootUri = Some(sprintf "file://%s" testDir) + InitializationOptions = Some(Server.serialize defaultConfigDto) + Capabilities = clientCaps + ClientInfo = Some { Name = "Test"; Version = Some "1.0" } + WorkspaceFolders = None + Trace = None + WorkDoneToken = None } + + match! server.Initialize p with + | Ok _ -> () + | Error e -> failtest $"Initialize failed: {e}" + } + + testCaseAsync "Backward compatibility - uses RootPath when both RootUri and WorkspaceFolders are None" + <| async { + let testDir = Path.Combine(__SOURCE_DIRECTORY__, "TestCases", "ServerTests") + let (server: IFSharpLspServer), _events = createServer () + + let p: InitializeParams = + { ProcessId = Some 1 + RootPath = Some testDir + Locale = None + RootUri = None + InitializationOptions = Some(Server.serialize defaultConfigDto) + Capabilities = clientCaps + ClientInfo = Some { Name = "Test"; Version = Some "1.0" } + WorkspaceFolders = None + Trace = None + WorkDoneToken = None } + + match! server.Initialize p with + | Ok _ -> () + | Error e -> failtest $"Initialize failed: {e}" + } + + testCaseAsync "Empty WorkspaceFolders array falls back to RootUri" + <| async { + let testDir = Path.Combine(__SOURCE_DIRECTORY__, "TestCases", "ServerTests") + let (server: IFSharpLspServer), _events = createServer () + + let p: InitializeParams = + { ProcessId = Some 1 + RootPath = None + Locale = None + RootUri = Some(sprintf "file://%s" testDir) + InitializationOptions = Some(Server.serialize defaultConfigDto) + Capabilities = clientCaps + ClientInfo = Some { Name = "Test"; Version = Some "1.0" } + WorkspaceFolders = Some [||] + Trace = None + WorkDoneToken = None } + + match! server.Initialize p with + | Ok _ -> () + | Error e -> failtest $"Initialize failed: {e}" + } ]