diff --git a/src/CSharpLanguageServer/CSharpLanguageServer.fsproj b/src/CSharpLanguageServer/CSharpLanguageServer.fsproj index 8e2c76d7..79a1fb5d 100644 --- a/src/CSharpLanguageServer/CSharpLanguageServer.fsproj +++ b/src/CSharpLanguageServer/CSharpLanguageServer.fsproj @@ -33,6 +33,7 @@ + diff --git a/src/CSharpLanguageServer/Handlers/CSharpMetadata.fs b/src/CSharpLanguageServer/Handlers/CSharpMetadata.fs index 36039bda..1b96250d 100644 --- a/src/CSharpLanguageServer/Handlers/CSharpMetadata.fs +++ b/src/CSharpLanguageServer/Handlers/CSharpMetadata.fs @@ -5,20 +5,19 @@ open Ionide.LanguageServerProtocol.JsonRpc open CSharpLanguageServer.Types open CSharpLanguageServer.State +open CSharpLanguageServer.Lsp.Workspace [] module CSharpMetadata = let handle (context: ServerRequestContext) - (metadataParams: CSharpMetadataParams) + (p: CSharpMetadataParams) : AsyncLspResult = - async { - let uri = metadataParams.TextDocument.Uri - let metadataMaybe = - context.DecompiledMetadata - |> Map.tryFind uri - |> Option.map (fun x -> x.Metadata) - - return metadataMaybe |> LspResult.success - } + p.TextDocument.Uri + |> workspaceFolder context.Workspace + |> Option.map _.DecompiledMetadata + |> Option.bind (Map.tryFind p.TextDocument.Uri) + |> Option.map _.Metadata + |> LspResult.success + |> async.Return diff --git a/src/CSharpLanguageServer/Handlers/CallHierarchy.fs b/src/CSharpLanguageServer/Handlers/CallHierarchy.fs index b833e544..d51036b9 100644 --- a/src/CSharpLanguageServer/Handlers/CallHierarchy.fs +++ b/src/CSharpLanguageServer/Handlers/CallHierarchy.fs @@ -6,7 +6,9 @@ open Ionide.LanguageServerProtocol.Types open Ionide.LanguageServerProtocol.JsonRpc open CSharpLanguageServer.State +open CSharpLanguageServer.State.ServerState open CSharpLanguageServer.Roslyn.Conversions +open CSharpLanguageServer.Lsp.Workspace [] module CallHierarchy = @@ -18,9 +20,7 @@ module CallHierarchy = Microsoft.CodeAnalysis.SymbolKind.Event Microsoft.CodeAnalysis.SymbolKind.Property ] - let provider - (clientCapabilities: ClientCapabilities) - : U3 option = + let provider (_cc: ClientCapabilities) : U3 option = Some(U3.C1 true) let prepare @@ -28,10 +28,19 @@ module CallHierarchy = (p: CallHierarchyPrepareParams) : AsyncLspResult = async { - match! context.FindSymbol p.TextDocument.Uri p.Position with - | Some symbol when isCallableSymbol symbol -> - let! itemList = CallHierarchyItem.fromSymbol context.ResolveSymbolLocations symbol - return itemList |> List.toArray |> Some |> LspResult.success + match! workspaceDocumentSymbol context.Workspace AnyDocument p.TextDocument.Uri p.Position with + | Some wf, Some(symbol, project, _) when isCallableSymbol symbol -> + let! locations, updatedWf = workspaceFolderSymbolLocations symbol project wf + + context.Emit(WorkspaceFolderChange updatedWf) + + return + locations + |> Seq.map (CallHierarchyItem.fromSymbolAndLocation symbol) + |> Seq.toArray + |> Some + |> LspResult.success + | _ -> return None |> LspResult.success } @@ -40,6 +49,8 @@ module CallHierarchy = (p: CallHierarchyIncomingCallsParams) : AsyncLspResult = async { + let! ct = Async.CancellationToken + let toCallHierarchyIncomingCalls (info: SymbolCallerInfo) : CallHierarchyIncomingCall seq = let fromRanges = info.Locations @@ -49,13 +60,15 @@ module CallHierarchy = info.CallingSymbol.Locations |> Seq.choose Location.fromRoslynLocation |> Seq.map (fun loc -> - { From = CallHierarchyItem.fromSymbolAndLocation (info.CallingSymbol) loc + { From = CallHierarchyItem.fromSymbolAndLocation info.CallingSymbol loc FromRanges = fromRanges }) - match! context.FindSymbol p.Item.Uri p.Item.Range.Start with - | None -> return None |> LspResult.success - | Some symbol -> - let! callers = context.FindCallers symbol + match! workspaceDocumentSymbol context.Workspace AnyDocument p.Item.Uri p.Item.Range.Start with + | Some wf, Some(symbol, _, _) -> + let! callers = + SymbolFinder.FindCallersAsync(symbol, wf.Solution.Value, cancellationToken = ct) + |> Async.AwaitTask + // TODO: If we remove info.IsDirect, then we will get lots of false positive. But if we keep it, // we will miss many callers. Maybe it should have some change in LSP protocol. return @@ -66,6 +79,8 @@ module CallHierarchy = |> Seq.toArray |> Some |> LspResult.success + + | _, _ -> return None |> LspResult.success } let outgoingCalls diff --git a/src/CSharpLanguageServer/Handlers/CodeAction.fs b/src/CSharpLanguageServer/Handlers/CodeAction.fs index 407fc3fb..c8b68404 100644 --- a/src/CSharpLanguageServer/Handlers/CodeAction.fs +++ b/src/CSharpLanguageServer/Handlers/CodeAction.fs @@ -21,6 +21,7 @@ open CSharpLanguageServer.Logging open CSharpLanguageServer.Roslyn.Conversions open CSharpLanguageServer.State open CSharpLanguageServer.Util +open CSharpLanguageServer.Lsp.Workspace type CSharpCodeActionResolutionData = @@ -330,7 +331,10 @@ module CodeAction = (p: CodeActionParams) : AsyncLspResult = async { - match context.GetDocument p.TextDocument.Uri with + let wf, docForUri = + p.TextDocument.Uri |> workspaceDocument context.Workspace AnyDocument + + match docForUri with | None -> return None |> LspResult.success | Some doc -> let! ct = Async.CancellationToken @@ -376,7 +380,7 @@ module CodeAction = let! maybeLspCa = roslynCodeActionToResolvedLspCodeAction doc.Project.Solution - context.GetDocumentVersion + (Uri.unescape >> workspaceDocumentVersion context.Workspace) doc ct ca @@ -400,7 +404,11 @@ module CodeAction = let resolutionData = p.Data |> Option.map deserialize - match context.GetDocument resolutionData.Value.TextDocumentUri with + let wf, docForUri = + resolutionData.Value.TextDocumentUri + |> workspaceDocument context.Workspace AnyDocument + + match docForUri with | None -> return raise (Exception(sprintf "no document for uri %s" resolutionData.Value.TextDocumentUri)) | Some doc -> let! ct = Async.CancellationToken @@ -414,7 +422,11 @@ module CodeAction = roslynCodeActions |> Seq.tryFind (fun ca -> ca.Title = p.Title) let toResolvedLspCodeAction = - roslynCodeActionToResolvedLspCodeAction doc.Project.Solution context.GetDocumentVersion doc ct + roslynCodeActionToResolvedLspCodeAction + doc.Project.Solution + (Uri.unescape >> workspaceDocumentVersion context.Workspace) + doc + ct let! lspCodeAction = match selectedCodeAction with diff --git a/src/CSharpLanguageServer/Handlers/CodeLens.fs b/src/CSharpLanguageServer/Handlers/CodeLens.fs index 6ec0fb1c..bb7c73e4 100644 --- a/src/CSharpLanguageServer/Handlers/CodeLens.fs +++ b/src/CSharpLanguageServer/Handlers/CodeLens.fs @@ -4,12 +4,14 @@ open Microsoft.CodeAnalysis open Microsoft.CodeAnalysis.CSharp open Microsoft.CodeAnalysis.CSharp.Syntax open Microsoft.CodeAnalysis.Text +open Microsoft.CodeAnalysis.FindSymbols open Ionide.LanguageServerProtocol.Server open Ionide.LanguageServerProtocol.Types open Ionide.LanguageServerProtocol.JsonRpc open CSharpLanguageServer.State open CSharpLanguageServer.Roslyn.Conversions +open CSharpLanguageServer.Lsp.Workspace type private DocumentSymbolCollectorForCodeLens(semanticModel: SemanticModel) = inherit CSharpSyntaxWalker(SyntaxWalkerDepth.Token) @@ -89,9 +91,10 @@ module CodeLens = WorkDoneProgress = None } let handle (context: ServerRequestContext) (p: CodeLensParams) : AsyncLspResult = async { - let docMaybe = context.GetDocument p.TextDocument.Uri + let wf, docForUri = + p.TextDocument.Uri |> workspaceDocument context.Workspace AnyDocument - match docMaybe with + match docForUri with | None -> return None |> LspResult.success | Some doc -> let! ct = Async.CancellationToken @@ -120,6 +123,7 @@ module CodeLens = } let resolve (context: ServerRequestContext) (p: CodeLens) : AsyncLspResult = async { + let! ct = Async.CancellationToken let lensData: CodeLensData = p.Data @@ -127,14 +131,18 @@ module CodeLens = |> Option.bind Option.ofObj |> Option.defaultValue CodeLensData.Default - match! context.FindSymbol lensData.DocumentUri lensData.Position with - | None -> return p |> LspResult.success - | Some symbol -> - let! locations = context.FindReferences symbol false + match! workspaceDocumentSymbol context.Workspace AnyDocument lensData.DocumentUri lensData.Position with + | Some wf, Some(symbol, _, _) -> + let! refs = + SymbolFinder.FindReferencesAsync(symbol, wf.Solution.Value, cancellationToken = ct) + |> Async.AwaitTask + // FIXME: refNum is wrong. There are lots of false positive even if we distinct locations by // (l.SourceTree.FilePath, l.SourceSpan) let refNum = - locations + refs + |> Seq.collect _.Locations + |> Seq.map _.Location |> Seq.distinctBy (fun l -> (l.GetMappedLineSpan().Path, l.SourceSpan)) |> Seq.length @@ -153,4 +161,6 @@ module CodeLens = Arguments = Some [| arg |> serialize |] } return { p with Command = Some command } |> LspResult.success + + | _, _ -> return p |> LspResult.success } diff --git a/src/CSharpLanguageServer/Handlers/Completion.fs b/src/CSharpLanguageServer/Handlers/Completion.fs index f26e697a..899a374e 100644 --- a/src/CSharpLanguageServer/Handlers/Completion.fs +++ b/src/CSharpLanguageServer/Handlers/Completion.fs @@ -12,6 +12,7 @@ open CSharpLanguageServer.State open CSharpLanguageServer.Util open CSharpLanguageServer.Roslyn.Conversions open CSharpLanguageServer.Logging +open CSharpLanguageServer.Lsp.Workspace [] module Completion = @@ -185,7 +186,10 @@ module Completion = (p: CompletionParams) : Async option>> = async { - match context.GetDocument p.TextDocument.Uri with + let wf, docForUri = + p.TextDocument.Uri |> workspaceDocument context.Workspace AnyDocument + + match docForUri with | None -> return None |> LspResult.success | Some doc -> let! ct = Async.CancellationToken diff --git a/src/CSharpLanguageServer/Handlers/Definition.fs b/src/CSharpLanguageServer/Handlers/Definition.fs index 64a7dc2a..e8e05016 100644 --- a/src/CSharpLanguageServer/Handlers/Definition.fs +++ b/src/CSharpLanguageServer/Handlers/Definition.fs @@ -4,6 +4,8 @@ open Ionide.LanguageServerProtocol.Types open Ionide.LanguageServerProtocol.JsonRpc open CSharpLanguageServer.State +open CSharpLanguageServer.State.ServerState +open CSharpLanguageServer.Lsp.Workspace [] module Definition = @@ -14,9 +16,13 @@ module Definition = (p: DefinitionParams) : Async option>> = async { - match! context.FindSymbol' p.TextDocument.Uri p.Position with - | None -> return None |> LspResult.success - | Some(symbol, project, _) -> - let! locations = context.ResolveSymbolLocations symbol (Some project) + match! workspaceDocumentSymbol context.Workspace AnyDocument p.TextDocument.Uri p.Position with + | Some wf, Some(symbol, project, _) -> + let! locations, updatedWf = workspaceFolderSymbolLocations symbol project wf + + context.Emit(WorkspaceFolderChange updatedWf) + return locations |> Array.ofList |> Definition.C2 |> U2.C1 |> Some |> LspResult.success + + | _, _ -> return None |> LspResult.success } diff --git a/src/CSharpLanguageServer/Handlers/Diagnostic.fs b/src/CSharpLanguageServer/Handlers/Diagnostic.fs index ef33f4d0..621b81d5 100644 --- a/src/CSharpLanguageServer/Handlers/Diagnostic.fs +++ b/src/CSharpLanguageServer/Handlers/Diagnostic.fs @@ -9,6 +9,8 @@ open CSharpLanguageServer.Roslyn.Conversions open CSharpLanguageServer.State open CSharpLanguageServer.Types open CSharpLanguageServer.Util +open CSharpLanguageServer.Lsp.Workspace + [] module Diagnostic = @@ -36,7 +38,10 @@ module Diagnostic = Items = [||] RelatedDocuments = None } - match context.GetDocument p.TextDocument.Uri with + let wf, docForUri = + p.TextDocument.Uri |> workspaceDocument context.Workspace AnyDocument + + match docForUri with | None -> return emptyReport |> U2.C1 |> LspResult.success | Some doc -> @@ -107,7 +112,7 @@ module Diagnostic = let emptyWorkspaceDiagnosticReport: WorkspaceDiagnosticReport = { Items = Array.empty } - match context.State.Solution, p.PartialResultToken with + match context.Workspace.SingletonFolder.Solution, p.PartialResultToken with | None, _ -> return emptyWorkspaceDiagnosticReport |> LspResult.success | Some solution, None -> diff --git a/src/CSharpLanguageServer/Handlers/DocumentFormatting.fs b/src/CSharpLanguageServer/Handlers/DocumentFormatting.fs index 46667f4a..4323c495 100644 --- a/src/CSharpLanguageServer/Handlers/DocumentFormatting.fs +++ b/src/CSharpLanguageServer/Handlers/DocumentFormatting.fs @@ -7,9 +7,10 @@ open Ionide.LanguageServerProtocol.JsonRpc open CSharpLanguageServer.State open CSharpLanguageServer.Util open CSharpLanguageServer.Roslyn.Document +open CSharpLanguageServer.Lsp.Workspace -[] +[] module DocumentFormatting = let provider (_cc: ClientCapabilities) : U2 option = Some(U2.C1 true) @@ -22,12 +23,12 @@ module DocumentFormatting = } let handle (context: ServerRequestContext) (p: DocumentFormattingParams) : AsyncLspResult = - let formatDocument = - p.Options - |> context.State.Settings.GetEffectiveFormattingOptions - |> formatDocument + let lspFormattingOptions = + p.Options |> context.State.Settings.GetEffectiveFormattingOptions + + let wf, doc = p.TextDocument.Uri |> workspaceDocument context.Workspace UserDocument - context.GetUserDocument p.TextDocument.Uri + doc |> async.Return - |> Async.bindOption formatDocument + |> Async.bindOption (formatDocument lspFormattingOptions) |> Async.map LspResult.success diff --git a/src/CSharpLanguageServer/Handlers/DocumentHighlight.fs b/src/CSharpLanguageServer/Handlers/DocumentHighlight.fs index 87486b08..73393d61 100644 --- a/src/CSharpLanguageServer/Handlers/DocumentHighlight.fs +++ b/src/CSharpLanguageServer/Handlers/DocumentHighlight.fs @@ -10,10 +10,11 @@ open Ionide.LanguageServerProtocol.JsonRpc open CSharpLanguageServer.State open CSharpLanguageServer.Roslyn.Conversions open CSharpLanguageServer.Util +open CSharpLanguageServer.Lsp.Workspace [] module DocumentHighlight = - let provider (_: ClientCapabilities) : U2 option = Some(U2.C1 true) + let provider (_cc: ClientCapabilities) : U2 option = Some(U2.C1 true) let private shouldHighlight (symbol: ISymbol) = match symbol with @@ -56,13 +57,13 @@ module DocumentHighlight = Kind = Some DocumentHighlightKind.Read }) } - match! context.FindSymbol' p.TextDocument.Uri p.Position with - | Some(symbol, _, Some doc) -> + match! workspaceDocumentSymbol context.Workspace AnyDocument p.TextDocument.Uri p.Position with + | Some wf, Some(symbol, _, Some doc) -> if shouldHighlight symbol then let! highlights = getHighlights symbol doc return highlights |> Seq.toArray |> Some |> LspResult.success else return None |> LspResult.success - | _ -> return None |> LspResult.success + | _, _ -> return None |> LspResult.success } diff --git a/src/CSharpLanguageServer/Handlers/DocumentOnTypeFormatting.fs b/src/CSharpLanguageServer/Handlers/DocumentOnTypeFormatting.fs index c5b91b8e..6ea4fb4d 100644 --- a/src/CSharpLanguageServer/Handlers/DocumentOnTypeFormatting.fs +++ b/src/CSharpLanguageServer/Handlers/DocumentOnTypeFormatting.fs @@ -10,6 +10,8 @@ open Ionide.LanguageServerProtocol.JsonRpc open CSharpLanguageServer.State open CSharpLanguageServer.Roslyn.Conversions open CSharpLanguageServer.Roslyn.Document +open CSharpLanguageServer.Lsp.Workspace + [] module DocumentOnTypeFormatting = @@ -43,12 +45,12 @@ module DocumentOnTypeFormatting = let handle (context: ServerRequestContext) (p: DocumentOnTypeFormattingParams) : AsyncLspResult = async { let lspFormattingOptions = - if context.State.Settings.ApplyFormattingOptions then - Some p.Options - else - None + p.Options |> context.State.Settings.GetEffectiveFormattingOptions + + let wf, docForUri = + p.TextDocument.Uri |> workspaceDocument context.Workspace UserDocument - match context.GetUserDocument p.TextDocument.Uri with + match docForUri with | None -> return None |> LspResult.success | Some doc -> let! options = getDocumentFormattingOptionSet doc lspFormattingOptions diff --git a/src/CSharpLanguageServer/Handlers/DocumentRangeFormatting.fs b/src/CSharpLanguageServer/Handlers/DocumentRangeFormatting.fs index a42be2c0..92629e96 100644 --- a/src/CSharpLanguageServer/Handlers/DocumentRangeFormatting.fs +++ b/src/CSharpLanguageServer/Handlers/DocumentRangeFormatting.fs @@ -8,6 +8,7 @@ open Ionide.LanguageServerProtocol.JsonRpc open CSharpLanguageServer.State open CSharpLanguageServer.Roslyn.Conversions open CSharpLanguageServer.Roslyn.Document +open CSharpLanguageServer.Lsp.Workspace [] module DocumentRangeFormatting = @@ -15,12 +16,12 @@ module DocumentRangeFormatting = let handle (context: ServerRequestContext) (p: DocumentRangeFormattingParams) : AsyncLspResult = async { let lspFormattingOptions = - if context.State.Settings.ApplyFormattingOptions then - Some p.Options - else - None + p.Options |> context.State.Settings.GetEffectiveFormattingOptions - match context.GetUserDocument p.TextDocument.Uri with + let wf, docForUri = + p.TextDocument.Uri |> workspaceDocument context.Workspace UserDocument + + match docForUri with | None -> return None |> LspResult.success | Some doc -> let! ct = Async.CancellationToken diff --git a/src/CSharpLanguageServer/Handlers/DocumentSymbol.fs b/src/CSharpLanguageServer/Handlers/DocumentSymbol.fs index d1f796b5..f19b9220 100644 --- a/src/CSharpLanguageServer/Handlers/DocumentSymbol.fs +++ b/src/CSharpLanguageServer/Handlers/DocumentSymbol.fs @@ -12,6 +12,7 @@ open Ionide.LanguageServerProtocol.JsonRpc open CSharpLanguageServer.State open CSharpLanguageServer.Roslyn.Conversions open CSharpLanguageServer.Util +open CSharpLanguageServer.Lsp.Workspace [] module DocumentSymbol = @@ -292,7 +293,10 @@ module DocumentSymbol = |> Option.bind _.HierarchicalDocumentSymbolSupport |> Option.defaultValue false - match context.GetDocument p.TextDocument.Uri with + let wf, docForUri = + p.TextDocument.Uri |> workspaceDocument context.Workspace AnyDocument + + match docForUri with | None -> return None |> LspResult.success | Some doc -> let! ct = Async.CancellationToken diff --git a/src/CSharpLanguageServer/Handlers/Hover.fs b/src/CSharpLanguageServer/Handlers/Hover.fs index 35d59b61..912b133e 100644 --- a/src/CSharpLanguageServer/Handlers/Hover.fs +++ b/src/CSharpLanguageServer/Handlers/Hover.fs @@ -5,6 +5,7 @@ open Ionide.LanguageServerProtocol.JsonRpc open CSharpLanguageServer open CSharpLanguageServer.State +open CSharpLanguageServer.Lsp.Workspace open CSharpLanguageServer.Util [] @@ -23,7 +24,10 @@ module Hover = hover |> Some - let handle (context: ServerRequestContext) (p: HoverParams) : AsyncLspResult = - context.FindSymbol p.TextDocument.Uri p.Position - |> Async.bindOption (makeHoverForSymbol >> async.Return) - |> Async.map LspResult.success + let handle (context: ServerRequestContext) (p: HoverParams) : AsyncLspResult = async { + let! wf, symInfo = workspaceDocumentSymbol context.Workspace AnyDocument p.TextDocument.Uri p.Position + + match symInfo with + | Some(sym, _, _) -> return makeHoverForSymbol sym |> LspResult.success + | None -> return None |> LspResult.success + } diff --git a/src/CSharpLanguageServer/Handlers/Implementation.fs b/src/CSharpLanguageServer/Handlers/Implementation.fs index ba7981f9..2662f023 100644 --- a/src/CSharpLanguageServer/Handlers/Implementation.fs +++ b/src/CSharpLanguageServer/Handlers/Implementation.fs @@ -1,27 +1,53 @@ namespace CSharpLanguageServer.Handlers +open Microsoft.CodeAnalysis +open Microsoft.CodeAnalysis.FindSymbols open Ionide.LanguageServerProtocol.Types open Ionide.LanguageServerProtocol.JsonRpc open CSharpLanguageServer.State +open CSharpLanguageServer.State.ServerState open CSharpLanguageServer.Util +open CSharpLanguageServer.Lsp.Workspace [] module Implementation = - let provider (_: ClientCapabilities) : U3 option = + let provider (_cc: ClientCapabilities) : U3 option = Some(U3.C1 true) - let findImplementationsOfSymbol (context: ServerRequestContext) sym = async { - let! impls = context.FindImplementations sym - let! locations = impls |> Seq.map (flip context.ResolveSymbolLocations None) |> Async.Parallel + let findImplLocationsOfSymbol wf project (sym: ISymbol) = async { + let! ct = Async.CancellationToken - return locations |> Array.collect List.toArray |> Declaration.C2 |> U2.C1 |> Some + let! impls = + SymbolFinder.FindImplementationsAsync(sym, wf.Solution.Value, cancellationToken = ct) + |> Async.AwaitTask + + let mutable updatedWf = wf + + let locations = System.Collections.Generic.List() + + for i in impls do + let! implLocations, wf = workspaceFolderSymbolLocations i project updatedWf + + locations.AddRange(implLocations) + updatedWf <- wf + + return locations |> Seq.toArray, updatedWf } let handle (context: ServerRequestContext) (p: ImplementationParams) : Async option>> = - context.FindSymbol p.TextDocument.Uri p.Position - |> Async.bindOption (findImplementationsOfSymbol context) - |> Async.map LspResult.success + async { + let! wf, symInfo = workspaceDocumentSymbol context.Workspace AnyDocument p.TextDocument.Uri p.Position + + match wf, symInfo with + | Some wf, Some(sym, project, _) -> + let! impls, updatedWf = findImplLocationsOfSymbol wf project sym + context.Emit(WorkspaceFolderChange updatedWf) + + return impls |> Declaration.C2 |> U2.C1 |> Some |> LspResult.success + + | _, _ -> return None |> LspResult.success + } diff --git a/src/CSharpLanguageServer/Handlers/Initialization.fs b/src/CSharpLanguageServer/Handlers/Initialization.fs index c8d8c8ce..e2979fa6 100644 --- a/src/CSharpLanguageServer/Handlers/Initialization.fs +++ b/src/CSharpLanguageServer/Handlers/Initialization.fs @@ -69,28 +69,22 @@ module Initialization = p.WorkspaceFolders ) - // TODO use p.WorkspaceFolders - let rootPath, rootPathSource = + let workspaceFoldersFallbackUri: DocumentUri = p.RootUri - |> Option.map (fun rootUri -> (Uri.toPath rootUri, "InitializeParams.rootUri")) - |> Option.orElse ( - p.RootPath - |> Option.map (fun rootPath -> (rootPath, "InitializeParams.rootPath")) - ) - |> Option.defaultValue (Directory.GetCurrentDirectory(), "Process CWD") + |> Option.orElse (p.RootPath |> Option.map Uri.fromPath) + |> Option.defaultValue (Directory.GetCurrentDirectory() |> Uri.fromPath) - do! - windowShowMessage ( - sprintf "csharp-ls: will use \"%s\" (%s) as workspace root path" rootPath rootPathSource - ) + let workspaceFolders = + p.WorkspaceFolders + |> Option.defaultValue Array.empty + |> Seq.append + [ { Uri = workspaceFoldersFallbackUri + Name = "root" } ] + |> List.ofSeq - logger.LogDebug( - "handleInitialize: using rootPath \"{rootPath}\" from {rootPathSource}", - rootPath, - rootPathSource - ) + logger.LogInformation("handleInitialize: using workspaceFolders: {folders}", serialize workspaceFolders) - context.Emit(RootPathChange rootPath) + context.Emit(WorkspaceConfigurationChanged workspaceFolders) // setup timer so actors get period ticks setupTimer () @@ -183,10 +177,9 @@ module Initialization = ) // - // start loading the solution + // start loading workspace // - logger.LogDebug("handleInitialized: post SolutionReloadRequest") - stateActor.Post(SolutionReloadRequest(TimeSpan.FromMilliseconds(100))) + stateActor.Post(WorkspaceReloadRequested(TimeSpan.FromMilliseconds(100))) logger.LogDebug("handleInitialized: Ok") diff --git a/src/CSharpLanguageServer/Handlers/InlayHint.fs b/src/CSharpLanguageServer/Handlers/InlayHint.fs index 356ece62..b32e3d87 100644 --- a/src/CSharpLanguageServer/Handlers/InlayHint.fs +++ b/src/CSharpLanguageServer/Handlers/InlayHint.fs @@ -13,6 +13,8 @@ open Ionide.LanguageServerProtocol.JsonRpc open CSharpLanguageServer.State open CSharpLanguageServer.Roslyn.Conversions open CSharpLanguageServer.Util +open CSharpLanguageServer.Lsp.Workspace + [] module InlayHint = @@ -239,7 +241,10 @@ module InlayHint = Some(U3.C2 inlayHintOptions) let handle (context: ServerRequestContext) (p: InlayHintParams) : AsyncLspResult = async { - match context.GetUserDocument p.TextDocument.Uri with + let wf, docForUri = + p.TextDocument.Uri |> workspaceDocument context.Workspace UserDocument + + match docForUri with | None -> return None |> LspResult.success | Some doc -> let! ct = Async.CancellationToken diff --git a/src/CSharpLanguageServer/Handlers/References.fs b/src/CSharpLanguageServer/Handlers/References.fs index 49d1d50d..ba332aa7 100644 --- a/src/CSharpLanguageServer/Handlers/References.fs +++ b/src/CSharpLanguageServer/Handlers/References.fs @@ -1,29 +1,42 @@ namespace CSharpLanguageServer.Handlers +open Microsoft.CodeAnalysis +open Microsoft.CodeAnalysis.FindSymbols open Ionide.LanguageServerProtocol.Types open Ionide.LanguageServerProtocol.JsonRpc open CSharpLanguageServer.State +open CSharpLanguageServer.Lsp.Workspace open CSharpLanguageServer.Roslyn.Conversions open CSharpLanguageServer.Logging [] module References = - let private logger = Logging.getLoggerByName "References" - - let provider (_: ClientCapabilities) : U2 option = Some(U2.C1 true) + let provider (_cc: ClientCapabilities) : U2 option = Some(U2.C1 true) let handle (context: ServerRequestContext) (p: ReferenceParams) : AsyncLspResult = async { - match! context.FindSymbol p.TextDocument.Uri p.Position with - | None -> return None |> LspResult.success - | Some symbol -> - let! locations = context.FindReferences symbol p.Context.IncludeDeclaration + let! ct = Async.CancellationToken + + match! workspaceDocumentSymbol context.Workspace AnyDocument p.TextDocument.Uri p.Position with + | Some wf, Some(symbol, _, _) -> + let! refs = + SymbolFinder.FindReferencesAsync(symbol, wf.Solution.Value, cancellationToken = ct) + |> Async.AwaitTask + + let locationsFromReferencedSym (r: ReferencedSymbol) = + let locations = r.Locations |> Seq.map _.Location + + match p.Context.IncludeDeclaration with + | true -> locations |> Seq.append r.Definition.Locations + | false -> locations return - locations + refs + |> Seq.collect locationsFromReferencedSym |> Seq.choose Location.fromRoslynLocation |> Seq.distinct |> Seq.toArray |> Some |> LspResult.success + | _, _ -> return None |> LspResult.success } diff --git a/src/CSharpLanguageServer/Handlers/Rename.fs b/src/CSharpLanguageServer/Handlers/Rename.fs index 32b85dfc..1ebfcb60 100644 --- a/src/CSharpLanguageServer/Handlers/Rename.fs +++ b/src/CSharpLanguageServer/Handlers/Rename.fs @@ -14,6 +14,8 @@ open CSharpLanguageServer.State open CSharpLanguageServer.Logging open CSharpLanguageServer.Roslyn.Conversions open CSharpLanguageServer.Util +open CSharpLanguageServer.Lsp.Workspace + [] module Rename = @@ -86,7 +88,11 @@ module Rename = | false -> Some(U2.C1 true) let prepare (context: ServerRequestContext) (p: PrepareRenameParams) : AsyncLspResult = async { - match context.GetUserDocument p.TextDocument.Uri with + + let wf, docForUri = + p.TextDocument.Uri |> workspaceDocument context.Workspace UserDocument + + match docForUri with | None -> return None |> LspResult.success | Some doc -> let! ct = Async.CancellationToken @@ -142,9 +148,8 @@ module Rename = } let handle (context: ServerRequestContext) (p: RenameParams) : AsyncLspResult = async { - match! context.FindSymbol' p.TextDocument.Uri p.Position with - | None -> return None |> LspResult.success - | Some(symbol, project, _) -> + match! workspaceDocumentSymbol context.Workspace AnyDocument p.TextDocument.Uri p.Position with + | Some wf, Some(symbol, project, _) -> let! ct = Async.CancellationToken let originalSolution = project.Solution @@ -158,12 +163,18 @@ module Rename = ) |> Async.AwaitTask + let! docTextEdit = - lspDocChangesFromSolutionDiff ct originalSolution updatedSolution (fun uri -> - context.OpenDocs.TryFind uri |> Option.map _.Version) + lspDocChangesFromSolutionDiff + ct + originalSolution + updatedSolution + (workspaceDocumentVersion context.Workspace) return WorkspaceEdit.Create(docTextEdit, context.ClientCapabilities) |> Some |> LspResult.success + + | _, _ -> return None |> LspResult.success } diff --git a/src/CSharpLanguageServer/Handlers/SemanticTokens.fs b/src/CSharpLanguageServer/Handlers/SemanticTokens.fs index 9afc6107..256d71b8 100644 --- a/src/CSharpLanguageServer/Handlers/SemanticTokens.fs +++ b/src/CSharpLanguageServer/Handlers/SemanticTokens.fs @@ -10,6 +10,8 @@ open Microsoft.CodeAnalysis.Text open CSharpLanguageServer.State open CSharpLanguageServer.Util open CSharpLanguageServer.Roslyn.Conversions +open CSharpLanguageServer.Lsp.Workspace + [] module SemanticTokens = @@ -52,14 +54,14 @@ module SemanticTokens = classificationTypeMap |> Map.values |> Seq.distinct - |> flip Seq.zip (Seq.initInfinite uint32) + |> (fun x -> Seq.zip x (Seq.initInfinite uint32)) |> Map.ofSeq let private semanticTokenModifierMap = classificationModifierMap |> Map.values |> Seq.distinct - |> flip Seq.zip (Seq.initInfinite uint32) + |> (fun x -> Seq.zip x (Seq.initInfinite uint32)) |> Map.ofSeq let private semanticTokenTypes = @@ -75,12 +77,12 @@ module SemanticTokens = let private getSemanticTokenIdFromClassification (classification: string) = classificationTypeMap |> Map.tryFind classification - |> Option.bind (flip Map.tryFind semanticTokenTypeMap) + |> Option.bind (fun x -> Map.tryFind x semanticTokenTypeMap) let private getSemanticTokenModifierFlagFromClassification (classification: string) = classificationModifierMap |> Map.tryFind classification - |> Option.bind (flip Map.tryFind semanticTokenModifierMap) + |> Option.bind (fun x -> Map.tryFind x semanticTokenModifierMap) |> Option.defaultValue 0u |> int32 |> (<<<) 1u @@ -113,7 +115,7 @@ module SemanticTokens = (range: Range option) : AsyncLspResult = async { - let docMaybe = context.GetUserDocument uri + let wf, docMaybe = uri |> workspaceDocument context.Workspace UserDocument match docMaybe with | None -> return None |> LspResult.success diff --git a/src/CSharpLanguageServer/Handlers/SignatureHelp.fs b/src/CSharpLanguageServer/Handlers/SignatureHelp.fs index 6eb32b21..079d0386 100644 --- a/src/CSharpLanguageServer/Handlers/SignatureHelp.fs +++ b/src/CSharpLanguageServer/Handlers/SignatureHelp.fs @@ -1,11 +1,8 @@ namespace CSharpLanguageServer.Handlers -open System - open Microsoft.CodeAnalysis open Microsoft.CodeAnalysis.CSharp open Microsoft.CodeAnalysis.CSharp.Syntax -open Ionide.LanguageServerProtocol.Server open Ionide.LanguageServerProtocol.Types open Ionide.LanguageServerProtocol.JsonRpc @@ -13,7 +10,7 @@ open CSharpLanguageServer open CSharpLanguageServer.State open CSharpLanguageServer.Util open CSharpLanguageServer.Roslyn.Conversions -open CSharpLanguageServer.Types +open CSharpLanguageServer.Lsp.Workspace module SignatureInformation = let internal fromMethod (m: IMethodSymbol) = @@ -43,7 +40,7 @@ module SignatureHelp = // Algorithm from omnisharp-roslyn (https://github.com/OmniSharp/omnisharp-roslyn/blob/2d582b05839dbd23baf6e78fa2279163723a824c/src/OmniSharp.Roslyn.CSharp/Services/Signatures/SignatureHelpService.cs#L139C1-L166C10) let private methodScore (types: TypeInfo list) (m: IMethodSymbol) = - let score (invocation: TypeInfo) (definition: IParameterSymbol) = + let score (invocation: TypeInfo, definition: IParameterSymbol) = if isNull invocation.ConvertedType then 1 else if SymbolEqualityComparer.Default.Equals(invocation.ConvertedType, definition.Type) then @@ -54,16 +51,17 @@ module SignatureHelp = if m.Parameters.Length < types.Length then Microsoft.FSharp.Core.int.MinValue else - Seq.zip types m.Parameters |> Seq.map (uncurry score) |> Seq.sum + Seq.zip types m.Parameters |> Seq.map score |> Seq.sum - let provider (_: ClientCapabilities) : SignatureHelpOptions option = + let provider (_cc: ClientCapabilities) : SignatureHelpOptions option = { TriggerCharacters = Some([| "("; ","; "<"; "{"; "[" |]) WorkDoneProgress = None RetriggerCharacters = None } |> Some let handle (context: ServerRequestContext) (p: SignatureHelpParams) : AsyncLspResult = async { - let docMaybe = context.GetUserDocument p.TextDocument.Uri + let wf, docMaybe = + p.TextDocument.Uri |> workspaceDocument context.Workspace UserDocument match docMaybe with | None -> return None |> LspResult.success @@ -72,7 +70,7 @@ module SignatureHelp = let! sourceText = doc.GetTextAsync(ct) |> Async.AwaitTask let! semanticModel = doc.GetSemanticModelAsync(ct) |> Async.AwaitTask - let position = Position.toRoslynPosition sourceText.Lines p.Position + let position = p.Position |> Position.toRoslynPosition sourceText.Lines let! syntaxTree = doc.GetSyntaxTreeAsync(ct) |> Async.AwaitTask let! root = syntaxTree.GetRootAsync(ct) |> Async.AwaitTask diff --git a/src/CSharpLanguageServer/Handlers/TextDocumentSync.fs b/src/CSharpLanguageServer/Handlers/TextDocumentSync.fs index 31498973..c113dcc3 100644 --- a/src/CSharpLanguageServer/Handlers/TextDocumentSync.fs +++ b/src/CSharpLanguageServer/Handlers/TextDocumentSync.fs @@ -10,9 +10,11 @@ open CSharpLanguageServer open CSharpLanguageServer.Roslyn.Conversions open CSharpLanguageServer.State open CSharpLanguageServer.State.ServerState -open CSharpLanguageServer.Roslyn.Symbol open CSharpLanguageServer.Roslyn.Solution +open CSharpLanguageServer.Lsp.Workspace open CSharpLanguageServer.Logging +open CSharpLanguageServer.Lsp.Workspace + [] module TextDocumentSync = @@ -44,35 +46,48 @@ module TextDocumentSync = |> Some - let didOpen (context: ServerRequestContext) (openParams: DidOpenTextDocumentParams) : Async> = - match context.GetDocumentForUriOfType AnyDocument openParams.TextDocument.Uri with - | Some(doc, docType) -> + let didOpen (context: ServerRequestContext) (p: DidOpenTextDocumentParams) : Async> = + let wf, docAndDocTypeForUri = + p.TextDocument.Uri |> workspaceDocumentDetails context.Workspace AnyDocument + + match wf, docAndDocTypeForUri with + | Some(wf), Some(doc, docType) -> match docType with | UserDocument -> // we want to load the document in case it has been changed since we have the solution loaded // also, as a bonus we can recover from corrupted document view in case document in roslyn solution // went out of sync with editor - let updatedDoc = SourceText.From(openParams.TextDocument.Text) |> doc.WithText + let updatedDoc = SourceText.From(p.TextDocument.Text) |> doc.WithText + + context.Emit(DocumentOpened(p.TextDocument.Uri, p.TextDocument.Version, DateTime.Now)) - context.Emit(OpenDocAdd(openParams.TextDocument.Uri, openParams.TextDocument.Version, DateTime.Now)) - context.Emit(SolutionChange updatedDoc.Project.Solution) + context.Emit( + WorkspaceFolderChange + { wf with + Solution = Some updatedDoc.Project.Solution } + ) Ok() |> async.Return | _ -> Ok() |> async.Return - | None -> - let docFilePathMaybe = Util.tryParseFileUri openParams.TextDocument.Uri + | Some wf, None -> + let docFilePathMaybe = Util.tryParseFileUri p.TextDocument.Uri match docFilePathMaybe with | Some docFilePath -> async { // ok, this document is not in solution, register a new document - let! newDocMaybe = solutionTryAddDocument docFilePath openParams.TextDocument.Text context.Solution + let! newDocMaybe = solutionTryAddDocument docFilePath p.TextDocument.Text wf.Solution.Value match newDocMaybe with | Some newDoc -> - context.Emit(OpenDocAdd(openParams.TextDocument.Uri, openParams.TextDocument.Version, DateTime.Now)) - context.Emit(SolutionChange newDoc.Project.Solution) + context.Emit(DocumentOpened(p.TextDocument.Uri, p.TextDocument.Version, DateTime.Now)) + + context.Emit( + WorkspaceFolderChange + { wf with + Solution = Some newDoc.Project.Solution } + ) | None -> () @@ -81,35 +96,40 @@ module TextDocumentSync = | None -> Ok() |> async.Return - let didChange (context: ServerRequestContext) (changeParams: DidChangeTextDocumentParams) : Async> = async { - let docMaybe = context.GetUserDocument changeParams.TextDocument.Uri + | _, _ -> Ok() |> async.Return + + let didChange (context: ServerRequestContext) (p: DidChangeTextDocumentParams) : Async> = async { + let wf, docMaybe = + p.TextDocument.Uri |> workspaceDocument context.Workspace UserDocument - match docMaybe with - | None -> () - | Some doc -> + match wf, docMaybe with + | Some wf, Some doc -> let! ct = Async.CancellationToken let! sourceText = doc.GetTextAsync(ct) |> Async.AwaitTask //logMessage (sprintf "TextDocumentDidChange: changeParams: %s" (string changeParams)) //logMessage (sprintf "TextDocumentDidChange: sourceText: %s" (string sourceText)) let updatedSourceText = - sourceText - |> applyLspContentChangesOnRoslynSourceText changeParams.ContentChanges + sourceText |> applyLspContentChangesOnRoslynSourceText p.ContentChanges let updatedDoc = doc.WithText(updatedSourceText) //logMessage (sprintf "TextDocumentDidChange: newSourceText: %s" (string updatedSourceText)) - let updatedSolution = updatedDoc.Project.Solution + let updatedWf = + { wf with + Solution = Some updatedDoc.Project.Solution } - context.Emit(SolutionChange updatedSolution) - context.Emit(OpenDocAdd(changeParams.TextDocument.Uri, changeParams.TextDocument.Version, DateTime.Now)) + context.Emit(WorkspaceFolderChange updatedWf) + context.Emit(DocumentOpened(p.TextDocument.Uri, p.TextDocument.Version, DateTime.Now)) + + | _, _ -> () return Ok() } - let didClose (context: ServerRequestContext) (closeParams: DidCloseTextDocumentParams) : Async> = - context.Emit(OpenDocRemove closeParams.TextDocument.Uri) + let didClose (context: ServerRequestContext) (p: DidCloseTextDocumentParams) : Async> = + context.Emit(DocumentClosed p.TextDocument.Uri) Ok() |> async.Return let willSave (_context: ServerRequestContext) (_p: WillSaveTextDocumentParams) : Async> = async { @@ -122,23 +142,30 @@ module TextDocumentSync = : AsyncLspResult = async { return LspResult.notImplemented } - let didSave (context: ServerRequestContext) (saveParams: DidSaveTextDocumentParams) : Async> = - // we need to add this file to solution if not already - let doc = context.GetDocument saveParams.TextDocument.Uri + let didSave (context: ServerRequestContext) (p: DidSaveTextDocumentParams) : Async> = + let wf, doc = p.TextDocument.Uri |> workspaceDocument context.Workspace AnyDocument + + match wf, doc with + | Some _, Some doc -> Ok() |> async.Return - match doc with - | Some _ -> Ok() |> async.Return + | Some wf, None -> async { + let docFilePath = Util.parseFileUri p.TextDocument.Uri - | None -> async { - let docFilePath = Util.parseFileUri saveParams.TextDocument.Uri - let! newDocMaybe = solutionTryAddDocument docFilePath saveParams.Text.Value context.Solution + // we need to add this file to solution if not already + let! newDocMaybe = solutionTryAddDocument docFilePath p.Text.Value wf.Solution.Value match newDocMaybe with | Some newDoc -> - context.Emit(OpenDocTouch(saveParams.TextDocument.Uri, DateTime.Now)) - context.Emit(SolutionChange newDoc.Project.Solution) + let updatedWf = + { wf with + Solution = Some newDoc.Project.Solution } + + context.Emit(DocumentTouched(p.TextDocument.Uri, DateTime.Now)) + context.Emit(WorkspaceFolderChange updatedWf) | None -> () return Ok() } + + | _, _ -> Ok() |> async.Return diff --git a/src/CSharpLanguageServer/Handlers/TypeDefinition.fs b/src/CSharpLanguageServer/Handlers/TypeDefinition.fs index b01005a5..f684613d 100644 --- a/src/CSharpLanguageServer/Handlers/TypeDefinition.fs +++ b/src/CSharpLanguageServer/Handlers/TypeDefinition.fs @@ -5,34 +5,42 @@ open Ionide.LanguageServerProtocol.Types open Ionide.LanguageServerProtocol.JsonRpc open CSharpLanguageServer.State +open CSharpLanguageServer.State.ServerState open CSharpLanguageServer.Util +open CSharpLanguageServer.Lsp.Workspace [] module TypeDefinition = - let provider (_: ClientCapabilities) : U3 option = + let provider (_cc: ClientCapabilities) : U3 option = Some(U3.C1 true) let handle (context: ServerRequestContext) (p: TypeDefinitionParams) : Async option>> = + async { - match! context.FindSymbol' p.TextDocument.Uri p.Position with - | None -> return None |> LspResult.success - | Some(symbol, project, _) -> + match! workspaceDocumentSymbol context.Workspace AnyDocument p.TextDocument.Uri p.Position with + | Some wf, Some(symbol, project, _) -> let typeSymbol = match symbol with - | :? ILocalSymbol as localSymbol -> [ localSymbol.Type ] - | :? IFieldSymbol as fieldSymbol -> [ fieldSymbol.Type ] - | :? IPropertySymbol as propertySymbol -> [ propertySymbol.Type ] - | :? IParameterSymbol as parameterSymbol -> [ parameterSymbol.Type ] - | _ -> [] - - let! locations = - typeSymbol - |> Seq.map (flip context.ResolveSymbolLocations (Some project)) - |> Async.Parallel - |> Async.map (Seq.collect id >> Seq.toArray) - - return locations |> Declaration.C2 |> U2.C1 |> Some |> LspResult.success + | :? ILocalSymbol as localSymbol -> Some localSymbol.Type + | :? IFieldSymbol as fieldSymbol -> Some fieldSymbol.Type + | :? IPropertySymbol as propertySymbol -> Some propertySymbol.Type + | :? IParameterSymbol as parameterSymbol -> Some parameterSymbol.Type + | _ -> None + + let! locations, wf = + match typeSymbol with + | None -> async.Return([], wf) + | Some symbol -> async { + let! aggregatedLspLocations, updatedWf = workspaceFolderSymbolLocations symbol project wf + + context.Emit(WorkspaceFolderChange updatedWf) + return (aggregatedLspLocations, updatedWf) + } + + return locations |> Seq.toArray |> Declaration.C2 |> U2.C1 |> Some |> LspResult.success + + | _, _ -> return None |> LspResult.success } diff --git a/src/CSharpLanguageServer/Handlers/TypeHierarchy.fs b/src/CSharpLanguageServer/Handlers/TypeHierarchy.fs index 7fbae774..dcd8d84c 100644 --- a/src/CSharpLanguageServer/Handlers/TypeHierarchy.fs +++ b/src/CSharpLanguageServer/Handlers/TypeHierarchy.fs @@ -1,12 +1,15 @@ namespace CSharpLanguageServer.Handlers open Microsoft.CodeAnalysis +open Microsoft.CodeAnalysis.FindSymbols open Ionide.LanguageServerProtocol.Types open Ionide.LanguageServerProtocol.JsonRpc open CSharpLanguageServer.State +open CSharpLanguageServer.State.ServerState open CSharpLanguageServer.Roslyn.Conversions open CSharpLanguageServer.Util +open CSharpLanguageServer.Lsp.Workspace [] module TypeHierarchy = @@ -23,11 +26,20 @@ module TypeHierarchy = (p: TypeHierarchyPrepareParams) : AsyncLspResult = async { - match! context.FindSymbol p.TextDocument.Uri p.Position with - | Some symbol when isTypeSymbol symbol -> - let! itemList = TypeHierarchyItem.fromSymbol context.ResolveSymbolLocations symbol - return itemList |> List.toArray |> Some |> LspResult.success - | _ -> return None |> LspResult.success + match! workspaceDocumentSymbol context.Workspace AnyDocument p.TextDocument.Uri p.Position with + | Some wf, Some(symbol, project, _) when isTypeSymbol symbol -> + let! symLocations, updatedWf = workspaceFolderSymbolLocations symbol project wf + + context.Emit(WorkspaceFolderChange updatedWf) + + return + symLocations + |> Seq.map (TypeHierarchyItem.fromSymbolAndLocation symbol) + |> Seq.toArray + |> Some + |> LspResult.success + + | _, _ -> return None |> LspResult.success } let supertypes @@ -35,8 +47,8 @@ module TypeHierarchy = (p: TypeHierarchySupertypesParams) : AsyncLspResult = async { - match! context.FindSymbol p.Item.Uri p.Item.Range.Start with - | Some symbol when isTypeSymbol symbol -> + match! workspaceDocumentSymbol context.Workspace AnyDocument p.Item.Uri p.Item.Range.Start with + | Some wf, Some(symbol, project, _) when isTypeSymbol symbol -> let typeSymbol = symbol :?> INamedTypeSymbol let baseType = @@ -48,13 +60,24 @@ module TypeHierarchy = let interfaces = Seq.toList typeSymbol.Interfaces let supertypes = baseType @ interfaces - let! items = - supertypes - |> Seq.map (TypeHierarchyItem.fromSymbol context.ResolveSymbolLocations) - |> Async.Parallel + let items = System.Collections.Generic.List() + let mutable updatedWf = wf + + for typeSym in supertypes do + let! locations, wf = workspaceFolderSymbolLocations typeSym project updatedWf + + let typeSymItems = + locations |> Seq.map (TypeHierarchyItem.fromSymbolAndLocation typeSym) - return items |> Seq.collect id |> Seq.toArray |> Some |> LspResult.success - | _ -> return None |> LspResult.success + items.AddRange(typeSymItems) + + updatedWf <- wf + + context.Emit(WorkspaceFolderChange updatedWf) + + return items |> Seq.toArray |> Some |> LspResult.success + + | _, _ -> return None |> LspResult.success } let subtypes @@ -62,23 +85,53 @@ module TypeHierarchy = (p: TypeHierarchySubtypesParams) : AsyncLspResult = async { - match! context.FindSymbol p.Item.Uri p.Item.Range.Start with - | Some symbol when isTypeSymbol symbol -> + let! ct = Async.CancellationToken + + match! workspaceDocumentSymbol context.Workspace AnyDocument p.Item.Uri p.Item.Range.Start with + | Some wf, Some(symbol, project, _) when isTypeSymbol symbol -> let typeSymbol = symbol :?> INamedTypeSymbol // We only want immediately derived classes/interfaces/implementations here (we only need // subclasses not subclasses' subclasses) + let findDerivedClasses' (symbol: INamedTypeSymbol) (transitive: bool) : Async = + SymbolFinder.FindDerivedClassesAsync(symbol, wf.Solution.Value, transitive, cancellationToken = ct) + |> Async.AwaitTask + + let findDerivedInterfaces' (symbol: INamedTypeSymbol) (transitive: bool) : Async = + SymbolFinder.FindDerivedInterfacesAsync( + symbol, + wf.Solution.Value, + transitive, + cancellationToken = ct + ) + |> Async.AwaitTask + + let findImplementations' (symbol: INamedTypeSymbol) (transitive: bool) : Async = + SymbolFinder.FindImplementationsAsync(symbol, wf.Solution.Value, transitive, cancellationToken = ct) + |> Async.AwaitTask + let! subtypes = - [ context.FindDerivedClasses' typeSymbol false - context.FindDerivedInterfaces' typeSymbol false - context.FindImplementations' typeSymbol false ] + [ findDerivedClasses' typeSymbol false + findDerivedInterfaces' typeSymbol false + findImplementations' typeSymbol false ] |> Async.Parallel |> Async.map (Seq.collect id >> Seq.toList) - let! items = - subtypes - |> Seq.map (TypeHierarchyItem.fromSymbol context.ResolveSymbolLocations) - |> Async.Parallel + let items = System.Collections.Generic.List() + let mutable updatedWf = wf + + for typeSym in subtypes do + let! locations, wf = workspaceFolderSymbolLocations typeSym project updatedWf + + let typeSymItems = + locations |> Seq.map (TypeHierarchyItem.fromSymbolAndLocation typeSym) + + items.AddRange(typeSymItems) + + updatedWf <- wf + + context.Emit(WorkspaceFolderChange updatedWf) + + return items |> Seq.toArray |> Some |> LspResult.success - return items |> Seq.collect id |> Seq.toArray |> Some |> LspResult.success - | _ -> return None |> LspResult.success + | _, _ -> return None |> LspResult.success } diff --git a/src/CSharpLanguageServer/Handlers/Workspace.fs b/src/CSharpLanguageServer/Handlers/Workspace.fs index d7a5f9f0..dc6e4c19 100644 --- a/src/CSharpLanguageServer/Handlers/Workspace.fs +++ b/src/CSharpLanguageServer/Handlers/Workspace.fs @@ -11,10 +11,11 @@ open Microsoft.CodeAnalysis.Text open CSharpLanguageServer open CSharpLanguageServer.State open CSharpLanguageServer.State.ServerState -open CSharpLanguageServer.Roslyn.Symbol open CSharpLanguageServer.Roslyn.Solution open CSharpLanguageServer.Logging open CSharpLanguageServer.Types +open CSharpLanguageServer.Lsp.Workspace + [] module Workspace = @@ -25,12 +26,14 @@ module Workspace = FileOperations = None } |> Some + let dynamicRegistrationForDidChangeWatchedFiles (clientCapabilities: ClientCapabilities) = clientCapabilities.Workspace |> Option.bind _.DidChangeWatchedFiles |> Option.bind _.DynamicRegistration |> Option.defaultValue false + let didChangeWatchedFilesRegistration (clientCapabilities: ClientCapabilities) : Registration option = match dynamicRegistrationForDidChangeWatchedFiles clientCapabilities with | false -> None @@ -47,53 +50,86 @@ module Workspace = Method = "workspace/didChangeWatchedFiles" RegisterOptions = registerOptions |> serialize |> Some } + let private tryReloadDocumentOnUri logger (context: ServerRequestContext) uri = async { - match context.GetUserDocument uri with - | Some doc -> + let wf, doc = uri |> workspaceDocument context.Workspace UserDocument + + match wf, doc with + | Some wf, Some doc -> let fileText = uri |> Util.parseFileUri |> File.ReadAllText let updatedDoc = SourceText.From(fileText) |> doc.WithText - context.Emit(SolutionChange updatedDoc.Project.Solution) + let updatedWf = + { wf with + Solution = Some updatedDoc.Project.Solution } + + context.Emit(WorkspaceFolderChange updatedWf) - | None -> + | Some wf, None -> let docFilePathMaybe = uri |> Util.tryParseFileUri match docFilePathMaybe with | Some docFilePath -> // ok, this document is not on solution, register a new one let fileText = docFilePath |> File.ReadAllText - let! newDocMaybe = solutionTryAddDocument docFilePath fileText context.Solution + let! newDocMaybe = solutionTryAddDocument docFilePath fileText wf.Solution.Value match newDocMaybe with - | Some newDoc -> context.Emit(SolutionChange newDoc.Project.Solution) + | Some newDoc -> + let updatedWf = + { wf with + Solution = Some newDoc.Project.Solution } + + context.Emit(WorkspaceFolderChange updatedWf) | None -> () | None -> () + + | _, _ -> () } + let private removeDocument (context: ServerRequestContext) uri = - match context.GetUserDocument uri with - | Some existingDoc -> + let wf, doc = uri |> workspaceDocument context.Workspace UserDocument + + match wf, doc with + | Some wf, Some existingDoc -> let updatedProject = existingDoc.Project.RemoveDocument(existingDoc.Id) - context.Emit(SolutionChange updatedProject.Solution) - context.Emit(OpenDocRemove uri) - | None -> () + let updatedWf = + { wf with + Solution = Some updatedProject.Solution } + + context.Emit(WorkspaceFolderChange updatedWf) + context.Emit(DocumentClosed uri) + + | _, _ -> () + let didChangeWatchedFiles (context: ServerRequestContext) (p: DidChangeWatchedFilesParams) : Async> = + + let windowShowMessage (m: string) = + match context.State.LspClient with + | Some lspClient -> + lspClient.WindowShowMessage( + { Type = MessageType.Info + Message = sprintf "csharp-ls: %s" m } + ) + | None -> async.Return() + async { for change in p.Changes do match Path.GetExtension(change.Uri) with | ".csproj" -> - do! context.WindowShowMessage "change to .csproj detected, will reload solution" - context.Emit(SolutionReloadRequest(TimeSpan.FromSeconds(5: int64))) + do! windowShowMessage "change to .csproj detected, will reload solution" + context.Emit(WorkspaceReloadRequested(TimeSpan.FromSeconds(5: int64))) | ".sln" | ".slnx" -> - do! context.WindowShowMessage "change to .sln detected, will reload solution" - context.Emit(SolutionReloadRequest(TimeSpan.FromSeconds(5: int64))) + do! windowShowMessage "change to .sln detected, will reload solution" + context.Emit(WorkspaceReloadRequested(TimeSpan.FromSeconds(5: int64))) | ".cs" -> match change.Type with @@ -107,6 +143,7 @@ module Workspace = return Ok() } + let didChangeConfiguration (context: ServerRequestContext) (configParams: DidChangeConfigurationParams) diff --git a/src/CSharpLanguageServer/Handlers/WorkspaceSymbol.fs b/src/CSharpLanguageServer/Handlers/WorkspaceSymbol.fs index 5defb325..487f8176 100644 --- a/src/CSharpLanguageServer/Handlers/WorkspaceSymbol.fs +++ b/src/CSharpLanguageServer/Handlers/WorkspaceSymbol.fs @@ -3,6 +3,7 @@ namespace CSharpLanguageServer.Handlers open System open Microsoft.CodeAnalysis +open Microsoft.CodeAnalysis.FindSymbols open Ionide.LanguageServerProtocol.Types open Ionide.LanguageServerProtocol.JsonRpc @@ -11,16 +12,40 @@ open CSharpLanguageServer.Roslyn.Conversions [] module WorkspaceSymbol = - let provider (_: ClientCapabilities) : U2 option = Some(U2.C1 true) + let provider (_cc: ClientCapabilities) : U2 option = Some(U2.C1 true) let handle (context: ServerRequestContext) (p: WorkspaceSymbolParams) : AsyncLspResult option> = async { + let! ct = Async.CancellationToken + let pattern = if String.IsNullOrEmpty(p.Query) then None else Some p.Query - let! symbols = context.FindSymbols pattern + let! symbols = + match context.Workspace.SingletonFolder.Solution with + | None -> async.Return Seq.empty + | Some solution -> + match pattern with + | Some pat -> + SymbolFinder.FindSourceDeclarationsWithPatternAsync( + solution, + pat, + SymbolFilter.TypeAndMember, + cancellationToken = ct + ) + |> Async.AwaitTask + | None -> + let true' = System.Func(fun _ -> true) + + SymbolFinder.FindSourceDeclarationsAsync( + solution, + true', + SymbolFilter.TypeAndMember, + cancellationToken = ct + ) + |> Async.AwaitTask return symbols diff --git a/src/CSharpLanguageServer/Lsp/Server.fs b/src/CSharpLanguageServer/Lsp/Server.fs index 7be56891..180305a9 100644 --- a/src/CSharpLanguageServer/Lsp/Server.fs +++ b/src/CSharpLanguageServer/Lsp/Server.fs @@ -39,9 +39,6 @@ type CSharpLspServer(lspClient: CSharpLspClient, settings: ServerSettings) = Settings = settings } ) - let getDocumentForUriFromCurrentState docType uri = - stateActor.PostAndAsyncReply(fun rc -> GetDocumentOfTypeForUri(docType, uri, rc)) - let mutable timer: Threading.Timer option = None let setupTimer () = @@ -73,7 +70,7 @@ type CSharpLspServer(lspClient: CSharpLspClient, settings: ServerSettings) = let! state = stateActor.PostAndAsyncReply GetState - let context = ServerRequestContext(requestId, state, stateActor.Post) + let context = ServerRequestContext(state, stateActor.Post) return! handlerFn context param } diff --git a/src/CSharpLanguageServer/Lsp/Workspace.fs b/src/CSharpLanguageServer/Lsp/Workspace.fs new file mode 100644 index 00000000..0c3f50b5 --- /dev/null +++ b/src/CSharpLanguageServer/Lsp/Workspace.fs @@ -0,0 +1,235 @@ +module CSharpLanguageServer.Lsp.Workspace + +open System +open System.IO + +open Microsoft.CodeAnalysis +open Microsoft.CodeAnalysis.FindSymbols +open Ionide.LanguageServerProtocol.Types +open Microsoft.Extensions.Logging + +open CSharpLanguageServer.Util +open CSharpLanguageServer.Types +open CSharpLanguageServer.Logging +open CSharpLanguageServer.Roslyn.Document +open CSharpLanguageServer.Roslyn.Symbol +open CSharpLanguageServer.Roslyn.Conversions + +let logger = Logging.getLoggerByName "Lsp.Workspace" + +type LspWorkspaceDecompiledMetadataDocument = + { Metadata: CSharpMetadataInformation + Document: Document } + +type LspWorkspaceFolder = + { Uri: string + Name: string + RoslynWorkspace: Workspace option + Solution: Solution option + DecompiledMetadata: Map } + + static member Empty = + { Uri = Directory.GetCurrentDirectory() |> Uri.fromPath + Name = "(no name)" + RoslynWorkspace = None + Solution = None + DecompiledMetadata = Map.empty } + +type LspWorkspaceDocumentType = + | UserDocument // user Document from solution, on disk + | DecompiledDocument // Document decompiled from metadata, readonly + | AnyDocument + +let workspaceFolderResolveSymbolLocation + (project: Microsoft.CodeAnalysis.Project) + (symbol: Microsoft.CodeAnalysis.ISymbol) + (l: Microsoft.CodeAnalysis.Location) + (folder: LspWorkspaceFolder) + = + async { + match l.IsInMetadata, l.IsInSource with + | true, _ -> + let! ct = Async.CancellationToken + let! compilation = project.GetCompilationAsync(ct) |> Async.AwaitTask + + let fullName = + symbol |> symbolGetContainingTypeOrThis |> symbolGetFullReflectionName + + let containingAssemblyName = + l.MetadataModule |> nonNull "l.MetadataModule" |> _.ContainingAssembly.Name + + let uri = + $"csharp:/metadata/projects/{project.Name}/assemblies/{containingAssemblyName}/symbols/{fullName}.cs" + + let mdDocument, folder = + match Map.tryFind uri folder.DecompiledMetadata with + | Some value -> (value.Document, folder) + | None -> + let (documentFromMd, text) = documentFromMetadata compilation project l fullName + + let csharpMetadata = + { ProjectName = project.Name + AssemblyName = containingAssemblyName + SymbolName = fullName + Source = text } + + let md = + { Metadata = csharpMetadata + Document = documentFromMd } + + let updatedFolder = + { folder with + DecompiledMetadata = Map.add uri md folder.DecompiledMetadata } + + (documentFromMd, updatedFolder) + + // figure out location on the document (approx implementation) + let! syntaxTree = mdDocument.GetSyntaxTreeAsync(ct) |> Async.AwaitTask + + let collector = DocumentSymbolCollectorForMatchingSymbolName(uri, symbol) + let! root = syntaxTree.GetRootAsync(ct) |> Async.AwaitTask + collector.Visit(root) + + let fallbackLocationInMetadata = + { Uri = uri + Range = + { Start = { Line = 0u; Character = 0u } + End = { Line = 0u; Character = 1u } } } + + return + match collector.GetLocations() with + | [] -> [ fallbackLocationInMetadata ], folder + | ls -> ls, folder + + | false, true -> + return + match (Location.fromRoslynLocation l) with + | Some loc -> [ loc ], folder + | None -> [], folder + + | _, _ -> return [], folder + } + +/// The process of retrieving locations may update LspWorkspaceFolder itself, +/// thus return value is a pair of symbol location list * LspWorkspaceFolder +let workspaceFolderSymbolLocations + (symbol: Microsoft.CodeAnalysis.ISymbol) + (project: Microsoft.CodeAnalysis.Project) + folder + = + async { + let mutable wf = folder + let mutable aggregatedLspLocations = [] + + for l in symbol.Locations do + let! symLspLocations, updatedWf = workspaceFolderResolveSymbolLocation project symbol l wf + + aggregatedLspLocations <- aggregatedLspLocations @ symLspLocations + wf <- updatedWf + + return aggregatedLspLocations, wf + } + +type LspWorkspaceOpenDocInfo = { Version: int; Touched: DateTime } + +type LspWorkspace = + { Folders: LspWorkspaceFolder list + OpenDocs: Map } + + static member Empty = { Folders = []; OpenDocs = Map.empty } + + member this.SingletonFolder = this.Folders |> Seq.exactlyOne + + member this.WithSingletonFolderUpdated(update: LspWorkspaceFolder -> LspWorkspaceFolder) = + let updatedFolders = + this.Folders + |> Seq.tryExactlyOne + |> Option.defaultValue LspWorkspaceFolder.Empty + |> update + |> List.singleton + + { this with Folders = updatedFolders } + + member this.WithSolution(solution: Solution option) = + this.WithSingletonFolderUpdated(fun f -> { f with Solution = solution }) + +let workspaceFrom (workspaceFolders: WorkspaceFolder list) = + // TODO: currently only the first workspace folder is taken into account (see Seq.take 1) + match workspaceFolders.Length with + | 0 -> failwith "workspaceFrom: at least 1 workspace folder must be provided!" + | 1 -> () + | _ -> logger.LogWarning("workspaceFrom: only the first WorkspaceFolder will be loaded!") + + let folders = + workspaceFolders + |> Seq.take 1 + |> Seq.map (fun f -> + { LspWorkspaceFolder.Empty with + Uri = f.Uri + Name = f.Name }) + |> List.ofSeq + + { LspWorkspace.Empty with + Folders = folders } + +let workspaceFolder (workspace: LspWorkspace) _uri = Some workspace.SingletonFolder + +let workspaceDocumentDetails (workspace: LspWorkspace) docType (u: string) = + let uri = Uri(u.Replace("%3A", ":", true, null)) + + let wf = workspaceFolder workspace uri + let solution = wf |> Option.bind _.Solution + + let docAndDocType = + match wf, solution with + | Some wf, Some solution -> + let matchingUserDocuments = + solution.Projects + |> Seq.collect (fun p -> p.Documents) + |> Seq.filter (fun d -> Uri(d.FilePath, UriKind.Absolute) = uri) + |> List.ofSeq + + let matchingUserDocumentMaybe = + match matchingUserDocuments with + | [ d ] -> Some(d, UserDocument) + | _ -> None + + let matchingDecompiledDocumentMaybe = + wf.DecompiledMetadata + |> Map.tryFind u + |> Option.map (fun x -> (x.Document, DecompiledDocument)) + + match docType with + | UserDocument -> matchingUserDocumentMaybe + | DecompiledDocument -> matchingDecompiledDocumentMaybe + | AnyDocument -> matchingUserDocumentMaybe |> Option.orElse matchingDecompiledDocumentMaybe + + | _, _ -> None + + wf, docAndDocType + +let workspaceDocument workspace docType (u: string) = + let wf, docAndType = workspaceDocumentDetails workspace docType u + let doc = docAndType |> Option.map fst + wf, doc + +let workspaceDocumentSymbol workspace docType (uri: DocumentUri) (pos: Ionide.LanguageServerProtocol.Types.Position) = async { + let wf, docForUri = uri |> workspaceDocument workspace AnyDocument + + match wf, docForUri with + | Some wf, Some doc -> + let! ct = Async.CancellationToken + let! sourceText = doc.GetTextAsync(ct) |> Async.AwaitTask + let position = Position.toRoslynPosition sourceText.Lines pos + let! symbol = SymbolFinder.FindSymbolAtPositionAsync(doc, position, ct) |> Async.AwaitTask + + let symbolInfo = + symbol |> Option.ofObj |> Option.map (fun sym -> sym, doc.Project, Some doc) + + return Some wf, symbolInfo + + | wf, _ -> return (wf, None) +} + +let workspaceDocumentVersion workspace uri = + uri |> workspace.OpenDocs.TryFind |> Option.map _.Version diff --git a/src/CSharpLanguageServer/Roslyn/Conversions.fs b/src/CSharpLanguageServer/Roslyn/Conversions.fs index fe61bf7d..ba6211e5 100644 --- a/src/CSharpLanguageServer/Roslyn/Conversions.fs +++ b/src/CSharpLanguageServer/Roslyn/Conversions.fs @@ -149,13 +149,6 @@ module CallHierarchyItem = SelectionRange = location.Range Data = None } - let fromSymbol - (wmResolveSymbolLocations: ISymbol -> Project option -> Async>) - (symbol: ISymbol) - : Async = - wmResolveSymbolLocations symbol None - |> Async.map (List.map (fromSymbolAndLocation symbol)) - module TypeHierarchyItem = let private displayStyle = SymbolDisplayFormat( @@ -188,13 +181,6 @@ module TypeHierarchyItem = SelectionRange = location.Range Data = None } - let fromSymbol - (wmResolveSymbolLocations: ISymbol -> Project option -> Async>) - (symbol: ISymbol) - : Async = - wmResolveSymbolLocations symbol None - |> Async.map (List.map (fromSymbolAndLocation symbol)) - module SymbolInformation = let fromSymbol (format: SymbolDisplayFormat) (symbol: ISymbol) : SymbolInformation list = let toSymbolInformation loc = diff --git a/src/CSharpLanguageServer/State/ServerRequestContext.fs b/src/CSharpLanguageServer/State/ServerRequestContext.fs index 6c6ed43c..9337c938 100644 --- a/src/CSharpLanguageServer/State/ServerRequestContext.fs +++ b/src/CSharpLanguageServer/State/ServerRequestContext.fs @@ -1,289 +1,9 @@ namespace CSharpLanguageServer.State -open Microsoft.CodeAnalysis -open Microsoft.CodeAnalysis.FindSymbols -open Ionide.LanguageServerProtocol.Types - open CSharpLanguageServer.State.ServerState -open CSharpLanguageServer.Types -open CSharpLanguageServer.Roslyn.Document -open CSharpLanguageServer.Roslyn.Symbol -open CSharpLanguageServer.Roslyn.Solution -open CSharpLanguageServer.Roslyn.Conversions -open CSharpLanguageServer.Util -open CSharpLanguageServer.Logging - -type ServerRequestContext(requestId: int, state: ServerState, emitServerEvent) = - let mutable solutionMaybe = state.Solution - let logger = Logging.getLoggerByName "ServerRequestContext" - - member _.RequestId = requestId +type ServerRequestContext(state: ServerState, emit: ServerStateEvent -> unit) = member _.State = state + member _.Workspace = state.Workspace member _.ClientCapabilities = state.ClientCapabilities - member _.Solution = solutionMaybe.Value - member _.OpenDocs = state.OpenDocs - member _.DecompiledMetadata = state.DecompiledMetadata - - member _.WindowShowMessage(m: string) = - match state.LspClient with - | Some lspClient -> - lspClient.WindowShowMessage( - { Type = MessageType.Info - Message = sprintf "csharp-ls: %s" m } - ) - | None -> async.Return() - - member this.GetDocumentForUriOfType = getDocumentForUriOfType this.State - - member this.GetUserDocument(u: string) = - this.GetDocumentForUriOfType UserDocument u |> Option.map fst - - member this.GetDocument(u: string) = - this.GetDocumentForUriOfType AnyDocument u |> Option.map fst - - member _.Emit ev = - match ev with - | SolutionChange newSolution -> solutionMaybe <- Some newSolution - | _ -> () - - emitServerEvent ev - - member this.EmitMany es = - for e in es do - this.Emit e - - member private this.ResolveSymbolLocation - (project: Microsoft.CodeAnalysis.Project option) - sym - (l: Microsoft.CodeAnalysis.Location) - = - async { - match l.IsInMetadata, l.IsInSource, project with - | true, _, Some project -> - let! ct = Async.CancellationToken - let! compilation = project.GetCompilationAsync(ct) |> Async.AwaitTask - - let fullName = sym |> symbolGetContainingTypeOrThis |> symbolGetFullReflectionName - - let containingAssemblyName = - l.MetadataModule |> nonNull "l.MetadataModule" |> _.ContainingAssembly.Name - - let uri = - $"csharp:/metadata/projects/{project.Name}/assemblies/{containingAssemblyName}/symbols/{fullName}.cs" - - let mdDocument, stateChanges = - match Map.tryFind uri state.DecompiledMetadata with - | Some value -> (value.Document, []) - | None -> - let (documentFromMd, text) = documentFromMetadata compilation project l fullName - - let csharpMetadata = - { ProjectName = project.Name - AssemblyName = containingAssemblyName - SymbolName = fullName - Source = text } - - (documentFromMd, - [ DecompiledMetadataAdd( - uri, - { Metadata = csharpMetadata - Document = documentFromMd } - ) ]) - - this.EmitMany stateChanges - - // figure out location on the document (approx implementation) - let! syntaxTree = mdDocument.GetSyntaxTreeAsync(ct) |> Async.AwaitTask - - let collector = DocumentSymbolCollectorForMatchingSymbolName(uri, sym) - let! root = syntaxTree.GetRootAsync(ct) |> Async.AwaitTask - collector.Visit(root) - - let fallbackLocationInMetadata = - { Uri = uri - Range = - { Start = { Line = 0u; Character = 0u } - End = { Line = 0u; Character = 1u } } } - - return - match collector.GetLocations() with - | [] -> [ fallbackLocationInMetadata ] - | ls -> ls - - | false, true, _ -> - return - match (Location.fromRoslynLocation l) with - | Some loc -> [ loc ] - | None -> [] - - | _, _, _ -> return [] - } - - member this.ResolveSymbolLocations - (symbol: Microsoft.CodeAnalysis.ISymbol) - (project: Microsoft.CodeAnalysis.Project option) - = - async { - let mutable aggregatedLspLocations = [] - - for l in symbol.Locations do - let! symLspLocations = this.ResolveSymbolLocation project symbol l - - aggregatedLspLocations <- aggregatedLspLocations @ symLspLocations - - return aggregatedLspLocations - } - - member this.FindSymbol' (uri: DocumentUri) (pos: Position) : Async<(ISymbol * Project * Document option) option> = async { - match this.GetDocument uri with - | None -> return None - | Some doc -> - let! ct = Async.CancellationToken - let! sourceText = doc.GetTextAsync(ct) |> Async.AwaitTask - let position = Position.toRoslynPosition sourceText.Lines pos - let! symbol = SymbolFinder.FindSymbolAtPositionAsync(doc, position, ct) |> Async.AwaitTask - return symbol |> Option.ofObj |> Option.map (fun sym -> sym, doc.Project, Some doc) - } - - member this.FindSymbol (uri: DocumentUri) (pos: Position) : Async = - this.FindSymbol' uri pos |> Async.map (Option.map (fun (sym, _, _) -> sym)) - - member private __._FindDerivedClasses (symbol: INamedTypeSymbol) (transitive: bool) : Async = async { - match state.Solution with - | None -> return [] - | Some currentSolution -> - let! ct = Async.CancellationToken - - return! - SymbolFinder.FindDerivedClassesAsync(symbol, currentSolution, transitive, cancellationToken = ct) - |> Async.AwaitTask - } - - member private __._FindDerivedInterfaces - (symbol: INamedTypeSymbol) - (transitive: bool) - : Async = - async { - match state.Solution with - | None -> return [] - | Some currentSolution -> - let! ct = Async.CancellationToken - - return! - SymbolFinder.FindDerivedInterfacesAsync(symbol, currentSolution, transitive, cancellationToken = ct) - |> Async.AwaitTask - } - - member __.FindImplementations(symbol: ISymbol) : Async = async { - match state.Solution with - | None -> return [] - | Some currentSolution -> - let! ct = Async.CancellationToken - - return! - SymbolFinder.FindImplementationsAsync(symbol, currentSolution, cancellationToken = ct) - |> Async.AwaitTask - } - - member __.FindImplementations' (symbol: INamedTypeSymbol) (transitive: bool) : Async = async { - match state.Solution with - | None -> return [] - | Some currentSolution -> - let! ct = Async.CancellationToken - - return! - SymbolFinder.FindImplementationsAsync(symbol, currentSolution, transitive, cancellationToken = ct) - |> Async.AwaitTask - } - - member this.FindDerivedClasses(symbol: INamedTypeSymbol) : Async = - this._FindDerivedClasses symbol true - - member this.FindDerivedClasses' (symbol: INamedTypeSymbol) (transitive: bool) : Async = - this._FindDerivedClasses symbol transitive - - member this.FindDerivedInterfaces(symbol: INamedTypeSymbol) : Async = - this._FindDerivedInterfaces symbol true - - member this.FindDerivedInterfaces' (symbol: INamedTypeSymbol) (transitive: bool) : Async = - this._FindDerivedInterfaces symbol transitive - - member __.FindCallers(symbol: ISymbol) : Async = async { - match state.Solution with - | None -> return [] - | Some currentSolution -> - let! ct = Async.CancellationToken - - return! - SymbolFinder.FindCallersAsync(symbol, currentSolution, cancellationToken = ct) - |> Async.AwaitTask - } - - member this.ResolveTypeSymbolLocations - (project: Microsoft.CodeAnalysis.Project) - (symbols: Microsoft.CodeAnalysis.ITypeSymbol list) - = - async { - let mutable aggregatedLspLocations = [] - - for sym in symbols do - for l in sym.Locations do - let! symLspLocations = this.ResolveSymbolLocation (Some project) sym l - - aggregatedLspLocations <- aggregatedLspLocations @ symLspLocations - - return aggregatedLspLocations - } - - member this.FindSymbols(pattern: string option) : Async = async { - let findTask ct = - match pattern with - | Some pat -> - fun (sln: Solution) -> - SymbolFinder.FindSourceDeclarationsWithPatternAsync( - sln, - pat, - SymbolFilter.TypeAndMember, - cancellationToken = ct - ) - | None -> - let true' = System.Func(fun _ -> true) - - 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 - } - - member this.FindReferences (symbol: ISymbol) (withDefinition: bool) : Async = async { - match this.State.Solution with - | None -> return [] - | Some solution -> - let! ct = Async.CancellationToken - - let locationsFromReferencedSym (r: ReferencedSymbol) = - let locations = r.Locations |> Seq.map _.Location - - match withDefinition with - | true -> locations |> Seq.append r.Definition.Locations - | false -> locations - - let! refs = - SymbolFinder.FindReferencesAsync(symbol, solution, cancellationToken = ct) - |> Async.AwaitTask - - return refs |> Seq.collect locationsFromReferencedSym - } - - member this.GetDocumentVersion(uri: DocumentUri) : int option = - Uri.unescape uri |> this.OpenDocs.TryFind |> Option.map _.Version + member _.Emit = emit diff --git a/src/CSharpLanguageServer/State/ServerState.fs b/src/CSharpLanguageServer/State/ServerState.fs index d41e6734..33398fd1 100644 --- a/src/CSharpLanguageServer/State/ServerState.fs +++ b/src/CSharpLanguageServer/State/ServerState.fs @@ -1,7 +1,6 @@ module CSharpLanguageServer.State.ServerState open System -open System.IO open System.Threading open System.Threading.Tasks @@ -13,20 +12,14 @@ open Microsoft.Extensions.Logging open CSharpLanguageServer.Logging open CSharpLanguageServer.Roslyn.Conversions open CSharpLanguageServer.Roslyn.Solution -open CSharpLanguageServer.Roslyn.Symbol +open CSharpLanguageServer.Lsp.Workspace open CSharpLanguageServer.Types open CSharpLanguageServer.Util -type DecompiledMetadataDocument = - { Metadata: CSharpMetadataInformation - Document: Document } - type ServerRequestMode = | ReadOnly | ReadWrite -type ServerOpenDocInfo = { Version: int; Touched: DateTime } - type RequestMetrics = { Count: int TotalDuration: TimeSpan @@ -53,16 +46,13 @@ type ServerRequest = and ServerState = { Settings: ServerSettings - RootPath: string LspClient: ILspClient option ClientCapabilities: ClientCapabilities - Solution: Solution option - OpenDocs: Map - DecompiledMetadata: Map + Workspace: LspWorkspace LastRequestId: int PendingRequests: ServerRequest list RunningRequests: Map - SolutionReloadPending: DateTime option + WorkspaceReloadPending: DateTime option PushDiagnosticsDocumentBacklog: string list PushDiagnosticsCurrentDocTask: (string * Task) option RequestStats: Map @@ -70,22 +60,18 @@ and ServerState = static member Empty = { Settings = ServerSettings.Default - RootPath = Directory.GetCurrentDirectory() LspClient = None ClientCapabilities = emptyClientCapabilities - Solution = None - OpenDocs = Map.empty - DecompiledMetadata = Map.empty + Workspace = LspWorkspace.Empty LastRequestId = 0 PendingRequests = [] RunningRequests = Map.empty - SolutionReloadPending = None + WorkspaceReloadPending = None PushDiagnosticsDocumentBacklog = [] PushDiagnosticsCurrentDocTask = None RequestStats = Map.empty LastStatsDumpTime = DateTime.MinValue } - let pullFirstRequestMaybe requestQueue = match requestQueue with | [] -> (None, []) @@ -115,61 +101,25 @@ let pullNextRequestMaybe requestQueue = (Some nextRequest, queueRemainder) -type ServerDocumentType = - | UserDocument // user Document from solution, on disk - | DecompiledDocument // Document decompiled from metadata, readonly - | AnyDocument - - type ServerStateEvent = - | SettingsChange of ServerSettings - | RootPathChange of string - | ClientChange of ILspClient option | ClientCapabilityChange of ClientCapabilities - | SolutionChange of Solution - | DecompiledMetadataAdd of string * DecompiledMetadataDocument - | OpenDocAdd of string * int * DateTime - | OpenDocRemove of string - | OpenDocTouch of string * DateTime - | GetState of AsyncReplyChannel - | GetDocumentOfTypeForUri of ServerDocumentType * string * AsyncReplyChannel - | StartRequest of string * ServerRequestMode * int * AsyncReplyChannel + | ClientChange of ILspClient option + | DocumentClosed of string + | DocumentOpened of string * int * DateTime + | DocumentTouched of string * DateTime + | DumpAndResetRequestStats | FinishRequest of int + | GetState of AsyncReplyChannel + | PeriodicTimerTick | ProcessRequestQueue - | SolutionReloadRequest of TimeSpan | PushDiagnosticsDocumentBacklogUpdate - | PushDiagnosticsProcessPendingDocuments | PushDiagnosticsDocumentDiagnosticsResolution of Result<(string * int option * Diagnostic array), Exception> - | PeriodicTimerTick - | DumpAndResetRequestStats - - -let getDocumentForUriOfType state docType (u: string) = - let uri = Uri(u.Replace("%3A", ":", true, null)) - - match state.Solution with - | Some solution -> - let matchingUserDocuments = - solution.Projects - |> Seq.collect (fun p -> p.Documents) - |> Seq.filter (fun d -> Uri(d.FilePath, UriKind.Absolute) = uri) - |> List.ofSeq - - let matchingUserDocumentMaybe = - match matchingUserDocuments with - | [ d ] -> Some(d, UserDocument) - | _ -> None - - let matchingDecompiledDocumentMaybe = - Map.tryFind u state.DecompiledMetadata - |> Option.map (fun x -> (x.Document, DecompiledDocument)) - - match docType with - | UserDocument -> matchingUserDocumentMaybe - | DecompiledDocument -> matchingDecompiledDocumentMaybe - | AnyDocument -> matchingUserDocumentMaybe |> Option.orElse matchingDecompiledDocumentMaybe - | None -> None - + | PushDiagnosticsProcessPendingDocuments + | SettingsChange of ServerSettings + | StartRequest of string * ServerRequestMode * int * AsyncReplyChannel + | WorkspaceConfigurationChanged of WorkspaceFolder list + | WorkspaceFolderChange of LspWorkspaceFolder + | WorkspaceReloadRequested of TimeSpan let processFinishRequest postSelf state request = request.Semaphore.Dispose() @@ -227,7 +177,6 @@ let processFinishRequest postSelf state request = postSelf ProcessRequestQueue newState - let processDumpAndResetRequestStats (logger: ILogger) state = let formatStats stats = let calculateRequestStatsMetrics (name, metrics) = @@ -277,7 +226,6 @@ let processDumpAndResetRequestStats (logger: ILogger) state = RequestStats = Map.empty LastStatsDumpTime = DateTime.Now } - let processServerEvent (logger: ILogger) state postSelf msg : Async = async { match msg with | SettingsChange newSettings -> @@ -287,7 +235,7 @@ let processServerEvent (logger: ILogger) state postSelf msg : Async not (state.Settings.SolutionPath = newState.Settings.SolutionPath) if solutionChanged then - postSelf (SolutionReloadRequest(TimeSpan.FromMilliseconds(250))) + postSelf (WorkspaceReloadRequested(TimeSpan.FromMilliseconds(250))) return newState @@ -295,12 +243,6 @@ let processServerEvent (logger: ILogger) state postSelf msg : Async replyChannel.Reply(state) return state - | GetDocumentOfTypeForUri(docType, uri, replyChannel) -> - let documentAndTypeMaybe = getDocumentForUriOfType state docType uri - replyChannel.Reply(documentAndTypeMaybe |> Option.map fst) - - return state - | StartRequest(name, requestMode, requestPriority, replyChannel) -> postSelf ProcessRequestQueue @@ -371,61 +313,64 @@ let processServerEvent (logger: ILogger) state postSelf msg : Async newState - | RootPathChange rootPath -> return { state with RootPath = rootPath } + | WorkspaceConfigurationChanged workspaceFolders -> + let newWorkspace = workspaceFrom workspaceFolders + return { state with Workspace = newWorkspace } | ClientChange lspClient -> return { state with LspClient = lspClient } | ClientCapabilityChange cc -> return { state with ClientCapabilities = cc } - | SolutionChange s -> - postSelf PushDiagnosticsDocumentBacklogUpdate - return { state with Solution = Some s } - - | DecompiledMetadataAdd(uri, md) -> - let newDecompiledMd = Map.add uri md state.DecompiledMetadata + | WorkspaceFolderChange updatedWf -> + let updatedWorkspaceFolderList = + state.Workspace.Folders + |> List.map (fun wf -> if wf.Uri = updatedWf.Uri then updatedWf else wf) return { state with - DecompiledMetadata = newDecompiledMd } + Workspace.Folders = updatedWorkspaceFolderList } - | OpenDocAdd(doc, ver, timestamp) -> + | DocumentOpened(uri, ver, timestamp) -> postSelf PushDiagnosticsDocumentBacklogUpdate let openDocInfo = { Version = ver; Touched = timestamp } - let newOpenDocs = state.OpenDocs |> Map.add doc openDocInfo - return { state with OpenDocs = newOpenDocs } + let newOpenDocs = state.Workspace.OpenDocs |> Map.add uri openDocInfo - | OpenDocRemove uri -> + return + { state with + Workspace.OpenDocs = newOpenDocs } + + | DocumentClosed uri -> postSelf PushDiagnosticsDocumentBacklogUpdate - let newOpenDocVersions = state.OpenDocs |> Map.remove uri + let newOpenDocVersions = state.Workspace.OpenDocs |> Map.remove uri return { state with - OpenDocs = newOpenDocVersions } + Workspace.OpenDocs = newOpenDocVersions } - | OpenDocTouch(uri, timestamp) -> + | DocumentTouched(uri, timestamp) -> postSelf PushDiagnosticsDocumentBacklogUpdate - let openDocInfo = state.OpenDocs |> Map.tryFind uri + let openDocInfo = state.Workspace.OpenDocs |> Map.tryFind uri match openDocInfo with | None -> return state | Some openDocInfo -> let updatedOpenDocInfo = { openDocInfo with Touched = timestamp } - let newOpenDocVersions = state.OpenDocs |> Map.add uri updatedOpenDocInfo + let newOpenDocVersions = state.Workspace.OpenDocs |> Map.add uri updatedOpenDocInfo return { state with - OpenDocs = newOpenDocVersions } + Workspace.OpenDocs = newOpenDocVersions } - | SolutionReloadRequest reloadNoLaterThanIn -> + | WorkspaceReloadRequested reloadNoLaterThanIn -> // we need to wait a bit before starting this so we // can buffer many incoming requests at once let newSolutionReloadDeadline = let suggestedDeadline = DateTime.Now + reloadNoLaterThanIn - match state.SolutionReloadPending with + match state.WorkspaceReloadPending with | Some currentDeadline -> if (suggestedDeadline < currentDeadline) then suggestedDeadline @@ -435,14 +380,14 @@ let processServerEvent (logger: ILogger) state postSelf msg : Async return { state with - SolutionReloadPending = newSolutionReloadDeadline |> Some } + WorkspaceReloadPending = newSolutionReloadDeadline |> Some } | PushDiagnosticsDocumentBacklogUpdate -> // here we build new backlog for background diagnostics processing // which will consider documents by their last modification date // for processing first let newBacklog = - state.OpenDocs + state.Workspace.OpenDocs |> Seq.sortByDescending (fun kv -> kv.Value.Touched) |> Seq.map (fun kv -> kv.Key) |> List.ofSeq @@ -477,9 +422,9 @@ let processServerEvent (logger: ILogger) state postSelf msg : Async { state with PushDiagnosticsDocumentBacklog = newBacklog } - let docAndTypeMaybe = docUri |> getDocumentForUriOfType state AnyDocument + let wf, docForUri = docUri |> workspaceDocument state.Workspace AnyDocument - match docAndTypeMaybe with + match docForUri with | None -> // could not find document for this enqueued uri logger.LogDebug( @@ -489,7 +434,7 @@ let processServerEvent (logger: ILogger) state postSelf msg : Async return newState - | Some(doc, _docType) -> + | Some doc -> let resolveDocumentDiagnostics () : Task = task { let! semanticModelMaybe = doc.GetSemanticModelAsync() @@ -558,17 +503,22 @@ let processServerEvent (logger: ILogger) state postSelf msg : Async postSelf DumpAndResetRequestStats let solutionReloadDeadline = - state.SolutionReloadPending |> Option.defaultValue (DateTime.Now.AddDays(1)) + state.WorkspaceReloadPending |> Option.defaultValue (DateTime.Now.AddDays(1)) match solutionReloadDeadline < DateTime.Now with | true -> + let workspaceFolder = state.Workspace.SingletonFolder + let! newSolution = - solutionLoadSolutionWithPathOrOnCwd state.LspClient.Value state.Settings.SolutionPath state.RootPath + solutionLoadSolutionWithPathOrOnCwd + state.LspClient.Value + state.Settings.SolutionPath + (workspaceFolder.Uri |> Uri.toPath) return { state with - Solution = newSolution - SolutionReloadPending = None } + Workspace = state.Workspace.WithSolution(newSolution) + WorkspaceReloadPending = None } | false -> return state diff --git a/src/CSharpLanguageServer/Util.fs b/src/CSharpLanguageServer/Util.fs index 5310acfa..c701a5c7 100644 --- a/src/CSharpLanguageServer/Util.fs +++ b/src/CSharpLanguageServer/Util.fs @@ -62,13 +62,6 @@ let rec unpackException (exn: Exception) = | None -> exn | _ -> exn -// flip f takes its (first) two arguments in the reverse order of f, just like -// the function with the same name in Haskell. -let flip f x y = f y x - -let curry f x y = f (x, y) -let uncurry f (x, y) = f x y - let formatInColumns (data: list>) : string = if List.isEmpty data then diff --git a/tests/CSharpLanguageServer.Tests/CSharpLanguageServer.Tests.fsproj b/tests/CSharpLanguageServer.Tests/CSharpLanguageServer.Tests.fsproj index 97bf2ef4..62c149c2 100644 --- a/tests/CSharpLanguageServer.Tests/CSharpLanguageServer.Tests.fsproj +++ b/tests/CSharpLanguageServer.Tests/CSharpLanguageServer.Tests.fsproj @@ -23,6 +23,9 @@ + + + diff --git a/tests/CSharpLanguageServer.Tests/ImplementationTests.fs b/tests/CSharpLanguageServer.Tests/ImplementationTests.fs new file mode 100644 index 00000000..6b29ae19 --- /dev/null +++ b/tests/CSharpLanguageServer.Tests/ImplementationTests.fs @@ -0,0 +1,25 @@ +module CSharpLanguageServer.Tests.ImplementationTests + +open System + +open NUnit.Framework +open Ionide.LanguageServerProtocol.Types + +open CSharpLanguageServer.Tests.Tooling + +[] +let ``test textDocument/implementation works`` () = + use client = activateFixture "genericProject" + use classFile = client.Open "Project/Class.cs" + + let implementationParams0: ImplementationParams = + { TextDocument = { Uri = classFile.Uri } + Position = { Line = 10u; Character = 8u } + WorkDoneToken = None + PartialResultToken = None } + + let implementation0: U2 option = + client.Request("textDocument/implementation", implementationParams0) + + // TODO: fix this test + () diff --git a/tests/CSharpLanguageServer.Tests/ReferenceTests.fs b/tests/CSharpLanguageServer.Tests/ReferenceTests.fs index 7bab5060..07e7bba6 100644 --- a/tests/CSharpLanguageServer.Tests/ReferenceTests.fs +++ b/tests/CSharpLanguageServer.Tests/ReferenceTests.fs @@ -165,17 +165,21 @@ let testReferenceWorksToAspNetRazorPageReferencedValue () = Assert.AreEqual(2, locations0.Value.Length) let expectedLocations0: Location array = - [| { Uri = testControllerCsFile.Uri + [| { Uri = viewsTestIndexCshtmlFile.Uri Range = - { Start = { Line = 11u; Character = 12u } - End = { Line = 11u; Character = 18u } } } + { Start = { Line = 1u; Character = 7u } + End = { Line = 1u; Character = 13u } } } - { Uri = viewsTestIndexCshtmlFile.Uri + { Uri = testControllerCsFile.Uri Range = - { Start = { Line = 1u; Character = 7u } - End = { Line = 1u; Character = 13u } } } |] + { Start = { Line = 11u; Character = 12u } + End = { Line = 11u; Character = 18u } } } |] + + let sortedLocations0 = + locations0.Value + |> Array.sortBy (fun f -> (f.Range.Start.Line, f.Range.Start.Character)) - Assert.AreEqual(expectedLocations0, locations0.Value) + Assert.AreEqual(expectedLocations0, sortedLocations0) // // do same but with IncludeDeclaration=true diff --git a/tests/CSharpLanguageServer.Tests/SignatureHelpTests.fs b/tests/CSharpLanguageServer.Tests/SignatureHelpTests.fs new file mode 100644 index 00000000..84cf0583 --- /dev/null +++ b/tests/CSharpLanguageServer.Tests/SignatureHelpTests.fs @@ -0,0 +1,44 @@ +module CSharpLanguageServer.Tests.SignatureHelpTests + +open NUnit.Framework +open Ionide.LanguageServerProtocol.Types + +open CSharpLanguageServer.Tests.Tooling + +[] +let ``test textDocument/signatureHelp works`` () = + use client = activateFixture "genericProject" + use classFile = client.Open "Project/Class.cs" + + let signatureHelpParams0: SignatureHelpParams = + { TextDocument = { Uri = classFile.Uri } + Position = { Line = 9u; Character = 16u } + WorkDoneToken = None + Context = None } + + let signatureHelp0: SignatureHelp option = + client.Request("textDocument/signatureHelp", signatureHelpParams0) + + match signatureHelp0 with + | None -> failwith "Some SignatureHelp was expected" + | Some sh -> + Assert.AreEqual(1, sh.Signatures.Length) + + let expectedSignature0 = + { Label = "void Class.MethodA(string arg)" + Documentation = + Some( + U2.C2 + { Kind = MarkupKind.Markdown + Value = "" } + ) + Parameters = + Some + [| { Label = U2.C1 "string arg" + Documentation = None } |] + ActiveParameter = None } + + Assert.AreEqual(expectedSignature0, sh.Signatures[0]) + + Assert.AreEqual(Some 0u, sh.ActiveSignature) + Assert.AreEqual(None, sh.ActiveParameter) diff --git a/tests/CSharpLanguageServer.Tests/TypeDefinitionTests.fs b/tests/CSharpLanguageServer.Tests/TypeDefinitionTests.fs new file mode 100644 index 00000000..763fce82 --- /dev/null +++ b/tests/CSharpLanguageServer.Tests/TypeDefinitionTests.fs @@ -0,0 +1,38 @@ +module CSharpLanguageServer.Tests.TypeDefinitionTests + +open System + +open NUnit.Framework +open Ionide.LanguageServerProtocol.Types + +open CSharpLanguageServer.Tests.Tooling + +[] +let ``test textDocument/typeDefinition works`` () = + use client = activateFixture "genericProject" + use classFile = client.Open "Project/Class.cs" + + let typeDefinitionParams0: TypeDefinitionParams = + { TextDocument = { Uri = classFile.Uri } + Position = { Line = 9u; Character = 16u } + WorkDoneToken = None + PartialResultToken = None } + + let typeDefinition0: U2 option = + client.Request("textDocument/typeDefinition", typeDefinitionParams0) + + match typeDefinition0 with + | Some(U2.C1(U2.C2 ls)) -> + Assert.AreEqual(1, ls.Length) + + let expectedTypeDefLocationsForStringArg = + [| { Uri = "csharp:/metadata/projects/Project/assemblies/System.Runtime/symbols/System.String.cs" + Range = + { Start = { Line = 12u; Character = 20u } + End = { Line = 12u; Character = 26u } } } |] + + Assert.AreEqual(expectedTypeDefLocationsForStringArg, ls) + + | _ -> failwith "Some U2.C1 (U2.C2) was expected" + + ()