Skip to content

Fix and improve workspace/symbol handler #239

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 5 commits into
base: main
Choose a base branch
from
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
107 changes: 67 additions & 40 deletions src/CSharpLanguageServer/Handlers/WorkspaceSymbol.fs
Original file line number Diff line number Diff line change
Expand Up @@ -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

[<RequireQualifiedAccess>]
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<bool, WorkspaceSymbolOptions> 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<bool, WorkspaceSymbolOptions> 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<U2<SymbolInformation[], WorkspaceSymbol[]> 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<SymbolInformation[], WorkspaceSymbol[]> = 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<WorkspaceSymbol> =
Expand Down
2 changes: 1 addition & 1 deletion src/CSharpLanguageServer/RoslynHelpers.fs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
11 changes: 8 additions & 3 deletions src/CSharpLanguageServer/State/ServerRequestContext.fs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
namespace CSharpLanguageServer.State

open FSharp.Control
open Microsoft.CodeAnalysis
open Microsoft.CodeAnalysis.FindSymbols
open Ionide.LanguageServerProtocol.Types
Expand Down Expand Up @@ -196,7 +197,7 @@ type ServerRequestContext (requestId: int, state: ServerState, emitServerEvent)
return aggregatedLspLocations
}

member this.FindSymbols (pattern: string option): Async<Microsoft.CodeAnalysis.ISymbol seq> = async {
member this.FindSymbols (pattern: string option): AsyncSeq<Microsoft.CodeAnalysis.ISymbol> = asyncSeq {
let findTask ct =
match pattern with
| Some pat ->
Expand All @@ -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<Microsoft.CodeAnalysis.Location seq> = async {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

<ItemGroup>
<Compile Include="Tooling.fs" />
<!--
<Compile Include="CodeActionTests.fs" />
<Compile Include="DiagnosticTests.fs" />
<Compile Include="DocumentationTests.fs" />
Expand All @@ -19,6 +20,8 @@
<Compile Include="DocumentFormattingTests.fs" />
<Compile Include="InternalTests.fs" />
<Compile Include="CompletionTests.fs" />
-->
<Compile Include="WorkspaceSymbolTests.fs" />
</ItemGroup>

<ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
class Class
{
public void MethodA(string arg)
{
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net9.0</TargetFramework>
</PropertyGroup>
</Project>
43 changes: 43 additions & 0 deletions tests/CSharpLanguageServer.Tests/WorkspaceSymbolTests.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
module CSharpLanguageServer.Tests.WorkspaceSymbolTests

open NUnit.Framework
open Ionide.LanguageServerProtocol.Types

open CSharpLanguageServer.Tests.Tooling

[<TestCase>]
let testWorkspaceSymbolWorks () =
use client = setupServerClient defaultClientProfile
"TestData/testWorkspaceSymbolWorks"
client.StartAndWaitForSolutionLoad()

let serverCaps = client.GetState().ServerCapabilities.Value

Assert.AreEqual(
true |> U2<bool, WorkspaceSymbolOptions>.C1 |> Some,
serverCaps.WorkspaceSymbolProvider)

use classFile = client.Open("Project/Class.cs")

let completionParams0: WorkspaceSymbolParams =
{ WorkDoneToken = None
PartialResultToken = None
Query = "Class"
}

let symbols0 : U2<SymbolInformation[], WorkspaceSymbol[]> 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"
Loading