diff --git a/src/CSharpLanguageServer/Handlers/WorkspaceSymbol.fs b/src/CSharpLanguageServer/Handlers/WorkspaceSymbol.fs index 771347ad..a96fe57d 100644 --- a/src/CSharpLanguageServer/Handlers/WorkspaceSymbol.fs +++ b/src/CSharpLanguageServer/Handlers/WorkspaceSymbol.fs @@ -2,58 +2,85 @@ namespace CSharpLanguageServer.Handlers open System +open FSharp.Control open Microsoft.CodeAnalysis open Ionide.LanguageServerProtocol.Server open Ionide.LanguageServerProtocol.Types open Ionide.LanguageServerProtocol.JsonRpc open CSharpLanguageServer.State +open CSharpLanguageServer.Logging 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 = Some true - WorkDoneProgress = None - } - - Some - { Id = Guid.NewGuid().ToString() - Method = "workspace/symbol" - RegisterOptions = registerOptions |> serialize |> Some } + let private logger = LogProvider.getLoggerByName "WorkspaceSymbol" + + let provider (_: ClientCapabilities) : U2 option = + let workspaceSymbolOptions: WorkspaceSymbolOptions = + { WorkDoneProgress = None + ResolveProvider = None + } + + U2.C2 workspaceSymbolOptions |> 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 = - if String.IsNullOrEmpty(p.Query) then - None - else - Some p.Query - let! symbols = context.FindSymbols pattern - 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 pattern = if String.IsNullOrEmpty(p.Query) then None else Some p.Query + + 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/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 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 { diff --git a/tests/CSharpLanguageServer.Tests/CSharpLanguageServer.Tests.fsproj b/tests/CSharpLanguageServer.Tests/CSharpLanguageServer.Tests.fsproj index c556816a..e775cdca 100644 --- a/tests/CSharpLanguageServer.Tests/CSharpLanguageServer.Tests.fsproj +++ b/tests/CSharpLanguageServer.Tests/CSharpLanguageServer.Tests.fsproj @@ -9,6 +9,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"