diff --git a/src/CSharpLanguageServer/CSharpLanguageServer.fsproj b/src/CSharpLanguageServer/CSharpLanguageServer.fsproj index 37abcdb6..f41cb068 100644 --- a/src/CSharpLanguageServer/CSharpLanguageServer.fsproj +++ b/src/CSharpLanguageServer/CSharpLanguageServer.fsproj @@ -29,6 +29,7 @@ + diff --git a/src/CSharpLanguageServer/Handlers/TextDocumentSync.fs b/src/CSharpLanguageServer/Handlers/TextDocumentSync.fs index 2b6e1cfd..07efb59f 100644 --- a/src/CSharpLanguageServer/Handlers/TextDocumentSync.fs +++ b/src/CSharpLanguageServer/Handlers/TextDocumentSync.fs @@ -11,6 +11,7 @@ open CSharpLanguageServer open CSharpLanguageServer.Conversions open CSharpLanguageServer.State open CSharpLanguageServer.State.ServerState +open CSharpLanguageServer.State.ServerDiagnosticPush open CSharpLanguageServer.Types open CSharpLanguageServer.RoslynHelpers open CSharpLanguageServer.Logging @@ -107,7 +108,7 @@ module TextDocumentSync = let willSaveWaitUntilRegistration (_clientCapabilities: ClientCapabilities) : Registration option = None - let didOpen (diagnosticsPost: DiagnosticsEvent -> unit) + let didOpen (diagnosticsPost: PushDiagnosticEvent -> unit) (context: ServerRequestContext) (openParams: DidOpenTextDocumentParams) : Async> = @@ -159,7 +160,7 @@ module TextDocumentSync = | None -> LspResult.Ok() |> async.Return - let didChange (diagnosticsPost: DiagnosticsEvent -> unit) + let didChange (diagnosticsPost: PushDiagnosticEvent -> unit) (context: ServerRequestContext) (changeParams: DidChangeTextDocumentParams) : Async> = @@ -190,7 +191,7 @@ module TextDocumentSync = return Result.Ok() } - let didClose (diagnosticsPost: DiagnosticsEvent -> unit) + let didClose (diagnosticsPost: PushDiagnosticEvent -> unit) (context: ServerRequestContext) (closeParams: DidCloseTextDocumentParams) : Async> = @@ -206,7 +207,7 @@ module TextDocumentSync = return LspResult.notImplemented } - let didSave (diagnosticsPost: DiagnosticsEvent -> unit) + let didSave (diagnosticsPost: PushDiagnosticEvent -> unit) (context: ServerRequestContext) (saveParams: DidSaveTextDocumentParams) : Async> = diff --git a/src/CSharpLanguageServer/Handlers/Workspace.fs b/src/CSharpLanguageServer/Handlers/Workspace.fs index 2d87d1ff..f66c9b5c 100644 --- a/src/CSharpLanguageServer/Handlers/Workspace.fs +++ b/src/CSharpLanguageServer/Handlers/Workspace.fs @@ -10,6 +10,7 @@ open Microsoft.CodeAnalysis.Text open CSharpLanguageServer open CSharpLanguageServer.State open CSharpLanguageServer.State.ServerState +open CSharpLanguageServer.State.ServerDiagnosticPush open CSharpLanguageServer.RoslynHelpers open CSharpLanguageServer.Logging @@ -77,7 +78,7 @@ module Workspace = diagnosticsPost(DocumentRemoval uri) | None -> () - let didChangeWatchedFiles (diagnosticsPost: DiagnosticsEvent -> unit) + let didChangeWatchedFiles (diagnosticsPost: PushDiagnosticEvent -> unit) (context: ServerRequestContext) (p: DidChangeWatchedFilesParams) : Async> = async { diff --git a/src/CSharpLanguageServer/Lsp/Server.fs b/src/CSharpLanguageServer/Lsp/Server.fs index cd70668a..8d9a356d 100644 --- a/src/CSharpLanguageServer/Lsp/Server.fs +++ b/src/CSharpLanguageServer/Lsp/Server.fs @@ -14,6 +14,7 @@ open CSharpLanguageServer.Handlers open CSharpLanguageServer.Logging open CSharpLanguageServer.State open CSharpLanguageServer.State.ServerState +open CSharpLanguageServer.State.ServerDiagnosticPush open CSharpLanguageServer.Util module LspUtils = @@ -41,7 +42,7 @@ type CSharpLspServer( stateActor.PostAndAsyncReply(fun rc -> GetDocumentOfTypeForUri (docType, uri, rc)) let diagnostics = MailboxProcessor.Start( - diagnosticsEventLoop + pushDiagnosticEventLoop lspClient getDocumentForUriFromCurrentState) diff --git a/src/CSharpLanguageServer/State/ServerDiagnosticPush.fs b/src/CSharpLanguageServer/State/ServerDiagnosticPush.fs new file mode 100644 index 00000000..bc3d3957 --- /dev/null +++ b/src/CSharpLanguageServer/State/ServerDiagnosticPush.fs @@ -0,0 +1,154 @@ +module CSharpLanguageServer.State.ServerDiagnosticPush + +open System + +open Microsoft.CodeAnalysis +open Ionide.LanguageServerProtocol.Types + +open CSharpLanguageServer.Conversions +open CSharpLanguageServer.Logging +open CSharpLanguageServer.Lsp +open CSharpLanguageServer.Types + +type PushDiagnosticEvent = + | DocumentOpenOrChange of string * DateTime + | DocumentClose of string + | ProcessPendingDiagnostics + | DocumentBacklogUpdate + | DocumentRemoval of string + +type PushDiagnosticState = { + DocumentBacklog: string list + DocumentChanges: Map + LastPendingDiagnosticsProcessing: DateTime +} + +let emptyPushDiagnosticState = { + DocumentBacklog = [] + DocumentChanges = Map.empty + LastPendingDiagnosticsProcessing = DateTime.MinValue +} + +let processPushDiagnosticEvent + (publishDiagnostics: string -> Diagnostic[] -> Async) + (getDocumentForUri: string -> Async) + (state: PushDiagnosticState) + (currentEventQueueLength: int) + msg = + match msg with + | DocumentRemoval uri -> async { + let updatedDocumentChanges = state.DocumentChanges |> Map.remove uri + let newState = { state with DocumentChanges = updatedDocumentChanges } + return newState, [ DocumentBacklogUpdate ] + } + + | DocumentOpenOrChange (uri, timestamp) -> async { + let newDocChanges = state.DocumentChanges |> Map.add uri timestamp + let newState = { state with DocumentChanges = newDocChanges } + return newState, [ DocumentBacklogUpdate ] + } + + | DocumentBacklogUpdate -> async { + // here we build new backlog for background diagnostics processing + // which will consider documents by their last modification date + // for processing first + let newBacklog = + state.DocumentChanges + |> Seq.sortByDescending (fun kv -> kv.Value) + |> Seq.map (fun kv -> kv.Key) + |> List.ofSeq + + let newState = { state with DocumentBacklog = newBacklog } + return newState, [] + } + + | DocumentClose uri -> async { + let prunedBacklog = state.DocumentBacklog + |> Seq.filter (fun x -> x <> uri) + |> List.ofSeq + + let prunedDocumentChanges = state.DocumentChanges |> Map.remove uri + + let newState = { state with DocumentBacklog = prunedBacklog + DocumentChanges = prunedDocumentChanges } + return newState, [] + } + + | ProcessPendingDiagnostics -> + let doProcessPendingDiagnostics = async { + let docUriMaybe, newBacklog = + match state.DocumentBacklog with + | [] -> (None, []) + | uri :: remainder -> (Some uri, remainder) + + match docUriMaybe with + | Some docUri -> + let! docAndTypeMaybe = getDocumentForUri docUri + + match docAndTypeMaybe with + | Some doc -> + let! ct = Async.CancellationToken + let! semanticModelMaybe = doc.GetSemanticModelAsync(ct) |> Async.AwaitTask + match semanticModelMaybe |> Option.ofObj with + | Some semanticModel -> + let diagnostics = + semanticModel.GetDiagnostics() + |> Seq.map Diagnostic.fromRoslynDiagnostic + |> Array.ofSeq + + do! publishDiagnostics docUri diagnostics + | None -> () + | None -> () + | None -> () + + let newState = { state with DocumentBacklog = newBacklog + LastPendingDiagnosticsProcessing = DateTime.Now } + + let eventsToPost = match newBacklog with + | [] -> [] + | _ -> [ ProcessPendingDiagnostics ] + + return newState, eventsToPost + } + + let timeSinceLastProcessing = + (DateTime.Now - state.LastPendingDiagnosticsProcessing) + + if timeSinceLastProcessing > TimeSpan.FromMilliseconds(250) || currentEventQueueLength = 0 then + doProcessPendingDiagnostics + else + async { return state, [] } + +let pushDiagnosticEventLoop + (lspClient: CSharpLspClient) + getDocumentForUriFromCurrentState + (inbox: MailboxProcessor) = + let logger = LogProvider.getLoggerByName "ServerDiagnosticPush.pushDiagnosticEventLoop" + + let rec loop state = async { + try + let! msg = inbox.Receive() + let! (newState, eventsToPost) = + processPushDiagnosticEvent + // TODO: can we provide value for PublishDiagnosticsParams.Version? + (fun docUri diagnostics -> lspClient.TextDocumentPublishDiagnostics { Uri = docUri; + Version = None; + Diagnostics = diagnostics; + }) + (getDocumentForUriFromCurrentState AnyDocument) + state + inbox.CurrentQueueLength + msg + + for ev in eventsToPost do inbox.Post(ev) + + return! loop newState + with + | ex -> + logger.warn ( + Log.setMessage "unhandled exception in `diagnostics`: {message}" + >> Log.addContext "message" (string ex)) + raise ex + } + + loop emptyPushDiagnosticState diff --git a/src/CSharpLanguageServer/State/ServerState.fs b/src/CSharpLanguageServer/State/ServerState.fs index fa5653dd..b889d0bc 100644 --- a/src/CSharpLanguageServer/State/ServerState.fs +++ b/src/CSharpLanguageServer/State/ServerState.fs @@ -10,9 +10,8 @@ open Ionide.LanguageServerProtocol open CSharpLanguageServer.RoslynHelpers open CSharpLanguageServer.Types -open CSharpLanguageServer.Lsp open CSharpLanguageServer.Logging -open CSharpLanguageServer.Conversions +open CSharpLanguageServer.State.ServerDiagnosticPush type DecompiledMetadataDocument = { Metadata: CSharpMetadataInformation @@ -42,6 +41,7 @@ and ServerState = { RunningRequests: Map RequestQueue: ServerRequest list SolutionReloadPending: DateTime option + PushDiagnostics: MailboxProcessor option } let pullFirstRequestMaybe requestQueue = @@ -74,22 +74,19 @@ let pullNextRequestMaybe requestQueue = (Some nextRequest, queueRemainder) -let emptyServerState = { Settings = ServerSettings.Default - RootPath = Directory.GetCurrentDirectory() - LspClient = None - ClientCapabilities = emptyClientCapabilities - Solution = None - OpenDocVersions = Map.empty - DecompiledMetadata = Map.empty - LastRequestId = 0 - RunningRequests = Map.empty - RequestQueue = [] - SolutionReloadPending = None } - -type ServerDocumentType = - | UserDocument // user Document from solution, on disk - | DecompiledDocument // Document decompiled from metadata, readonly - | AnyDocument +let emptyServerState = + { Settings = ServerSettings.Default + RootPath = Directory.GetCurrentDirectory() + LspClient = None + ClientCapabilities = emptyClientCapabilities + Solution = None + OpenDocVersions = Map.empty + DecompiledMetadata = Map.empty + LastRequestId = 0 + RunningRequests = Map.empty + RequestQueue = [] + SolutionReloadPending = None + PushDiagnostics = None } type ServerStateEvent = | SettingsChange of ServerSettings @@ -294,150 +291,6 @@ let serverEventLoop initialState (inbox: MailboxProcessor) = loop initialState - -type DiagnosticsEvent = - | DocumentOpenOrChange of string * DateTime - | DocumentClose of string - | ProcessPendingDiagnostics - | DocumentBacklogUpdate - | DocumentRemoval of string - -type DiagnosticsState = { - DocumentBacklog: string list - DocumentChanges: Map - LastPendingDiagnosticsProcessing: DateTime -} - -let emptyDiagnosticsState = { - DocumentBacklog = [] - DocumentChanges = Map.empty - LastPendingDiagnosticsProcessing = DateTime.MinValue -} - -let processDiagnosticsEvent - (publishDiagnostics: string -> Diagnostic[] -> Async) - (getDocumentForUri: string -> Async) - (state: DiagnosticsState) - (currentEventQueueLength: int) - msg = - match msg with - | DocumentRemoval uri -> async { - let updatedDocumentChanges = state.DocumentChanges |> Map.remove uri - let newState = { state with DocumentChanges = updatedDocumentChanges } - return newState, [ DocumentBacklogUpdate ] - } - - | DocumentOpenOrChange (uri, timestamp) -> async { - let newDocChanges = state.DocumentChanges |> Map.add uri timestamp - let newState = { state with DocumentChanges = newDocChanges } - return newState, [ DocumentBacklogUpdate ] - } - - | DocumentBacklogUpdate -> async { - // here we build new backlog for background diagnostics processing - // which will consider documents by their last modification date - // for processing first - let newBacklog = - state.DocumentChanges - |> Seq.sortByDescending (fun kv -> kv.Value) - |> Seq.map (fun kv -> kv.Key) - |> List.ofSeq - - let newState = { state with DocumentBacklog = newBacklog } - return newState, [] - } - - | DocumentClose uri -> async { - let prunedBacklog = state.DocumentBacklog - |> Seq.filter (fun x -> x <> uri) - |> List.ofSeq - - let prunedDocumentChanges = state.DocumentChanges |> Map.remove uri - - let newState = { state with DocumentBacklog = prunedBacklog - DocumentChanges = prunedDocumentChanges } - return newState, [] - } - - | ProcessPendingDiagnostics -> - let doProcessPendingDiagnostics = async { - let docUriMaybe, newBacklog = - match state.DocumentBacklog with - | [] -> (None, []) - | uri :: remainder -> (Some uri, remainder) - - match docUriMaybe with - | Some docUri -> - let! docAndTypeMaybe = getDocumentForUri docUri - - match docAndTypeMaybe with - | Some doc -> - let! ct = Async.CancellationToken - let! semanticModelMaybe = doc.GetSemanticModelAsync(ct) |> Async.AwaitTask - match semanticModelMaybe |> Option.ofObj with - | Some semanticModel -> - let diagnostics = - semanticModel.GetDiagnostics() - |> Seq.map Diagnostic.fromRoslynDiagnostic - |> Array.ofSeq - - do! publishDiagnostics docUri diagnostics - | None -> () - | None -> () - | None -> () - - let newState = { state with DocumentBacklog = newBacklog - LastPendingDiagnosticsProcessing = DateTime.Now } - - let eventsToPost = match newBacklog with - | [] -> [] - | _ -> [ ProcessPendingDiagnostics ] - - return newState, eventsToPost - } - - let timeSinceLastProcessing = - (DateTime.Now - state.LastPendingDiagnosticsProcessing) - - if timeSinceLastProcessing > TimeSpan.FromMilliseconds(250) || currentEventQueueLength = 0 then - doProcessPendingDiagnostics - else - async { return state, [] } - -let diagnosticsEventLoop - (lspClient: CSharpLspClient) - getDocumentForUriFromCurrentState - (inbox: MailboxProcessor) = - let logger = LogProvider.getLoggerByName "DiagnosticsEventLoop" - - let rec loop state = async { - try - let! msg = inbox.Receive() - let! (newState, eventsToPost) = - processDiagnosticsEvent - // TODO: can we provide value for PublishDiagnosticsParams.Version? - (fun docUri diagnostics -> lspClient.TextDocumentPublishDiagnostics { Uri = docUri; - Version = None; - Diagnostics = diagnostics; - }) - (getDocumentForUriFromCurrentState AnyDocument) - state - inbox.CurrentQueueLength - msg - - for ev in eventsToPost do inbox.Post(ev) - - return! loop newState - with - | ex -> - logger.warn ( - Log.setMessage "unhandled exception in `diagnostics`: {message}" - >> Log.addContext "message" (string ex)) - raise ex - } - - loop emptyDiagnosticsState - type ServerSettingsDto = { csharp: ServerSettingsCSharpDto option } diff --git a/src/CSharpLanguageServer/Types.fs b/src/CSharpLanguageServer/Types.fs index f2ca18f2..bbf9d184 100644 --- a/src/CSharpLanguageServer/Types.fs +++ b/src/CSharpLanguageServer/Types.fs @@ -54,3 +54,8 @@ let emptyClientCapabilities: ClientCapabilities = General = None Experimental = None } + +type ServerDocumentType = + | UserDocument // user Document from solution, on disk + | DecompiledDocument // Document decompiled from metadata, readonly + | AnyDocument