Skip to content
Draft
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
68 changes: 65 additions & 3 deletions src/FsAutoComplete/LspServers/AdaptiveFSharpLspServer.fs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a need to convert the array into a list? (It looks like all it does is search the collection, which could use Array.tryFind rather than list.tryFind?)


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
Expand Down
3 changes: 2 additions & 1 deletion test/FsAutoComplete.Tests.Lsp/Program.fs
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand Down
127 changes: 127 additions & 0 deletions test/FsAutoComplete.Tests.Lsp/WorkspaceFolderTests.fs
Original file line number Diff line number Diff line change
@@ -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}"
} ]
Loading