From e2834009fd5f102f637703afe3b649a5fccb182e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Saulius=20Menkevi=C4=8Dius?= Date: Tue, 8 Jul 2025 14:31:59 +0300 Subject: [PATCH 1/5] Server does not implement workspaceSymbol/resolve (yet) --- src/CSharpLanguageServer/Handlers/WorkspaceSymbol.fs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/CSharpLanguageServer/Handlers/WorkspaceSymbol.fs b/src/CSharpLanguageServer/Handlers/WorkspaceSymbol.fs index 771347ad..3131a32e 100644 --- a/src/CSharpLanguageServer/Handlers/WorkspaceSymbol.fs +++ b/src/CSharpLanguageServer/Handlers/WorkspaceSymbol.fs @@ -28,7 +28,7 @@ module WorkspaceSymbol = | false -> None | true -> let registerOptions: WorkspaceSymbolRegistrationOptions = - { ResolveProvider = Some true + { ResolveProvider = None WorkDoneProgress = None } From f056b851dc62d4cd11d3f806f6a222dc11d5586c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Saulius=20Menkevi=C4=8Dius?= Date: Tue, 8 Jul 2025 14:35:39 +0300 Subject: [PATCH 2/5] Reduce noise from applyWorkspaceTargetFrameworkProp when failing to load csprojs --- src/CSharpLanguageServer/RoslynHelpers.fs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/CSharpLanguageServer/RoslynHelpers.fs b/src/CSharpLanguageServer/RoslynHelpers.fs index 0fa4f895..e92c6b61 100644 --- a/src/CSharpLanguageServer/RoslynHelpers.fs +++ b/src/CSharpLanguageServer/RoslynHelpers.fs @@ -471,7 +471,7 @@ let applyWorkspaceTargetFrameworkProp (logger: ILog) (projs: string seq) props = logger.debug ( Log.setMessage "applyWorkspaceTargetFrameworkProp: failed to load {projectFilename}: {ex}" >> Log.addContext "projectFilename" projectFilename - >> Log.addContext "ex" (string ipfe) + >> Log.addContext "ex" (string (ipfe.GetType())) ) let distinctTfms = tfms |> Set.ofSeq From 32f67a5f063017a9e922355ad84a081645e58a3b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Saulius=20Menkevi=C4=8Dius?= Date: Tue, 8 Jul 2025 14:50:20 +0300 Subject: [PATCH 3/5] Add tests for workspace/symbol handler --- .../Handlers/WorkspaceSymbol.fs | 37 ++++++---------- .../CSharpLanguageServer.Tests.fsproj | 1 + .../testWorkspaceSymbolWorks/Project/Class.cs | 6 +++ .../Project/Project.csproj | 6 +++ .../WorkspaceSymbolTests.fs | 43 +++++++++++++++++++ 5 files changed, 69 insertions(+), 24 deletions(-) create mode 100644 tests/CSharpLanguageServer.Tests/TestData/testWorkspaceSymbolWorks/Project/Class.cs create mode 100644 tests/CSharpLanguageServer.Tests/TestData/testWorkspaceSymbolWorks/Project/Project.csproj create mode 100644 tests/CSharpLanguageServer.Tests/WorkspaceSymbolTests.fs diff --git a/src/CSharpLanguageServer/Handlers/WorkspaceSymbol.fs b/src/CSharpLanguageServer/Handlers/WorkspaceSymbol.fs index 3131a32e..77431081 100644 --- a/src/CSharpLanguageServer/Handlers/WorkspaceSymbol.fs +++ b/src/CSharpLanguageServer/Handlers/WorkspaceSymbol.fs @@ -12,30 +12,19 @@ open CSharpLanguageServer.Conversions [] module WorkspaceSymbol = - let private dynamicRegistration (clientCapabilities: ClientCapabilities) = - clientCapabilities.TextDocument - |> Option.bind (fun x -> x.Formatting) - |> Option.bind (fun x -> x.DynamicRegistration) - |> Option.defaultValue false - - let provider (clientCapabilities: ClientCapabilities) : U2 option = - match dynamicRegistration clientCapabilities with - | true -> None - | false -> U2.C1 true |> Some - - let registration (clientCapabilities: ClientCapabilities) : Registration option = - match dynamicRegistration clientCapabilities with - | false -> None - | true -> - let registerOptions: WorkspaceSymbolRegistrationOptions = - { ResolveProvider = None - WorkDoneProgress = None - } - - Some - { Id = Guid.NewGuid().ToString() - Method = "workspace/symbol" - RegisterOptions = registerOptions |> serialize |> Some } + let provider (_: ClientCapabilities) : U2 option = + U2.C1 true |> Some + + let registration (_: ClientCapabilities) : Registration option = + let registrationOptions: WorkspaceSymbolRegistrationOptions = + { ResolveProvider = None + WorkDoneProgress = None + } + + Some + { Id = Guid.NewGuid().ToString() + Method = "workspace/symbol" + RegisterOptions = registrationOptions |> serialize |> Some } let handle (context: ServerRequestContext) (p: WorkspaceSymbolParams) : AsyncLspResult option> = async { let pattern = diff --git a/tests/CSharpLanguageServer.Tests/CSharpLanguageServer.Tests.fsproj b/tests/CSharpLanguageServer.Tests/CSharpLanguageServer.Tests.fsproj index c556816a..a704e4fd 100644 --- a/tests/CSharpLanguageServer.Tests/CSharpLanguageServer.Tests.fsproj +++ b/tests/CSharpLanguageServer.Tests/CSharpLanguageServer.Tests.fsproj @@ -19,6 +19,7 @@ + diff --git a/tests/CSharpLanguageServer.Tests/TestData/testWorkspaceSymbolWorks/Project/Class.cs b/tests/CSharpLanguageServer.Tests/TestData/testWorkspaceSymbolWorks/Project/Class.cs new file mode 100644 index 00000000..0f759e72 --- /dev/null +++ b/tests/CSharpLanguageServer.Tests/TestData/testWorkspaceSymbolWorks/Project/Class.cs @@ -0,0 +1,6 @@ +class Class +{ + public void MethodA(string arg) + { + } +} diff --git a/tests/CSharpLanguageServer.Tests/TestData/testWorkspaceSymbolWorks/Project/Project.csproj b/tests/CSharpLanguageServer.Tests/TestData/testWorkspaceSymbolWorks/Project/Project.csproj new file mode 100644 index 00000000..29aeae9e --- /dev/null +++ b/tests/CSharpLanguageServer.Tests/TestData/testWorkspaceSymbolWorks/Project/Project.csproj @@ -0,0 +1,6 @@ + + + Exe + net9.0 + + diff --git a/tests/CSharpLanguageServer.Tests/WorkspaceSymbolTests.fs b/tests/CSharpLanguageServer.Tests/WorkspaceSymbolTests.fs new file mode 100644 index 00000000..3bd87858 --- /dev/null +++ b/tests/CSharpLanguageServer.Tests/WorkspaceSymbolTests.fs @@ -0,0 +1,43 @@ +module CSharpLanguageServer.Tests.WorkspaceSymbolTests + +open NUnit.Framework +open Ionide.LanguageServerProtocol.Types + +open CSharpLanguageServer.Tests.Tooling + +[] +let testWorkspaceSymbolWorks () = + use client = setupServerClient defaultClientProfile + "TestData/testWorkspaceSymbolWorks" + client.StartAndWaitForSolutionLoad() + + let serverCaps = client.GetState().ServerCapabilities.Value + + Assert.AreEqual( + true |> U2.C1 |> Some, + serverCaps.WorkspaceSymbolProvider) + + use classFile = client.Open("Project/Class.cs") + + let completionParams0: WorkspaceSymbolParams = + { WorkDoneToken = None + PartialResultToken = None + Query = "Class" + } + + let symbols0 : U2 option = + client.Request("workspace/symbol", completionParams0) + + match symbols0 with + | Some (U2.C1 sis) -> + Assert.AreEqual(1, sis.Length) + + let sym0 = sis[0] + Assert.AreEqual("Class", sym0.Name) + Assert.AreEqual(SymbolKind.Class, sym0.Kind) + Assert.IsFalse(sym0.Tags.IsSome) + Assert.IsFalse(sym0.ContainerName.IsSome) + Assert.AreEqual(classFile.Uri, sym0.Location.Uri) + () + + | _ -> failwith "Some U2.C1 was expected" From 8c5e95e0f661260dc7d7a8227d14c593c7d8310b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Saulius=20Menkevi=C4=8Dius?= Date: Tue, 8 Jul 2025 15:19:06 +0300 Subject: [PATCH 4/5] Convert ServerRequestContext.FindSymbols to AsyncSeq method --- src/CSharpLanguageServer/Handlers/WorkspaceSymbol.fs | 10 ++++------ .../State/ServerRequestContext.fs | 11 ++++++++--- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/src/CSharpLanguageServer/Handlers/WorkspaceSymbol.fs b/src/CSharpLanguageServer/Handlers/WorkspaceSymbol.fs index 77431081..3ac71f51 100644 --- a/src/CSharpLanguageServer/Handlers/WorkspaceSymbol.fs +++ b/src/CSharpLanguageServer/Handlers/WorkspaceSymbol.fs @@ -2,6 +2,7 @@ namespace CSharpLanguageServer.Handlers open System +open FSharp.Control open Microsoft.CodeAnalysis open Ionide.LanguageServerProtocol.Server open Ionide.LanguageServerProtocol.Types @@ -27,12 +28,9 @@ module WorkspaceSymbol = RegisterOptions = registrationOptions |> serialize |> Some } let handle (context: ServerRequestContext) (p: WorkspaceSymbolParams) : AsyncLspResult option> = async { - let pattern = - if String.IsNullOrEmpty(p.Query) then - None - else - Some p.Query - let! symbols = context.FindSymbols pattern + let pattern = if String.IsNullOrEmpty(p.Query) then None else Some p.Query + + let! symbols = context.FindSymbols pattern |> AsyncSeq.toArrayAsync return symbols |> Seq.map (SymbolInformation.fromSymbol SymbolDisplayFormat.MinimallyQualifiedFormat) diff --git a/src/CSharpLanguageServer/State/ServerRequestContext.fs b/src/CSharpLanguageServer/State/ServerRequestContext.fs index a534ae95..7dc5febe 100644 --- a/src/CSharpLanguageServer/State/ServerRequestContext.fs +++ b/src/CSharpLanguageServer/State/ServerRequestContext.fs @@ -1,5 +1,6 @@ namespace CSharpLanguageServer.State +open FSharp.Control open Microsoft.CodeAnalysis open Microsoft.CodeAnalysis.FindSymbols open Ionide.LanguageServerProtocol.Types @@ -196,7 +197,7 @@ type ServerRequestContext (requestId: int, state: ServerState, emitServerEvent) return aggregatedLspLocations } - member this.FindSymbols (pattern: string option): Async = async { + member this.FindSymbols (pattern: string option): AsyncSeq = asyncSeq { let findTask ct = match pattern with | Some pat -> @@ -206,10 +207,14 @@ type ServerRequestContext (requestId: int, state: ServerState, emitServerEvent) fun (sln: Solution) -> SymbolFinder.FindSourceDeclarationsAsync(sln, true', SymbolFilter.TypeAndMember, cancellationToken=ct) match this.State.Solution with - | None -> return [] | Some solution -> let! ct = Async.CancellationToken - return! findTask ct solution |> Async.AwaitTask + let! items = findTask ct solution |> Async.AwaitTask + + for item in items do + yield item + + | None -> () } member this.FindReferences (symbol: ISymbol) (withDefinition: bool): Async = async { From 49e15315113c46e7aacdec6ab12c34cbbda20284 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Saulius=20Menkevi=C4=8Dius?= Date: Tue, 8 Jul 2025 15:53:01 +0300 Subject: [PATCH 5/5] partial implementation --- .../Handlers/WorkspaceSymbol.fs | 64 +++++++++++++++---- .../CSharpLanguageServer.Tests.fsproj | 2 + 2 files changed, 54 insertions(+), 12 deletions(-) diff --git a/src/CSharpLanguageServer/Handlers/WorkspaceSymbol.fs b/src/CSharpLanguageServer/Handlers/WorkspaceSymbol.fs index 3ac71f51..a96fe57d 100644 --- a/src/CSharpLanguageServer/Handlers/WorkspaceSymbol.fs +++ b/src/CSharpLanguageServer/Handlers/WorkspaceSymbol.fs @@ -9,12 +9,20 @@ open Ionide.LanguageServerProtocol.Types open Ionide.LanguageServerProtocol.JsonRpc open CSharpLanguageServer.State +open CSharpLanguageServer.Logging open CSharpLanguageServer.Conversions [] module WorkspaceSymbol = + let private logger = LogProvider.getLoggerByName "WorkspaceSymbol" + let provider (_: ClientCapabilities) : U2 option = - U2.C1 true |> Some + let workspaceSymbolOptions: WorkspaceSymbolOptions = + { WorkDoneProgress = None + ResolveProvider = None + } + + U2.C2 workspaceSymbolOptions |> Some let registration (_: ClientCapabilities) : Registration option = let registrationOptions: WorkspaceSymbolRegistrationOptions = @@ -30,17 +38,49 @@ module WorkspaceSymbol = let handle (context: ServerRequestContext) (p: WorkspaceSymbolParams) : AsyncLspResult option> = async { let pattern = if String.IsNullOrEmpty(p.Query) then None else Some p.Query - let! symbols = context.FindSymbols pattern |> AsyncSeq.toArrayAsync - return - symbols - |> Seq.map (SymbolInformation.fromSymbol SymbolDisplayFormat.MinimallyQualifiedFormat) - |> Seq.collect id - // TODO: make 100 configurable? - |> Seq.truncate 100 - |> Seq.toArray - |> U2.C1 - |> Some - |> LspResult.success + let emptySymbolInformationList: SymbolInformation[] = Array.empty + + match context.State.Solution, p.PartialResultToken with + | None, _ -> + return U2.C1 emptySymbolInformationList |> Some |> LspResult.success + + | Some _, None -> + failwith "will send full results" + + let! symbols = context.FindSymbols pattern |> AsyncSeq.toArrayAsync + return + symbols + |> Seq.map (SymbolInformation.fromSymbol SymbolDisplayFormat.MinimallyQualifiedFormat) + |> Seq.collect id + // TODO: make 100 configurable? + |> Seq.truncate 100 + |> Seq.toArray + |> U2.C1 + |> Some + |> LspResult.success + + | Some _, Some partialResultToken -> + failwith "will send partial results" + + let sendSymbolInformationPartialResult (sym: SymbolInformation list) = async { + let progressParams: ProgressParams = + let partialResult: U2 = sym |> Array.ofList |> U2.C1 + + { Token = partialResultToken; Value = serialize partialResult } + + logger.error ( + Log.setMessage "sending partial result" + ) + + let lspClient = context.State.LspClient.Value + do! lspClient.Progress(progressParams) + } + + do! context.FindSymbols pattern + |> AsyncSeq.map (SymbolInformation.fromSymbol SymbolDisplayFormat.MinimallyQualifiedFormat) + |> AsyncSeq.iterAsync sendSymbolInformationPartialResult + + return U2.C1 emptySymbolInformationList |> Some |> LspResult.success } let resolve (_context: ServerRequestContext) (_p: WorkspaceSymbol) : AsyncLspResult = diff --git a/tests/CSharpLanguageServer.Tests/CSharpLanguageServer.Tests.fsproj b/tests/CSharpLanguageServer.Tests/CSharpLanguageServer.Tests.fsproj index a704e4fd..e775cdca 100644 --- a/tests/CSharpLanguageServer.Tests/CSharpLanguageServer.Tests.fsproj +++ b/tests/CSharpLanguageServer.Tests/CSharpLanguageServer.Tests.fsproj @@ -9,6 +9,7 @@ +