diff --git a/CHANGELOG.md b/CHANGELOG.md index 54f5a02..ac3739f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,11 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.4.9-alpha.2] - 12.09.2022 + +### Changed +* Major revamp of the surface area of the API. + ## [0.4.8] - 12.09.2022 ### Added diff --git a/src/Client.fs b/src/Client.fs index 976f262..50e5dfd 100644 --- a/src/Client.fs +++ b/src/Client.fs @@ -1,146 +1,43 @@ namespace Ionide.LanguageServerProtocol open Ionide.LanguageServerProtocol.Types - -module private ClientUtil = - /// Return the JSON-RPC "not implemented" error - let notImplemented<'t> = async.Return LspResult.notImplemented<'t> - - /// Do nothing and ignore the notification - let ignoreNotification = async.Return(()) - -open ClientUtil +open System +open System.Threading +open System.Threading.Tasks [] type ILspClient = - /// The show message notification is sent from a server to a client to ask the client to display - /// a particular message in the user interface. - abstract member WindowShowMessage: ShowMessageParams -> Async - - /// The show message request is sent from a server to a client to ask the client to display - /// a particular message in the user interface. In addition to the show message notification the - /// request allows to pass actions and to wait for an answer from the client. - abstract member WindowShowMessageRequest: ShowMessageRequestParams -> AsyncLspResult - - - /// The log message notification is sent from the server to the client to ask the client to log - ///a particular message. - abstract member WindowLogMessage: LogMessageParams -> Async - - /// The telemetry notification is sent from the server to the client to ask the client to log - /// a telemetry event. - abstract member TelemetryEvent: Newtonsoft.Json.Linq.JToken -> Async - - /// The `client/registerCapability` request is sent from the server to the client to register for a new - /// capability on the client side. Not all clients need to support dynamic capability registration. - /// A client opts in via the dynamicRegistration property on the specific client capabilities. A client - /// can even provide dynamic registration for capability A but not for capability B. - abstract member ClientRegisterCapability: RegistrationParams -> AsyncLspResult - - /// The `client/unregisterCapability` request is sent from the server to the client to unregister a previously - /// registered capability. - abstract member ClientUnregisterCapability: UnregistrationParams -> AsyncLspResult - - - /// Many tools support more than one root folder per workspace. Examples for this are VS Code’s multi-root - /// support, Atom’s project folder support or Sublime’s project support. If a client workspace consists of - /// multiple roots then a server typically needs to know about this. The protocol up to know assumes one root - /// folder which is announce to the server by the rootUri property of the InitializeParams. - /// If the client supports workspace folders and announces them via the corresponding workspaceFolders client - /// capability the InitializeParams contain an additional property workspaceFolders with the configured - /// workspace folders when the server starts. - /// - /// The workspace/workspaceFolders request is sent from the server to the client to fetch the current open - /// list of workspace folders. Returns null in the response if only a single file is open in the tool. - /// Returns an empty array if a workspace is open but no folders are configured. - abstract member WorkspaceWorkspaceFolders: unit -> AsyncLspResult - - /// The workspace/configuration request is sent from the server to the client to fetch configuration - /// settings from the client. - /// - /// The request can fetch n configuration settings in one roundtrip. The order of the returned configuration - /// settings correspond to the order of the passed ConfigurationItems (e.g. the first item in the response - /// is the result for the first configuration item in the params). - abstract member WorkspaceConfiguration: ConfigurationParams -> AsyncLspResult - - - abstract member WorkspaceApplyEdit: ApplyWorkspaceEditParams -> AsyncLspResult - - /// The workspace/semanticTokens/refresh request is sent from the server to the client. - /// Servers can use it to ask clients to refresh the editors for which this server provides semantic tokens. - /// As a result the client should ask the server to recompute the semantic tokens for these editors. - /// This is useful if a server detects a project wide configuration change which requires a re-calculation - /// of all semantic tokens. Note that the client still has the freedom to delay the re-calculation of - /// the semantic tokens if for example an editor is currently not visible. - abstract member WorkspaceSemanticTokensRefresh: unit -> Async - - - /// The `workspace/inlayHint/refresh` request is sent from the server to the client. - /// Servers can use it to ask clients to refresh the inlay hints currently shown in editors. - /// As a result the client should ask the server to recompute the inlay hints for these editors. - /// This is useful if a server detects a configuration change which requires a re-calculation - /// of all inlay hints. Note that the client still has the freedom to delay the re-calculation of the inlay hints - /// if for example an editor is currently not visible. - abstract member WorkspaceInlayHintRefresh: unit -> Async - - - /// Diagnostics notification are sent from the server to the client to signal results of validation runs. - /// - /// Diagnostics are “owned” by the server so it is the server’s responsibility to clear them if necessary. - /// The following rule is used for VS Code servers that generate diagnostics: - /// - /// * if a language is single file only (for example HTML) then diagnostics are cleared by the server when - /// the file is closed. - /// * if a language has a project system (for example C#) diagnostics are not cleared when a file closes. - /// When a project is opened all diagnostics for all files are recomputed (or read from a cache). - /// - /// When a file changes it is the server’s responsibility to re-compute diagnostics and push them to the - /// client. If the computed set is empty it has to push the empty array to clear former diagnostics. - /// Newly pushed diagnostics always replace previously pushed diagnostics. There is no merging that happens - /// on the client side. - abstract member TextDocumentPublishDiagnostics: PublishDiagnosticsParams -> Async - -[] -type LspClient() = /// The show message notification is sent from a server to a client to ask the client to display /// a particular message in the user interface. - abstract member WindowShowMessage: ShowMessageParams -> Async - - default __.WindowShowMessage(_) = ignoreNotification + abstract member ``window/showMessage``: EventHandler /// The show message request is sent from a server to a client to ask the client to display /// a particular message in the user interface. In addition to the show message notification the /// request allows to pass actions and to wait for an answer from the client. - abstract member WindowShowMessageRequest: ShowMessageRequestParams -> AsyncLspResult - - default __.WindowShowMessageRequest(_) = notImplemented + abstract member ``window/showMessageRequest``: + ShowMessageRequestParams * CancellationToken -> Task /// The log message notification is sent from the server to the client to ask the client to log ///a particular message. - abstract member WindowLogMessage: LogMessageParams -> Async + abstract member ``window/logMessage``: EventHandler - default __.WindowLogMessage(_) = ignoreNotification /// The telemetry notification is sent from the server to the client to ask the client to log /// a telemetry event. - abstract member TelemetryEvent: Newtonsoft.Json.Linq.JToken -> Async + abstract member ``telemetry/event``: EventHandler - default __.TelemetryEvent(_) = ignoreNotification /// The `client/registerCapability` request is sent from the server to the client to register for a new /// capability on the client side. Not all clients need to support dynamic capability registration. /// A client opts in via the dynamicRegistration property on the specific client capabilities. A client /// can even provide dynamic registration for capability A but not for capability B. - abstract member ClientRegisterCapability: RegistrationParams -> AsyncLspResult + abstract member ``client/registerCapability``: RegistrationParams * CancellationToken -> Task - default __.ClientRegisterCapability(_) = notImplemented /// The `client/unregisterCapability` request is sent from the server to the client to unregister a previously /// registered capability. - abstract member ClientUnregisterCapability: UnregistrationParams -> AsyncLspResult - - default __.ClientUnregisterCapability(_) = notImplemented + abstract member ``client/unregisterCapability``: UnregistrationParams * CancellationToken -> Task /// Many tools support more than one root folder per workspace. Examples for this are VS Code’s multi-root /// support, Atom’s project folder support or Sublime’s project support. If a client workspace consists of @@ -153,9 +50,8 @@ type LspClient() = /// The workspace/workspaceFolders request is sent from the server to the client to fetch the current open /// list of workspace folders. Returns null in the response if only a single file is open in the tool. /// Returns an empty array if a workspace is open but no folders are configured. - abstract member WorkspaceWorkspaceFolders: unit -> AsyncLspResult + abstract member ``workspace/workspaceFolders``: unit * CancellationToken -> Task - default __.WorkspaceWorkspaceFolders() = notImplemented /// The workspace/configuration request is sent from the server to the client to fetch configuration /// settings from the client. @@ -163,12 +59,12 @@ type LspClient() = /// The request can fetch n configuration settings in one roundtrip. The order of the returned configuration /// settings correspond to the order of the passed ConfigurationItems (e.g. the first item in the response /// is the result for the first configuration item in the params). - abstract member WorkspaceConfiguration: ConfigurationParams -> AsyncLspResult + abstract member ``workspace/configuration``: ConfigurationParams * CancellationToken -> Task - default __.WorkspaceConfiguration(_) = notImplemented - abstract member WorkspaceApplyEdit: ApplyWorkspaceEditParams -> AsyncLspResult - default __.WorkspaceApplyEdit(_) = notImplemented + /// The workspace/applyEdit request is sent from the server to the client to modify resource on the client side. + abstract member ``workspace/applyEdit``: + ApplyWorkspaceEditParams * CancellationToken -> Task /// The workspace/semanticTokens/refresh request is sent from the server to the client. /// Servers can use it to ask clients to refresh the editors for which this server provides semantic tokens. @@ -176,9 +72,7 @@ type LspClient() = /// This is useful if a server detects a project wide configuration change which requires a re-calculation /// of all semantic tokens. Note that the client still has the freedom to delay the re-calculation of /// the semantic tokens if for example an editor is currently not visible. - abstract member WorkspaceSemanticTokensRefresh: unit -> Async - - default __.WorkspaceSemanticTokensRefresh() = ignoreNotification + abstract member ``workspace/semanticTokens/refresh``: unit * CancellationToken -> Task /// The `workspace/inlayHint/refresh` request is sent from the server to the client. /// Servers can use it to ask clients to refresh the inlay hints currently shown in editors. @@ -186,9 +80,7 @@ type LspClient() = /// This is useful if a server detects a configuration change which requires a re-calculation /// of all inlay hints. Note that the client still has the freedom to delay the re-calculation of the inlay hints /// if for example an editor is currently not visible. - abstract member WorkspaceInlayHintRefresh: unit -> Async - - default __.WorkspaceInlayHintRefresh() = ignoreNotification + abstract member ``workspace/inlayHint/refresh``: unit * CancellationToken -> Task /// Diagnostics notification are sent from the server to the client to signal results of validation runs. /// @@ -204,20 +96,11 @@ type LspClient() = /// client. If the computed set is empty it has to push the empty array to clear former diagnostics. /// Newly pushed diagnostics always replace previously pushed diagnostics. There is no merging that happens /// on the client side. - abstract member TextDocumentPublishDiagnostics: PublishDiagnosticsParams -> Async - - default __.TextDocumentPublishDiagnostics(_) = ignoreNotification - - interface ILspClient with - member this.WindowShowMessage(p: ShowMessageParams) = this.WindowShowMessage(p) - member this.WindowShowMessageRequest(p: ShowMessageRequestParams) = this.WindowShowMessageRequest(p) - member this.WindowLogMessage(p: LogMessageParams) = this.WindowLogMessage(p) - member this.TelemetryEvent(p: Newtonsoft.Json.Linq.JToken) = this.TelemetryEvent(p) - member this.ClientRegisterCapability(p: RegistrationParams) = this.ClientRegisterCapability(p) - member this.ClientUnregisterCapability(p: UnregistrationParams) = this.ClientUnregisterCapability(p) - member this.WorkspaceWorkspaceFolders() = this.WorkspaceWorkspaceFolders() - member this.WorkspaceConfiguration(p: ConfigurationParams) = this.WorkspaceConfiguration(p) - member this.WorkspaceApplyEdit(p: ApplyWorkspaceEditParams) = this.WorkspaceApplyEdit(p) - member this.WorkspaceSemanticTokensRefresh() = this.WorkspaceSemanticTokensRefresh() - member this.WorkspaceInlayHintRefresh() = this.WorkspaceInlayHintRefresh() - member this.TextDocumentPublishDiagnostics(p: PublishDiagnosticsParams) = this.TextDocumentPublishDiagnostics(p) \ No newline at end of file + abstract member ``textDocument/publishDiagnostics``: EventHandler + + /// The workspace/codeLens/refresh request is sent from the server to the client. + /// Servers can use it to ask clients to refresh the code lenses currently shown in editors. + /// As a result the client should ask the server to recompute the code lenses for these editors. + /// This is useful if a server detects a configuration change which requires a re-calculation of all code lenses. + /// Note that the client still has the freedom to delay the re-calculation of the code lenses if for example an editor is currently not visible. + abstract member ``workspace/codeLens/refresh``: EventHandler \ No newline at end of file diff --git a/src/FsLibLog.fs b/src/FsLibLog.fs deleted file mode 100644 index 3c05177..0000000 --- a/src/FsLibLog.fs +++ /dev/null @@ -1,764 +0,0 @@ -// this is from https://github.com/TheAngryByrd/FsLibLog/blob/f81cba440bf0476bb4e2262b57a067a0d6ab78a7/src/FsLibLog/FsLibLog.fs - -namespace Ionide.LanguageServerProtocol.Logging - - -[] -module Types = - open System - - type LogLevel = - | Trace = 0 - | Debug = 1 - | Info = 2 - | Warn = 3 - | Error = 4 - | Fatal = 5 - - /// An optional message thunk. - /// - /// - If `None` is provided, this typically signals to the logger to do a isEnabled check. - /// - If `Some` is provided, this signals the logger to log. - type MessageThunk = (unit -> string) option - - /// The signature of a log message function - type Logger = LogLevel -> MessageThunk -> exn option -> obj array -> bool - type MappedContext = string -> obj -> bool -> IDisposable - - /// Type representing a Log - type Log = - { LogLevel: LogLevel - Message: MessageThunk - Exception: exn option - Parameters: obj list - AdditionalNamedParameters: ((string * obj * bool) list) } - static member StartLogLevel(logLevel: LogLevel) = - { LogLevel = logLevel - Message = None - Exception = None - Parameters = List.empty - AdditionalNamedParameters = List.empty } - - /// An interface wrapper for `Logger`. Useful when using depedency injection frameworks. - type ILog = - abstract member Log: Logger - abstract member MappedContext: MappedContext - - [] - module Inner = - open System.Collections.Generic - - type DisposableStack() = - let stack = Stack() - - interface IDisposable with - member __.Dispose() = - while stack.Count > 0 do - stack.Pop().Dispose() - - member __.Push(item: IDisposable) = stack.Push item - member __.Push(items: IDisposable list) = items |> List.iter stack.Push - - static member Create(items: IDisposable list) = - let ds = new DisposableStack() - ds.Push items - ds - - type ILog with - - /// **Description** - /// - /// Logs a log - /// - /// **Parameters** - /// * `log` - parameter of type `Log` - /// - /// **Output Type** - /// * `bool` - member logger.fromLog(log: Log) = - use __ = - log.AdditionalNamedParameters - |> List.map (fun (key, value, destructure) -> logger.MappedContext key value destructure) - // This stack is important, it causes us to unwind as if you have multiple uses in a row - |> DisposableStack.Create - - log.Parameters - |> List.toArray - |> logger.Log log.LogLevel log.Message log.Exception - - /// **Description** - /// - /// Logs a fatal log message given a log configurer. Lets caller know if log was sent with boolean return. - /// - /// **Parameters** - /// * `logConfig` - parameter of type `Log -> Log` - /// - /// **Output Type** - /// * `bool` - member logger.fatal'(logConfig: Log -> Log) = - Log.StartLogLevel LogLevel.Fatal - |> logConfig - |> logger.fromLog - - /// **Description** - /// - /// Logs a fatal log message given a log configurer - /// - /// **Parameters** - /// * `logConfig` - parameter of type `Log -> Log` - /// - /// **Output Type** - /// * `unit` - member logger.fatal(logConfig: Log -> Log) = logger.fatal' logConfig |> ignore - - /// **Description** - /// - /// Logs a error log message given a log configurer. Lets caller know if log was sent with boolean return. - /// - /// **Parameters** - /// * `logConfig` - parameter of type `Log -> Log` - /// - /// **Output Type** - /// * `bool` - member logger.error'(logConfig: Log -> Log) = - Log.StartLogLevel LogLevel.Error - |> logConfig - |> logger.fromLog - - /// **Description** - /// - /// Logs an error log message given a log configurer - /// - /// **Parameters** - /// * `logConfig` - parameter of type `Log -> Log` - /// - /// **Output Type** - /// * `unit` - member logger.error(logConfig: Log -> Log) = logger.error' logConfig |> ignore - - /// **Description** - /// - /// Logs a warn log message given a log configurer. Lets caller know if log was sent with boolean return. - /// - /// **Parameters** - /// * `logConfig` - parameter of type `Log -> Log` - /// - /// **Output Type** - /// * `bool` - member logger.warn'(logConfig: Log -> Log) = - Log.StartLogLevel LogLevel.Warn - |> logConfig - |> logger.fromLog - - /// **Description** - /// - /// Logs a warn log message given a log configurer - /// - /// **Parameters** - /// * `logConfig` - parameter of type `Log -> Log` - /// - /// **Output Type** - /// * `unit` - member logger.warn(logConfig: Log -> Log) = logger.warn' logConfig |> ignore - - /// **Description** - /// - /// Logs a info log message given a log configurer. Lets caller know if log was sent with boolean return. - /// - /// **Parameters** - /// * `logConfig` - parameter of type `Log -> Log` - /// - /// **Output Type** - /// * `bool` - member logger.info'(logConfig: Log -> Log) = - Log.StartLogLevel LogLevel.Info - |> logConfig - |> logger.fromLog - - /// **Description** - /// - /// Logs a info log message given a log configurer - /// - /// **Parameters** - /// * `logConfig` - parameter of type `Log -> Log` - /// - /// **Output Type** - /// * `unit` - member logger.info(logConfig: Log -> Log) = logger.info' logConfig |> ignore - - /// **Description** - /// - /// Logs a debug log message given a log configurer. Lets caller know if log was sent with boolean return. - /// - /// **Parameters** - /// * `logConfig` - parameter of type `Log -> Log` - /// - /// **Output Type** - /// * `bool` - member logger.debug'(logConfig: Log -> Log) = - Log.StartLogLevel LogLevel.Debug - |> logConfig - |> logger.fromLog - - /// **Description** - /// - /// Logs a debug log message given a log configurer - /// - /// **Parameters** - /// * `logConfig` - parameter of type `Log -> Log` - /// - /// **Output Type** - /// * `unit` - member logger.debug(logConfig: Log -> Log) = logger.debug' logConfig |> ignore - - /// **Description** - /// - /// Logs a trace log message given a log configurer. Lets caller know if log was sent with boolean return. - /// - /// **Parameters** - /// * `logConfig` - parameter of type `Log -> Log` - /// - /// **Output Type** - /// * `bool` - member logger.trace'(logConfig: Log -> Log) = - Log.StartLogLevel LogLevel.Trace - |> logConfig - |> logger.fromLog - - /// **Description** - /// - /// Logs a trace log message given a log configurer - /// - /// **Parameters** - /// * `logConfig` - parameter of type `Log -> Log` - /// - /// **Output Type** - /// * `unit` - member logger.trace(logConfig: Log -> Log) = logger.trace' logConfig |> ignore - - - /// An interface for retrieving a concrete logger such as Serilog, Nlog, etc. - type ILogProvider = - abstract member GetLogger: string -> Logger - abstract member OpenNestedContext: string -> IDisposable - abstract member OpenMappedContext: string -> obj -> bool -> IDisposable - - module Log = - - /// **Description** - /// - /// Amends a `Log` with a message - /// - /// **Parameters** - /// * `message` - parameter of type `string` - /// * `log` - parameter of type `Log` - /// - /// **Output Type** - /// * `Log` - let setMessage (message: string) (log: Log) = { log with Message = Some(fun () -> message) } - - /// **Description** - /// - /// Amends a `Log` with a message thunk. Useful for "expensive" string construction scenarios. - /// - /// **Parameters** - /// * `messageThunk` - parameter of type `unit -> string` - /// * `log` - parameter of type `Log` - /// - /// **Output Type** - /// * `Log` - /// - /// **Exceptions** - /// - let setMessageThunk (messageThunk: unit -> string) (log: Log) = { log with Message = Some messageThunk } - - /// **Description** - /// - /// Amends a `Log` with a parameter. - /// - /// **Parameters** - /// * `param` - parameter of type `'a` - /// * `log` - parameter of type `Log` - /// - /// **Output Type** - /// * `Log` - /// - /// **Exceptions** - /// - let addParameter (param: 'a) (log: Log) = { log with Parameters = List.append log.Parameters [ (box param) ] } - - /// **Description** - /// - /// Amends a `Log` with a list of parameters. - /// - /// **Parameters** - /// * `params` - parameter of type `obj list` - /// * `log` - parameter of type `Log` - /// - /// **Output Type** - /// * `Log` - /// - /// **Exceptions** - /// - let addParameters (``params``: obj list) (log: Log) = - let ``params`` = ``params`` |> List.map box - { log with Parameters = log.Parameters @ ``params`` } - - - - /// **Description** - /// - /// Amends a `Log` with additional named parameters for context. This helper adds more context to a log. - /// This DOES NOT affect the parameters set for a message template. - /// This is the same calling OpenMappedContext right before logging. - /// - /// **Parameters** - /// * `key` - parameter of type `string` - /// * `value` - parameter of type `obj` - /// * `log` - parameter of type `Log` - /// - /// **Output Type** - /// * `Log` - /// - /// **Exceptions** - /// - let addContext (key: string) (value: obj) (log: Log) = - { log with AdditionalNamedParameters = List.append log.AdditionalNamedParameters [ key, (box value), false ] } - - - /// **Description** - /// - /// Amends a `Log` with additional named parameters for context. This helper adds more context to a log. - /// This DOES NOT affect the parameters set for a message template. - /// This is the same calling OpenMappedContext right before logging. - /// This destructures an object rather than calling `ToString()` on it. - /// WARNING: Destructring can be expensive. - /// - /// **Parameters** - /// * `key` - parameter of type `string` - /// * `value` - parameter of type `obj` - /// * `log` - parameter of type `Log` - /// - /// **Output Type** - /// * `Log` - /// - /// **Exceptions** - /// - let addContextDestructured (key: string) (value: obj) (log: Log) = - { log with AdditionalNamedParameters = List.append log.AdditionalNamedParameters [ key, (box value), true ] } - - - /// **Description** - /// - /// Amends a `Log` with an `exn`. Handles nulls. - /// - /// **Parameters** - /// * `exception` - parameter of type `exn` - /// * `log` - parameter of type `Log` - /// - /// **Output Type** - /// * `Log` - /// - /// **Exceptions** - /// - let addException (``exception``: exn) (log: Log) = { log with Exception = Option.ofObj ``exception`` } - - /// **Description** - /// - /// Amends a `Log` with an `exn`. Handles nulls. - /// - /// **Parameters** - /// * `exception` - parameter of type `exn` - /// * `log` - parameter of type `Log` - /// - /// **Output Type** - /// * `Log` - /// - /// **Exceptions** - /// - let addExn (``exception``: exn) (log: Log) = addException ``exception`` log - - /// **Description** - /// - /// Amends a `Log` with a given `LogLevel` - /// - /// **Parameters** - /// * `logLevel` - parameter of type `LogLevel` - /// * `log` - parameter of type `Log` - /// - /// **Output Type** - /// * `Log` - /// - /// **Exceptions** - /// - let setLogLevel (logLevel: LogLevel) (log: Log) = { log with LogLevel = logLevel } - -module Providers = - module SerilogProvider = - open System - open System.Linq.Expressions - - let getLogManagerType () = Type.GetType("Serilog.Log, Serilog") - let isAvailable () = getLogManagerType () |> isNull |> not - - let getPushProperty () = - - let ndcContextType = - Type.GetType("Serilog.Context.LogContext, Serilog") - |> Option.ofObj - |> Option.defaultWith (fun () -> Type.GetType("Serilog.Context.LogContext, Serilog.FullNetFx")) - - () - - let pushPropertyMethod = - ndcContextType.GetMethod("PushProperty", [| typedefof; typedefof; typedefof |]) - - let nameParam = Expression.Parameter(typedefof, "name") - let valueParam = Expression.Parameter(typedefof, "value") - let destructureObjectParam = Expression.Parameter(typedefof, "destructureObjects") - - let pushPropertyMethodCall = - Expression.Call(null, pushPropertyMethod, nameParam, valueParam, destructureObjectParam) - - let pushProperty = - Expression - .Lambda>( - pushPropertyMethodCall, - nameParam, - valueParam, - destructureObjectParam - ) - .Compile() - - fun key value destructure -> pushProperty.Invoke(key, value, destructure) - - - let getForContextMethodCall () = - let logManagerType = getLogManagerType () - let method = logManagerType.GetMethod("ForContext", [| typedefof; typedefof; typedefof |]) - let propertyNameParam = Expression.Parameter(typedefof, "propertyName") - let valueParam = Expression.Parameter(typedefof, "value") - let destructureObjectsParam = Expression.Parameter(typedefof, "destructureObjects") - let exrs: Expression [] = [| propertyNameParam; valueParam; destructureObjectsParam |] - let methodCall = Expression.Call(null, method, exrs) - - let func = - Expression - .Lambda>(methodCall, propertyNameParam, valueParam, destructureObjectsParam) - .Compile() - - fun name -> func.Invoke("SourceContext", name, false) - - type SerilogGateway = - { Write: obj -> obj -> string -> obj [] -> unit - WriteException: obj -> obj -> exn -> string -> obj [] -> unit - IsEnabled: obj -> obj -> bool - TranslateLevel: LogLevel -> obj } - static member Create() = - let logEventLevelType = Type.GetType("Serilog.Events.LogEventLevel, Serilog") - - if (logEventLevelType |> isNull) then - failwith ("Type Serilog.Events.LogEventLevel was not found.") - - let debugLevel = Enum.Parse(logEventLevelType, "Debug", false) - let errorLevel = Enum.Parse(logEventLevelType, "Error", false) - let fatalLevel = Enum.Parse(logEventLevelType, "Fatal", false) - let informationLevel = Enum.Parse(logEventLevelType, "Information", false) - let verboseLevel = Enum.Parse(logEventLevelType, "Verbose", false) - let warningLevel = Enum.Parse(logEventLevelType, "Warning", false) - - let translateLevel (level: LogLevel) = - match level with - | LogLevel.Fatal -> fatalLevel - | LogLevel.Error -> errorLevel - | LogLevel.Warn -> warningLevel - | LogLevel.Info -> informationLevel - | LogLevel.Debug -> debugLevel - | LogLevel.Trace -> verboseLevel - | _ -> debugLevel - - let loggerType = Type.GetType("Serilog.ILogger, Serilog") - - if (loggerType |> isNull) then - failwith ("Type Serilog.ILogger was not found.") - - let isEnabledMethodInfo = loggerType.GetMethod("IsEnabled", [| logEventLevelType |]) - let instanceParam = Expression.Parameter(typedefof) - let instanceCast = Expression.Convert(instanceParam, loggerType) - let levelParam = Expression.Parameter(typedefof) - let levelCast = Expression.Convert(levelParam, logEventLevelType) - let isEnabledMethodCall = Expression.Call(instanceCast, isEnabledMethodInfo, levelCast) - - let isEnabled = - Expression.Lambda>(isEnabledMethodCall, instanceParam, levelParam).Compile() - - let writeMethodInfo = - loggerType.GetMethod("Write", [| logEventLevelType; typedefof; typedefof |]) - - let messageParam = Expression.Parameter(typedefof) - let propertyValuesParam = Expression.Parameter(typedefof) - - let writeMethodExp = - Expression.Call(instanceCast, writeMethodInfo, levelCast, messageParam, propertyValuesParam) - - let expression = - Expression.Lambda>( - writeMethodExp, - instanceParam, - levelParam, - messageParam, - propertyValuesParam - ) - - let write = expression.Compile() - - let writeExceptionMethodInfo = - loggerType.GetMethod("Write", [| logEventLevelType; typedefof; typedefof; typedefof |]) - - let exceptionParam = Expression.Parameter(typedefof) - - let writeMethodExp = - Expression.Call( - instanceCast, - writeExceptionMethodInfo, - levelCast, - exceptionParam, - messageParam, - propertyValuesParam - ) - - let writeException = - Expression - .Lambda>( - writeMethodExp, - instanceParam, - levelParam, - exceptionParam, - messageParam, - propertyValuesParam - ) - .Compile() - - { Write = - (fun logger level message formattedParmeters -> write.Invoke(logger, level, message, formattedParmeters)) - WriteException = - fun logger level ex message formattedParmeters -> - writeException.Invoke(logger, level, ex, message, formattedParmeters) - IsEnabled = fun logger level -> isEnabled.Invoke(logger, level) - TranslateLevel = translateLevel } - - type private SerigLogProvider() = - let getLoggerByName = getForContextMethodCall () - let pushProperty = getPushProperty () - let serilogGatewayInit = lazy (SerilogGateway.Create()) - - let writeMessage logger logLevel (messageFunc: MessageThunk) ``exception`` formatParams = - let serilogGateway = serilogGatewayInit.Value - let translatedValue = serilogGateway.TranslateLevel logLevel - - match messageFunc with - | None -> serilogGateway.IsEnabled logger translatedValue - | Some _ when serilogGateway.IsEnabled logger translatedValue |> not -> false - | Some m -> - match ``exception`` with - | Some ex -> serilogGateway.WriteException logger translatedValue ex (m ()) formatParams - | None -> serilogGateway.Write logger translatedValue (m ()) formatParams - - true - - interface ILogProvider with - member this.GetLogger(name: string) : Logger = getLoggerByName name |> writeMessage - - member this.OpenMappedContext (key: string) (value: obj) (destructure: bool) : IDisposable = - pushProperty key value destructure - - member this.OpenNestedContext(message: string) : IDisposable = pushProperty "NDC" message false - - let create () = SerigLogProvider() :> ILogProvider - -module LogProvider = - open System - open Types - open Providers - open System.Diagnostics - open Microsoft.FSharp.Quotations.Patterns - - let mutable private currentLogProvider = None - - let private knownProviders = [ (SerilogProvider.isAvailable, SerilogProvider.create) ] - - /// Greedy search for first available LogProvider. Order of known providers matters. - let private resolvedLogger = - lazy - (knownProviders - |> Seq.tryFind (fun (isAvailable, _) -> isAvailable ()) - |> Option.map (fun (_, create) -> create ())) - - let private noopLogger _ _ _ _ = false - - let private noopDisposable = - { new IDisposable with - member __.Dispose() = () } - - /// **Description** - /// - /// Allows custom override when `getLogger` searches for a LogProvider. - /// - /// **Parameters** - /// * `provider` - parameter of type `ILogProvider` - /// - /// **Output Type** - /// * `unit` - let setLoggerProvider (logProvider: ILogProvider) = currentLogProvider <- Some logProvider - - let getCurrentLogProvider () = - match currentLogProvider with - | None -> resolvedLogger.Value - | Some p -> Some p - - /// **Description** - /// - /// Opens a mapped diagnostic context. This will allow you to set additional parameters to a log given a scope. - /// - /// **Parameters** - /// * `key` - parameter of type `string` - The name of the property. - /// * `value` - parameter of type `obj` - The value of the property. - /// * `destructureObjects` - parameter of type `bool` - If true, and the value is a non-primitive, non-array type, then the value will be converted to a structure; otherwise, unknown types will be converted to scalars, which are generally stored as strings. WARNING: Destructring can be expensive. - /// - /// **Output Type** - /// * `IDisposable` - let openMappedContextDestucturable (key: string) (value: obj) (destructureObjects: bool) = - let provider = getCurrentLogProvider () - - match provider with - | Some p -> p.OpenMappedContext key value destructureObjects - | None -> noopDisposable - - /// **Description** - /// - /// Opens a mapped diagnostic context. This will allow you to set additional parameters to a log given a scope. Sets destructureObjects to false. - /// - /// **Parameters** - /// * `key` - parameter of type `string` - The name of the property. - /// * `value` - parameter of type `obj` - The value of the property. - /// - /// **Output Type** - /// * `IDisposable` - let openMappedContext (key: string) (value: obj) = - //TODO: We should try to find out if the value is a primitive - openMappedContextDestucturable key value false - - /// **Description** - /// - /// Opens a nested diagnostic context. This will allow you to set additional parameters to a log given a scope. - /// - /// **Parameters** - /// * `value` - parameter of type `string` - The value of the property. - /// - /// **Output Type** - /// * `IDisposable` - let openNestedContext (value: string) = - let provider = getCurrentLogProvider () - - match provider with - | Some p -> p.OpenNestedContext value - | None -> noopDisposable - - /// **Description** - /// - /// Creates a logger given a `string`. This will attempt to retrieve any loggers set with `setLoggerProvider`. It will fallback to a known list of providers. - /// - /// **Parameters** - /// * `string` - parameter of type `string` - /// - /// **Output Type** - /// * `ILog` - let getLoggerByName (name: string) = - let loggerProvider = getCurrentLogProvider () - - let logFunc = - match loggerProvider with - | Some loggerProvider -> loggerProvider.GetLogger(name) - | None -> noopLogger - - { new ILog with - member x.Log = logFunc - member x.MappedContext = openMappedContextDestucturable } - - /// **Description** - /// - /// Creates a logger given a `Type`. This will attempt to retrieve any loggers set with `setLoggerProvider`. It will fallback to a known list of providers. - /// - /// **Parameters** - /// * `type` - parameter of type `Type` - /// - /// **Output Type** - /// * `ILog` - let getLoggerByType (``type``: Type) = ``type`` |> string |> getLoggerByName - - /// **Description** - /// - /// Creates a logger given a `'a` type. This will attempt to retrieve any loggers set with `setLoggerProvider`. It will fallback to a known list of providers. - /// - /// **Output Type** - /// * `ILog` - /// - let getLoggerFor<'a> () = getLoggerByType (typeof<'a>) - - let rec getModuleType = - function - | PropertyGet (_, propertyInfo, _) -> propertyInfo.DeclaringType - // | Call (_, methInfo, _) -> sprintf "%s.%s" methInfo.DeclaringType.FullName methInfo.Name - // | Lambda(_, expr) -> getModuleType expr - // | ValueWithName(_,_,instance) -> instance - | x -> failwithf "Expression is not a property. %A" x - - /// **Description** - /// - /// Creates a logger given a Quotations.Expr type. This is only useful for module level declarations. It uses the DeclaringType on the PropertyInfo of the PropertyGet. - /// - /// It can be utilized like: - /// - /// `let rec logger = LogProvider.getLoggerByQuotation <@ logger @>` - /// - /// inside a module to get the modules full qualitfied name. - /// - /// **Parameters** - /// * `quotation` - parameter of type `Quotations.Expr` - /// - /// **Output Type** - /// * `ILog` - /// - /// **Exceptions** - /// - let getLoggerByQuotation (quotation: Quotations.Expr) = getModuleType quotation |> getLoggerByType - - - - /// **Description** - /// - /// Creates a logger based on `Reflection.MethodBase.GetCurrentMethod` call. This is only useful for calls within functions. This does not protect against inlined functions. - /// - /// **Output Type** - /// * `ILog` - /// - /// **Exceptions** - /// - let inline getLoggerByFunc () = - let mi = Reflection.MethodBase.GetCurrentMethod() - - sprintf "%s.%s" mi.DeclaringType.FullName mi.Name - |> getLoggerByName - - - /// **Description** - /// - /// Creates a logger. It's name is based on the current StackFrame. This will attempt to retrieve any loggers set with `setLoggerProvider`. It will fallback to a known list of providers. - /// Obsolete: getCurrentLogger is obsolete, choose another provider factory function. - /// - /// **Output Type** - /// * `ILog` - [] - let getCurrentLogger () = - let stackFrame = StackFrame(2, false) - getLoggerByType (stackFrame.GetMethod().DeclaringType) \ No newline at end of file diff --git a/src/Ionide.LanguageServerProtocol.fsproj b/src/Ionide.LanguageServerProtocol.fsproj index aeecce8..fa32021 100644 --- a/src/Ionide.LanguageServerProtocol.fsproj +++ b/src/Ionide.LanguageServerProtocol.fsproj @@ -12,26 +12,34 @@ https://github.com/ionide/LanguageServerProtocol - - - - + - - - + + + - - + + \ No newline at end of file diff --git a/src/JsonRpc.fs b/src/JsonRpc.fs deleted file mode 100644 index efd04cf..0000000 --- a/src/JsonRpc.fs +++ /dev/null @@ -1,80 +0,0 @@ -module Ionide.LanguageServerProtocol.JsonRpc - -open Newtonsoft.Json -open Newtonsoft.Json.Linq - -type MessageTypeTest = - { [] - Version: string - Id: int option - Method: string option } - -[] -type MessageType = - | Notification - | Request - | Response - | Error - -let getMessageType messageTest = - match messageTest with - | { Version = "2.0"; Id = Some _; Method = Some _ } -> MessageType.Request - | { Version = "2.0"; Id = Some _; Method = None } -> MessageType.Response - | { Version = "2.0"; Id = None; Method = Some _ } -> MessageType.Notification - | _ -> MessageType.Error - -type Request = - { [] - Version: string - Id: int - Method: string - Params: JToken option } - static member Create(id: int, method': string, rpcParams: JToken option) = - { Version = "2.0"; Id = id; Method = method'; Params = rpcParams } - -type Notification = - { [] - Version: string - Method: string - Params: JToken option } - static member Create(method': string, rpcParams: JToken option) = - { Version = "2.0"; Method = method'; Params = rpcParams } - -module ErrorCodes = - let parseError = -32700 - let invalidRequest = -32600 - let methodNotFound = -32601 - let invalidParams = -32602 - let internalError = -32603 - let serverErrorStart = -32000 - let serverErrorEnd = -32099 - let requestCancelled = -32800 - -type Error = - { Code: int - Message: string - Data: JToken option } - static member Create(code: int, message: string) = { Code = code; Message = message; Data = None } - static member ParseError = Error.Create(ErrorCodes.parseError, "Parse error") - static member InvalidRequest = Error.Create(ErrorCodes.invalidRequest, "Invalid Request") - static member MethodNotFound = Error.Create(ErrorCodes.methodNotFound, "Method not found") - static member InvalidParams = Error.Create(ErrorCodes.invalidParams, "Invalid params") - static member InternalError = Error.Create(ErrorCodes.internalError, "Internal error") - static member InternalErrorMessage message = Error.Create(ErrorCodes.internalError, message) - static member RequestCancelled = Error.Create(ErrorCodes.requestCancelled, "Request cancelled") - static member RequestCancelledMessage message = Error.Create(ErrorCodes.requestCancelled, message) - -type Response = - { [] - Version: string - Id: int option - Error: Error option - [] - Result: JToken option } - /// Json.NET conditional property serialization, controlled by naming convention - member x.ShouldSerializeResult() = x.Error.IsNone - - static member Success(id: int, result: JToken option) = - { Version = "2.0"; Id = Some id; Result = result; Error = None } - - static member Failure(id: int, error: Error) = { Version = "2.0"; Id = Some id; Result = None; Error = Some error } \ No newline at end of file diff --git a/src/LanguageServerProtocol.fs b/src/LanguageServerProtocol.fs index 1953807..d99bdb7 100644 --- a/src/LanguageServerProtocol.fs +++ b/src/LanguageServerProtocol.fs @@ -1,558 +1,66 @@ namespace Ionide.LanguageServerProtocol +module JsonRpc = + open StreamJsonRpc + + let verboseLogging (rpc: JsonRpc) = rpc.TraceSource.Switch.Level <- System.Diagnostics.SourceLevels.Verbose + let addTraceLogger listener (rpc: JsonRpc) = rpc.TraceSource.Listeners.Add listener + module Server = - open System open System.IO - open Ionide.LanguageServerProtocol.Logging - open Ionide.LanguageServerProtocol.Types - open System.Threading - open System.Threading.Tasks - open System.Reflection open StreamJsonRpc open Newtonsoft.Json open Ionide.LanguageServerProtocol.JsonUtils - open Newtonsoft.Json.Linq - - let logger = LogProvider.getLoggerByName "LSP Server" - - let jsonRpcFormatter = new JsonMessageFormatter() - jsonRpcFormatter.JsonSerializer.NullValueHandling <- NullValueHandling.Ignore - jsonRpcFormatter.JsonSerializer.ConstructorHandling <- ConstructorHandling.AllowNonPublicDefaultConstructor - jsonRpcFormatter.JsonSerializer.MissingMemberHandling <- MissingMemberHandling.Ignore - jsonRpcFormatter.JsonSerializer.Converters.Add(StrictNumberConverter()) - jsonRpcFormatter.JsonSerializer.Converters.Add(StrictStringConverter()) - jsonRpcFormatter.JsonSerializer.Converters.Add(StrictBoolConverter()) - jsonRpcFormatter.JsonSerializer.Converters.Add(SingleCaseUnionConverter()) - jsonRpcFormatter.JsonSerializer.Converters.Add(OptionConverter()) - jsonRpcFormatter.JsonSerializer.Converters.Add(ErasedUnionConverter()) - jsonRpcFormatter.JsonSerializer.ContractResolver <- OptionAndCamelCasePropertyNamesContractResolver() - - let deserialize<'t> (token: JToken) = token.ToObject<'t>(jsonRpcFormatter.JsonSerializer) - let serialize<'t> (o: 't) = JToken.FromObject(o, jsonRpcFormatter.JsonSerializer) - - let requestHandling<'param, 'result> (run: 'param -> AsyncLspResult<'result>) : Delegate = - let runAsTask param ct = - // Execute non-async portion of `run` before forking the async portion into a task. - // This is needed to avoid reordering of messages from a client. - let asyncLspResult = run param - - let asyncContinuation = - async { - let! lspResult = asyncLspResult - - return - match lspResult with - | Ok result -> result - | Error error -> - let rpcException = LocalRpcException(error.Message) - rpcException.ErrorCode <- error.Code - rpcException.ErrorData <- error.Data |> Option.defaultValue null - raise rpcException - } - - Async.StartAsTask(asyncContinuation, cancellationToken = ct) - - Func<'param, CancellationToken, Task<'result>>(runAsTask) :> Delegate - - /// Notifications don't generate a response or error, but to unify things we consider them as always successful. - /// They will still not send any response because their ID is null. - let private notificationSuccess (response: Async) = - async { - do! response - return Result.Ok() - } - type ClientNotificationSender = string -> obj -> AsyncLspResult - - type ClientRequestSender = - abstract member Send<'a> : string -> obj -> AsyncLspResult<'a> - - type private MessageHandlingResult = - | Normal - | WasExit - | WasShutdown - - type LspCloseReason = - | RequestedByClient = 0 - | ErrorExitWithoutShutdown = 1 - | ErrorStreamClosed = 2 - - let startWithSetup<'client when 'client :> Ionide.LanguageServerProtocol.ILspClient> - (setupRequestHandlings: 'client -> Map) - (input: Stream) - (output: Stream) - (clientCreator: (ClientNotificationSender * ClientRequestSender) -> 'client) + let jsonRpcFormatter () = + let f = new JsonMessageFormatter() + f.JsonSerializer.NullValueHandling <- NullValueHandling.Ignore + f.JsonSerializer.ConstructorHandling <- ConstructorHandling.AllowNonPublicDefaultConstructor + f.JsonSerializer.MissingMemberHandling <- MissingMemberHandling.Ignore + f.JsonSerializer.Converters.Add(StrictNumberConverter()) + f.JsonSerializer.Converters.Add(StrictStringConverter()) + f.JsonSerializer.Converters.Add(StrictBoolConverter()) + f.JsonSerializer.Converters.Add(SingleCaseUnionConverter()) + f.JsonSerializer.Converters.Add(OptionConverter()) + f.JsonSerializer.Converters.Add(ErasedUnionConverter()) + f.JsonSerializer.ContractResolver <- OptionAndCamelCasePropertyNamesContractResolver() + f + + + let configureBidirectionalServer<'server when 'server :> ILspServer and 'server :> ILspClient> + (serverInput: Stream) + (serverOutput: Stream) + (serverFactory: JsonSerializer -> 'server) + modifyJsonSerializer = - use jsonRpcHandler = new HeaderDelimitedMessageHandler(output, input, jsonRpcFormatter) - // Without overriding isFatalException, JsonRpc serializes exceptions and sends them to the client. - // This is particularly bad for notifications such as textDocument/didChange which don't require a response, - // and thus any exception that happens during e.g. text sync gets swallowed. - use jsonRpc = - { new JsonRpc(jsonRpcHandler) with - member this.IsFatalException(ex: Exception) = - match ex with - | :? LocalRpcException -> false - | _ -> true } - - /// When the server wants to send a notification to the client - let sendServerNotification (rpcMethod: string) (notificationObj: obj) : AsyncLspResult = - async { - do! - jsonRpc.NotifyWithParameterObjectAsync(rpcMethod, notificationObj) - |> Async.AwaitTask - - return () |> LspResult.success - } + let createJsonFormatter () = + let f = jsonRpcFormatter () + modifyJsonSerializer f.JsonSerializer + f - /// When the server wants to send a request to the client - let sendServerRequest (rpcMethod: string) (requestObj: obj) : AsyncLspResult<'response> = - async { - let! response = - jsonRpc.InvokeWithParameterObjectAsync<'response>(rpcMethod, requestObj) - |> Async.AwaitTask - - return response |> LspResult.success - } - - let lspClient = - clientCreator ( - sendServerNotification, - { new ClientRequestSender with - member __.Send x t = sendServerRequest x t } + let commonOptions = + JsonRpcTargetOptions( + ClientRequiresNamedArguments = true, + UseSingleObjectParameterDeserialization = true, + DisposeOnDisconnect = true ) - // Note on server shutdown. - // According the the LSP spec the shutdown sequence consists fo a client sending onShutdown request followed by - // onExit notification. The server can terminate after receiving onExit. However, real language clients implements - // the shutdown in their own way: - // 1. VSCode Language Client has a bug that causes it to NOT send an `exit` notification when stopping a server: - // https://github.com/microsoft/vscode-languageserver-node/pull/776 - // VSCode sends onShutdown and then closes the connection. - // 2. Neovim LSP sends onShutdown followed by onExit but does NOT close the connection on its own. - // 3. Emacs LSP mode sends onShutdown followed by onExit and then closes the connection. - // This is the reason for the complicated logic below. - - let mutable shutdownReceived = false - let mutable quitReceived = false - use quitSemaphore = new SemaphoreSlim(0, 1) - - let onShutdown () = - logger.trace (Log.setMessage "Shutdown received") - shutdownReceived <- true - - jsonRpc.AddLocalRpcMethod("shutdown", Action(onShutdown)) - - let onExit () = - logger.trace (Log.setMessage "Exit received") - quitReceived <- true - quitSemaphore.Release() |> ignore - - jsonRpc.AddLocalRpcMethod("exit", Action(onExit)) - - for handling in setupRequestHandlings lspClient do - let rpcMethodName = handling.Key - let rpcDelegate = handling.Value - - let rpcAttribute = JsonRpcMethodAttribute(rpcMethodName) - rpcAttribute.UseSingleObjectParameterDeserialization <- true - - jsonRpc.AddLocalRpcMethod(rpcDelegate.GetMethodInfo(), rpcDelegate.Target, rpcAttribute) - - jsonRpc.StartListening() - - // 1. jsonRpc.Completion finishes when either a connection is closed or a fatal exception is thrown. - // 2. quitSemaphore is released when the server receives both onShutdown and onExit. - // Completion of either of those causes the server to stop. - let completed_task_idx = Task.WaitAny(jsonRpc.Completion, quitSemaphore.WaitAsync()) - // jsonRpc.Completion throws on fatal exception. However, Task.WaitAny doesn't even when jsonRpc.Completion would. - // Here we check and re-raise if needed. - if completed_task_idx = 0 then - match jsonRpc.Completion.Exception with - | null -> () - | exn -> raise exn - - match shutdownReceived, quitReceived with - | true, true -> LspCloseReason.RequestedByClient - | false, true -> LspCloseReason.ErrorExitWithoutShutdown - | _ -> LspCloseReason.ErrorStreamClosed - - type ServerRequestHandling<'server when 'server :> Ionide.LanguageServerProtocol.ILspServer> = - { Run: 'server -> Delegate } - - let serverRequestHandling<'server, 'param, 'result when 'server :> Ionide.LanguageServerProtocol.ILspServer> - (run: 'server -> 'param -> AsyncLspResult<'result>) - : ServerRequestHandling<'server> = - { Run = fun s -> requestHandling (run s) } - - let defaultRequestHandlings () : Map> = - let requestHandling = serverRequestHandling - - [ "initialize", requestHandling (fun s p -> s.Initialize(p)) - "initialized", requestHandling (fun s p -> s.Initialized(p) |> notificationSuccess) - "textDocument/hover", requestHandling (fun s p -> s.TextDocumentHover(p)) - "textDocument/didOpen", requestHandling (fun s p -> s.TextDocumentDidOpen(p) |> notificationSuccess) - "textDocument/didChange", requestHandling (fun s p -> s.TextDocumentDidChange(p) |> notificationSuccess) - "textDocument/completion", requestHandling (fun s p -> s.TextDocumentCompletion(p)) - "completionItem/resolve", requestHandling (fun s p -> s.CompletionItemResolve(p)) - "textDocument/rename", requestHandling (fun s p -> s.TextDocumentRename(p)) - "textDocument/prepareRename", requestHandling (fun s p -> s.TextDocumentPrepareRename(p)) - "textDocument/definition", requestHandling (fun s p -> s.TextDocumentDefinition(p)) - "textDocument/typeDefinition", requestHandling (fun s p -> s.TextDocumentTypeDefinition(p)) - "textDocument/implementation", requestHandling (fun s p -> s.TextDocumentImplementation(p)) - "textDocument/codeAction", requestHandling (fun s p -> s.TextDocumentCodeAction(p)) - "codeAction/resolve", requestHandling (fun s p -> s.CodeActionResolve(p)) - "textDocument/codeLens", requestHandling (fun s p -> s.TextDocumentCodeLens(p)) - "codeLens/resolve", requestHandling (fun s p -> s.CodeLensResolve(p)) - "textDocument/references", requestHandling (fun s p -> s.TextDocumentReferences(p)) - "textDocument/documentHighlight", requestHandling (fun s p -> s.TextDocumentDocumentHighlight(p)) - "textDocument/documentLink", requestHandling (fun s p -> s.TextDocumentDocumentLink(p)) - "textDocument/signatureHelp", requestHandling (fun s p -> s.TextDocumentSignatureHelp(p)) - "documentLink/resolve", requestHandling (fun s p -> s.DocumentLinkResolve(p)) - "textDocument/documentColor", requestHandling (fun s p -> s.TextDocumentDocumentColor(p)) - "textDocument/colorPresentation", requestHandling (fun s p -> s.TextDocumentColorPresentation(p)) - "textDocument/formatting", requestHandling (fun s p -> s.TextDocumentFormatting(p)) - "textDocument/rangeFormatting", requestHandling (fun s p -> s.TextDocumentRangeFormatting(p)) - "textDocument/onTypeFormatting", requestHandling (fun s p -> s.TextDocumentOnTypeFormatting(p)) - "textDocument/willSave", requestHandling (fun s p -> s.TextDocumentWillSave(p) |> notificationSuccess) - "textDocument/willSaveWaitUntil", requestHandling (fun s p -> s.TextDocumentWillSaveWaitUntil(p)) - "textDocument/didSave", requestHandling (fun s p -> s.TextDocumentDidSave(p) |> notificationSuccess) - "textDocument/didClose", requestHandling (fun s p -> s.TextDocumentDidClose(p) |> notificationSuccess) - "textDocument/documentSymbol", requestHandling (fun s p -> s.TextDocumentDocumentSymbol(p)) - "textDocument/foldingRange", requestHandling (fun s p -> s.TextDocumentFoldingRange(p)) - "textDocument/selectionRange", requestHandling (fun s p -> s.TextDocumentSelectionRange(p)) - "textDocument/semanticTokens/full", requestHandling (fun s p -> s.TextDocumentSemanticTokensFull(p)) - "textDocument/semanticTokens/full/delta", requestHandling (fun s p -> s.TextDocumentSemanticTokensFullDelta(p)) - "textDocument/semanticTokens/range", requestHandling (fun s p -> s.TextDocumentSemanticTokensRange(p)) - "textDocument/inlayHint", requestHandling (fun s p -> s.TextDocumentInlayHint(p)) - "inlayHint/resolve", requestHandling (fun s p -> s.InlayHintResolve(p)) - "workspace/didChangeWatchedFiles", - requestHandling (fun s p -> s.WorkspaceDidChangeWatchedFiles(p) |> notificationSuccess) - "workspace/didChangeWorkspaceFolders", - requestHandling (fun s p -> - s.WorkspaceDidChangeWorkspaceFolders(p) - |> notificationSuccess) - "workspace/didChangeConfiguration", - requestHandling (fun s p -> s.WorkspaceDidChangeConfiguration(p) |> notificationSuccess) - "workspace/willCreateFiles", requestHandling (fun s p -> s.WorkspaceWillCreateFiles(p)) - "workspace/didCreateFiles", requestHandling (fun s p -> s.WorkspaceDidCreateFiles(p) |> notificationSuccess) - "workspace/willRenameFiles", requestHandling (fun s p -> s.WorkspaceWillRenameFiles(p)) - "workspace/didRenameFiles", requestHandling (fun s p -> s.WorkspaceDidRenameFiles(p) |> notificationSuccess) - "workspace/willDeleteFiles", requestHandling (fun s p -> s.WorkspaceWillDeleteFiles(p)) - "workspace/didDeleteFiles", requestHandling (fun s p -> s.WorkspaceDidDeleteFiles(p) |> notificationSuccess) - "workspace/symbol", requestHandling (fun s p -> s.WorkspaceSymbol(p)) - "workspace/executeCommand", requestHandling (fun s p -> s.WorkspaceExecuteCommand(p)) - "shutdown", requestHandling (fun s () -> s.Shutdown() |> notificationSuccess) - "exit", requestHandling (fun s () -> s.Exit() |> notificationSuccess) ] - |> Map.ofList - - let start<'client, 'server when 'client :> Ionide.LanguageServerProtocol.ILspClient and 'server :> Ionide.LanguageServerProtocol.ILspServer> - (requestHandlings: Map>) - (input: Stream) - (output: Stream) - (clientCreator: (ClientNotificationSender * ClientRequestSender) -> 'client) - (serverCreator: 'client -> 'server) - = - let requestHandlingSetup client = - let server = serverCreator client - - requestHandlings - |> Map.map (fun _ requestHandling -> requestHandling.Run server) - - startWithSetup requestHandlingSetup input output clientCreator - -module Client = - open System - open System.Diagnostics - open System.IO - open Ionide.LanguageServerProtocol - open Ionide.LanguageServerProtocol.JsonRpc - open Ionide.LanguageServerProtocol.Logging - open Ionide.LanguageServerProtocol.JsonUtils - open Newtonsoft.Json - open Newtonsoft.Json.Serialization - open Newtonsoft.Json.Linq - - - let logger = LogProvider.getLoggerByName "LSP Client" - - let internal jsonSettings = - let result = JsonSerializerSettings(NullValueHandling = NullValueHandling.Ignore) - result.Converters.Add(OptionConverter()) - result.Converters.Add(ErasedUnionConverter()) - result.ContractResolver <- CamelCasePropertyNamesContractResolver() - result - - let internal jsonSerializer = JsonSerializer.Create(jsonSettings) - - let internal deserialize (token: JToken) = token.ToObject<'t>(jsonSerializer) - - let internal serialize (o: 't) = JToken.FromObject(o, jsonSerializer) - - type NotificationHandler = { Run: JToken -> Async } - - let notificationHandling<'p, 'r> (handler: 'p -> Async<'r option>) : NotificationHandler = - let run (token: JToken) = - async { - try - let p = token.ToObject<'p>(jsonSerializer) - let! res = handler p - - return - res - |> Option.map (fun n -> JToken.FromObject(n, jsonSerializer)) - with - | _ -> return None - } - - { Run = run } - - // TODO: replace this module with StreamJsonRpc like we did for the server - module LowLevel = - open System - open System.IO - open System.Text - - let headerBufferSize = 300 - let minimumHeaderLength = 21 - let cr = byte '\r' - let lf = byte '\f' - let headerEncoding = Encoding.ASCII - - let private readLine (stream: Stream) = - let buffer = Array.zeroCreate headerBufferSize - let readCount = stream.Read(buffer, 0, 2) - let mutable count = readCount - - if count < 2 then - None - else - // TODO: Check that we don't over-fill headerBufferSize - while count < headerBufferSize - && (buffer.[count - 2] <> cr && buffer.[count - 1] <> lf) do - let additionalBytesRead = stream.Read(buffer, count, 1) - // TODO: exit when additionalBytesRead = 0, end of stream - count <- count + additionalBytesRead - - if count >= headerBufferSize then - None - else - Some(headerEncoding.GetString(buffer, 0, count - 2)) - - let rec private readHeaders (stream: Stream) = - let line = readLine stream - - match line with - | Some "" -> [] - | Some line -> - let separatorPos = line.IndexOf(": ") - - if separatorPos = -1 then - raise (Exception(sprintf "Separator not found in header '%s'" line)) - else - let name = line.Substring(0, separatorPos) - let value = line.Substring(separatorPos + 2) - let otherHeaders = readHeaders stream - (name, value) :: otherHeaders - | None -> raise (EndOfStreamException()) - - let read (stream: Stream) = - let headers = readHeaders stream - - let contentLength = - headers - |> List.tryFind (fun (name, _) -> name = "Content-Length") - |> Option.map snd - |> Option.bind (fun s -> - match Int32.TryParse(s) with - | true, x -> Some x - | _ -> None) - - if contentLength = None then - failwithf "Content-Length header not found" - else - let result = Array.zeroCreate contentLength.Value - let mutable readCount = 0 - - while readCount < contentLength.Value do - let toRead = contentLength.Value - readCount - let readInCurrentBatch = stream.Read(result, readCount, toRead) - readCount <- readCount + readInCurrentBatch - - let str = Encoding.UTF8.GetString(result, 0, readCount) - headers, str - - let write (stream: Stream) (data: string) = - let bytes = Encoding.UTF8.GetBytes(data) - - let header = - sprintf "Content-Type: application/vscode-jsonrpc; charset=utf-8\r\nContent-Length: %d\r\n\r\n" bytes.Length - - let headerBytes = Encoding.ASCII.GetBytes header - use ms = new MemoryStream(headerBytes.Length + bytes.Length) - ms.Write(headerBytes, 0, headerBytes.Length) - ms.Write(bytes, 0, bytes.Length) - stream.Write(ms.ToArray(), 0, int ms.Position) - - type Client(exec: string, args: string, notificationHandlings: Map) = - - let mutable outuptStream: StreamReader option = None - let mutable inputStream: StreamWriter option = None - - let sender = - MailboxProcessor.Start - (fun inbox -> - let rec loop () = - async { - let! str = inbox.Receive() - - inputStream - |> Option.iter (fun input -> - // fprintfn stderr "[CLIENT] Writing: %s" str - LowLevel.write input.BaseStream str - input.BaseStream.Flush()) - // do! Async.Sleep 1000 - return! loop () - } - - loop ()) - - let handleRequest (request: JsonRpc.Request) = - async { - let mutable methodCallResult = None - - match notificationHandlings |> Map.tryFind request.Method with - | Some handling -> - try - match request.Params with - | None -> () - | Some prms -> - let! result = handling.Run prms - methodCallResult <- result - with - | ex -> methodCallResult <- None - | None -> () - - match methodCallResult with - | Some ok -> return Some(JsonRpc.Response.Success(request.Id, Some ok)) - | None -> return None - } - - let handleNotification (notification: JsonRpc.Notification) = - async { - match notificationHandlings |> Map.tryFind notification.Method with - | Some handling -> - try - match notification.Params with - | None -> return Result.Error(JsonRpc.Error.InvalidParams) - | Some prms -> - let! result = handling.Run prms - return Result.Ok() - with - | ex -> return Result.Error(JsonRpc.Error.Create(JsonRpc.ErrorCodes.internalError, ex.ToString())) - | None -> return Result.Error(JsonRpc.Error.MethodNotFound) - } - - let messageHandler str = - let messageTypeTest = JsonConvert.DeserializeObject(str, jsonSettings) - - match getMessageType messageTypeTest with - | MessageType.Notification -> - let notification = JsonConvert.DeserializeObject(str, jsonSettings) - - async { - let! result = handleNotification notification - - match result with - | Result.Ok _ -> () - | Result.Error error -> - logger.error ( - Log.setMessage "HandleServerMessage - Error {error} when handling notification {notification}" - >> Log.addContextDestructured "error" error - >> Log.addContextDestructured "notification" notification - ) - //TODO: Handle error on receiving notification, send message to user? - () - } - |> Async.StartAsTask - |> ignore - | MessageType.Request -> - let request = JsonConvert.DeserializeObject(str, jsonSettings) - - async { - let! result = handleRequest request - - match result with - | Some response -> - let responseString = JsonConvert.SerializeObject(response, jsonSettings) - sender.Post(responseString) - | None -> () - } - |> Async.StartAsTask - |> ignore - | MessageType.Response - | MessageType.Error -> - logger.error ( - Log.setMessage "HandleServerMessage - Message had invalid jsonrpc version: {messageTypeTest}" - >> Log.addContextDestructured "messageTypeTest" messageTypeTest - ) - - () - - let request = JsonConvert.DeserializeObject(str, jsonSettings) - - async { - let! result = handleRequest request - - match result with - | Some response -> - let responseString = JsonConvert.SerializeObject(response, jsonSettings) - sender.Post(responseString) - | None -> () - } - |> Async.StartAsTask - |> ignore - - member __.SendNotification (rpcMethod: string) (requestObj: obj) = - let serializedResponse = JToken.FromObject(requestObj, jsonSerializer) - let notification = JsonRpc.Notification.Create(rpcMethod, Some serializedResponse) - let notString = JsonConvert.SerializeObject(notification, jsonSettings) - sender.Post(notString) - - member __.Start() = - async { - let si = ProcessStartInfo() - si.RedirectStandardOutput <- true - si.RedirectStandardInput <- true - si.RedirectStandardError <- true - si.UseShellExecute <- false - si.WorkingDirectory <- Environment.CurrentDirectory - si.FileName <- exec - si.Arguments <- args - - let proc = - try - Process.Start(si) - with - | ex -> - let newEx = System.Exception(sprintf "%s on %s" ex.Message exec, ex) - raise newEx + let serverJson = createJsonFormatter () + let serverInstance = serverFactory (createJsonFormatter().JsonSerializer) - inputStream <- Some(proc.StandardInput) - outuptStream <- Some(proc.StandardOutput) + let serverRpcHandler = new HeaderDelimitedMessageHandler(serverOutput, serverInput, serverJson) + let serverRpc = new JsonRpc(serverRpcHandler) + serverRpc.AddLocalRpcTarget(serverInstance, commonOptions) - let mutable quit = false - let outStream = proc.StandardOutput.BaseStream + // for the client side we invert the source/target streams + let clientJson = createJsonFormatter () + let clientRpcHandler = new HeaderDelimitedMessageHandler(serverInput, serverOutput, clientJson) + let clientRpc = new JsonRpc(clientRpcHandler) + clientRpc.AddLocalRpcTarget(serverInstance, commonOptions) - while not quit do - try - let _, notificationString = LowLevel.read outStream - // fprintfn stderr "[CLIENT] READING: %s" notificationString - messageHandler notificationString - with - | :? EndOfStreamException -> quit <- true - | ex -> () + serverRpc, clientRpc - return () - } - |> Async.Start \ No newline at end of file + let start (serverRpc: JsonRpc) = + serverRpc.StartListening() + serverRpc.Completion \ No newline at end of file diff --git a/src/Server.fs b/src/Server.fs index a5f458c..d5c2c98 100644 --- a/src/Server.fs +++ b/src/Server.fs @@ -2,14 +2,9 @@ namespace Ionide.LanguageServerProtocol open Ionide.LanguageServerProtocol.Types -module private ServerUtil = - /// Return the JSON-RPC "not implemented" error - let notImplemented<'t> = async.Return LspResult.notImplemented<'t> - - /// Do nothing and ignore the notification - let ignoreNotification = async.Return(()) - -open ServerUtil +open System +open System.Threading +open System.Threading.Tasks [] type ILspServer = @@ -17,27 +12,26 @@ type ILspServer = /// The initialize request is sent as the first request from the client to the server. /// The initialize request may only be sent once. - abstract member Initialize: InitializeParams -> AsyncLspResult + abstract member initialize: InitializeParams -> Task /// The initialized notification is sent from the client to the server after the client received the result /// of the initialize request but before the client is sending any other request or notification to the server. /// The server can use the initialized notification for example to dynamically register capabilities. /// The initialized notification may only be sent once. - abstract member Initialized: InitializedParams -> Async + abstract member initialized: EventHandler /// The shutdown request is sent from the client to the server. It asks the server to shut down, but to not /// exit (otherwise the response might not be delivered correctly to the client). There is a separate exit /// notification that asks the server to exit. - abstract member Shutdown: unit -> Async - + abstract member shutdown: unit -> Task /// A notification to ask the server to exit its process. - abstract member Exit: unit -> Async + abstract member exit: unit -> Task /// The hover request is sent from the client to the server to request hover information at a given text /// document position. - abstract member TextDocumentHover: TextDocumentPositionParams -> AsyncLspResult - + abstract member ``textDocument/hover``: + TextDocumentPositionParams * IProgress * CancellationToken -> Task /// The document open notification is sent from the client to the server to signal newly opened text /// documents. @@ -47,51 +41,49 @@ type ILspServer = /// necessarily mean that its content is presented in an editor. An open notification must not be sent /// more than once without a corresponding close notification send before. This means open and close /// notification must be balanced and the max open count for a particular textDocument is one. - abstract member TextDocumentDidOpen: DidOpenTextDocumentParams -> Async - + abstract member ``textDocument/didOpen``: EventHandler /// The document change notification is sent from the client to the server to signal changes to a text document. - abstract member TextDocumentDidChange: DidChangeTextDocumentParams -> Async + abstract member ``textDocument/didChange``: EventHandler /// The Completion request is sent from the client to the server to compute completion items at a given /// cursor position. Completion items are presented in the IntelliSense user interface. /// /// If computing full completion items is expensive, servers can additionally provide a handler for the - /// completion item resolve request (‘completionItem/resolve’). This request is sent when a completion - /// item is selected in the user interface. A typical use case is for example: the ‘textDocument/completion’ + /// completion item resolve request (`completionItem/resolve`). This request is sent when a completion + /// item is selected in the user interface. A typical use case is for example: the `textDocument/completion` /// request doesn’t fill in the documentation property for returned completion items since it is expensive - /// to compute. When the item is selected in the user interface then a ‘completionItem/resolve’ request is + /// to compute. When the item is selected in the user interface then a `completionItem/resolve` request is /// sent with the selected completion item as a param. The returned completion item should have the /// documentation property filled in. The request can delay the computation of the detail and documentation /// properties. However, properties that are needed for the initial sorting and filtering, like sortText, /// filterText, insertText, and textEdit must be provided in the textDocument/completion request and must /// not be changed during resolve. - abstract member TextDocumentCompletion: CompletionParams -> AsyncLspResult - + abstract member ``textDocument/completion``: + CompletionParams * IProgress * CancellationToken -> Task /// The request is sent from the client to the server to resolve additional information for a given /// completion item. - abstract member CompletionItemResolve: CompletionItem -> AsyncLspResult - + abstract member ``completionItem/resolve``: CompletionItem -> Task /// The rename request is sent from the client to the server to perform a workspace-wide rename of a symbol. - abstract member TextDocumentRename: RenameParams -> AsyncLspResult + abstract member ``textDocument/rename``: + RenameParams * IProgress * CancellationToken -> Task /// The prepare rename request is sent from the client to the server to setup and test the validity of a rename operation at a given location. /// If None is returned then it is deemed that a ‘textDocument/rename’ request is not valid at the given position. - abstract member TextDocumentPrepareRename: PrepareRenameParams -> AsyncLspResult - - + abstract member ``textDocument/prepareRename``: + PrepareRenameParams * IProgress * CancellationToken -> Task /// The goto definition request is sent from the client to the server to resolve the definition location of /// a symbol at a given text document position. - abstract member TextDocumentDefinition: TextDocumentPositionParams -> AsyncLspResult - + abstract member ``textDocument/definition``: + TextDocumentPositionParams * IProgress * CancellationToken -> Task /// The references request is sent from the client to the server to resolve project-wide references for /// the symbol denoted by the given text document position. - abstract member TextDocumentReferences: ReferenceParams -> AsyncLspResult - + abstract member ``textDocument/references``: + ReferenceParams * IProgress * CancellationToken -> Task /// The document highlight request is sent from the client to the server to resolve a document highlights /// for a given text document position. For programming languages this usually highlights all references @@ -100,538 +92,187 @@ type ILspServer = /// However we kept `textDocument/documentHighlight` and `textDocument/references` separate requests since /// the first one is allowed to be more fuzzy. Symbol matches usually have a DocumentHighlightKind of Read /// or Write whereas fuzzy or textual matches use Text as the kind. - abstract member TextDocumentDocumentHighlight: - TextDocumentPositionParams -> AsyncLspResult - + abstract member ``textDocument/documentHighlight``: + TextDocumentPositionParams * IProgress * CancellationToken -> + Task /// The document links request is sent from the client to the server to request the location of links /// in a document. - abstract member TextDocumentDocumentLink: DocumentLinkParams -> AsyncLspResult - + abstract member ``textDocument/documentLink``: + DocumentLinkParams * IProgress * CancellationToken -> Task /// The goto type definition request is sent from the client to the server to resolve the type definition /// location of a symbol at a given text document position. - abstract member TextDocumentTypeDefinition: TextDocumentPositionParams -> AsyncLspResult - + abstract member ``textDocument/typeDefinition``: + TextDocumentPositionParams * IProgress * CancellationToken -> Task /// The goto implementation request is sent from the client to the server to resolve the implementation /// location of a symbol at a given text document position. - abstract member TextDocumentImplementation: TextDocumentPositionParams -> AsyncLspResult - + abstract member ``textDocument/implementation``: + TextDocumentPositionParams * IProgress * CancellationToken -> Task /// The code action request is sent from the client to the server to compute commands for a given text /// document and range. These commands are typically code fixes to either fix problems or to /// beautify/refactor code. The result of a textDocument/codeAction request is an array of Command literals /// which are typically presented in the user interface. When the command is selected the server should be /// contacted again (via the workspace/executeCommand) request to execute the command. - abstract member TextDocumentCodeAction: CodeActionParams -> AsyncLspResult - + abstract member ``textDocument/codeAction``: + CodeActionParams * IProgress * CancellationToken -> + Task /// The code action request is sent from the client to the server to compute commands for a given text /// document and range. These commands are typically code fixes to either fix problems or to /// beautify/refactor code. The result of a textDocument/codeAction request is an array of Command literals /// which are typically presented in the user interface. When the command is selected the server should be /// contacted again (via the workspace/executeCommand) request to execute the command. - abstract member CodeActionResolve: CodeAction -> AsyncLspResult - + abstract member ``codeAction/resolve``: CodeAction * CancellationToken -> Task /// The code lens request is sent from the client to the server to compute code lenses for a given /// text document. - abstract member TextDocumentCodeLens: CodeLensParams -> AsyncLspResult - + abstract member ``textDocument/codeLens``: + CodeLensParams * IProgress * CancellationToken -> Task /// The code lens resolve request is sent from the client to the server to resolve the command for /// a given code lens item. - abstract member CodeLensResolve: CodeLens -> AsyncLspResult - + abstract member ``codeLens/resolve``: CodeLens * CancellationToken -> Task /// The signature help request is sent from the client to the server to request signature information at /// a given cursor position. - abstract member TextDocumentSignatureHelp: SignatureHelpParams -> AsyncLspResult - + abstract member ``textDocument/signatureHelp``: SignatureHelpParams * CancellationToken -> Task /// The document link resolve request is sent from the client to the server to resolve the target of /// a given document link. - abstract member DocumentLinkResolve: DocumentLink -> AsyncLspResult - + abstract member ``documentLink/resolve``: DocumentLink * CancellationToken -> Task /// The document color request is sent from the client to the server to list all color references /// found in a given text document. Along with the range, a color value in RGB is returned. - abstract member TextDocumentDocumentColor: DocumentColorParams -> AsyncLspResult + abstract member ``textDocument/documentColor``: + DocumentColorParams * IProgress * CancellationToken -> Task /// The color presentation request is sent from the client to the server to obtain a list of /// presentations for a color value at a given location. Clients can use the result to - abstract member TextDocumentColorPresentation: ColorPresentationParams -> AsyncLspResult - + abstract member ``textDocument/colorPresentation``: + ColorPresentationParams * IProgress * CancellationToken -> Task /// The document formatting request is sent from the client to the server to format a whole document. - abstract member TextDocumentFormatting: DocumentFormattingParams -> AsyncLspResult + abstract member ``textDocument/formatting``: + DocumentFormattingParams * IProgress * CancellationToken -> Task /// The document range formatting request is sent from the client to the server to format a given /// range in a document. - abstract member TextDocumentRangeFormatting: DocumentRangeFormattingParams -> AsyncLspResult - + abstract member ``textDocument/rangeFormatting``: + DocumentRangeFormattingParams * IProgress * CancellationToken -> Task /// The document on type formatting request is sent from the client to the server to format parts /// of the document during typing. - abstract member TextDocumentOnTypeFormatting: DocumentOnTypeFormattingParams -> AsyncLspResult - + abstract member ``textDocument/onTypeFormatting``: + DocumentOnTypeFormattingParams * CancellationToken -> Task /// The document symbol request is sent from the client to the server to return a flat list of all symbols /// found in a given text document. Neither the symbol’s location range nor the symbol’s container name /// should be used to infer a hierarchy. - abstract member TextDocumentDocumentSymbol: - DocumentSymbolParams -> AsyncLspResult option> - + abstract member ``textDocument/documentSymbol``: + DocumentSymbolParams * IProgress> * CancellationToken -> + Task option> /// The watched files notification is sent from the client to the server when the client detects changes /// to files watched by the language client. It is recommended that servers register for these file /// events using the registration mechanism. In former implementations clients pushed file events without /// the server actively asking for it. - abstract member WorkspaceDidChangeWatchedFiles: DidChangeWatchedFilesParams -> Async - + abstract member ``workspace/didChangeWatchedFiles``: EventHandler /// The `workspace/didChangeWorkspaceFolders` notification is sent from the client to the server to inform /// the server about workspace folder configuration changes. The notification is sent by default if both /// *ServerCapabilities/workspace/workspaceFolders* and *ClientCapabilities/workapce/workspaceFolders* are /// true; or if the server has registered to receive this notification it first. - abstract member WorkspaceDidChangeWorkspaceFolders: DidChangeWorkspaceFoldersParams -> Async - + abstract member ``workspace/didChangeWorkspaceFolders``: EventHandler /// A notification sent from the client to the server to signal the change of configuration settings. - abstract member WorkspaceDidChangeConfiguration: DidChangeConfigurationParams -> Async + abstract member ``workspace/didChangeConfiguration``: EventHandler /// The will create files request is sent from the client to the server before files are actually created /// as long as the creation is triggered from within the client either by a user action or by applying a /// workspace edit - abstract member WorkspaceWillCreateFiles: CreateFilesParams -> AsyncLspResult - + abstract member ``workspace/willCreateFiles``: CreateFilesParams * CancellationToken -> Task /// The did create files notification is sent from the client to the server when files were created /// from within the client. - abstract member WorkspaceDidCreateFiles: CreateFilesParams -> Async - + abstract member ``workspace/didCreateFiles``: EventHandler /// The will rename files request is sent from the client to the server before files are actually renamed /// as long as the rename is triggered from within the client either by a user action or by applying a /// workspace edit. - abstract member WorkspaceWillRenameFiles: RenameFilesParams -> AsyncLspResult - + abstract member ``workspace/willRenameFiles``: RenameFilesParams * CancellationToken -> Task /// The did rename files notification is sent from the client to the server when files were renamed from /// within the client. - abstract member WorkspaceDidRenameFiles: RenameFilesParams -> Async - + abstract member ``workspace/didRenameFiles``: EventHandler /// The will delete files request is sent from the client to the server before files are actually deleted /// as long as the deletion is triggered from within the client either by a user action or by applying a /// workspace edit. - abstract member WorkspaceWillDeleteFiles: DeleteFilesParams -> AsyncLspResult - + abstract member ``workspace/willDeleteFiles``: DeleteFilesParams * CancellationToken -> Task /// The did delete files notification is sent from the client to the server when files were deleted from /// within the client. - abstract member WorkspaceDidDeleteFiles: DeleteFilesParams -> Async - + abstract member ``workspace/didDeleteFiles``: EventHandler /// The workspace symbol request is sent from the client to the server to list project-wide symbols matching /// the query string. - abstract member WorkspaceSymbol: WorkspaceSymbolParams -> AsyncLspResult - - + abstract member ``workspace/symbol``: + WorkspaceSymbolParams * IProgress * CancellationToken -> + Task /// The `workspace/executeCommand` request is sent from the client to the server to trigger command execution /// on the server. In most cases the server creates a `WorkspaceEdit` structure and applies the changes to the /// workspace using the request `workspace/applyEdit` which is sent from the server to the client. - abstract member WorkspaceExecuteCommand: ExecuteCommandParams -> AsyncLspResult - + abstract member ``workspace/executeCommand``: + ExecuteCommandParams * CancellationToken -> Task /// The document will save notification is sent from the client to the server before the document is /// actually saved. - abstract member TextDocumentWillSave: WillSaveTextDocumentParams -> Async - + abstract member ``textDocument/willSave``: WillSaveTextDocumentParams * CancellationToken -> Task /// The document will save request is sent from the client to the server before the document is actually saved. /// The request can return an array of TextEdits which will be applied to the text document before it is saved. /// Please note that clients might drop results if computing the text edits took too long or if a server /// constantly fails on this request. This is done to keep the save fast and reliable. - abstract member TextDocumentWillSaveWaitUntil: WillSaveTextDocumentParams -> AsyncLspResult - + abstract member ``textDocument/willSaveWaitUntil``: + WillSaveTextDocumentParams * CancellationToken -> Task /// The document save notification is sent from the client to the server when the document was saved /// in the client. - abstract member TextDocumentDidSave: DidSaveTextDocumentParams -> Async - + abstract member ``textDocument/didSave``: EventHandler /// The document close notification is sent from the client to the server when the document got closed in the /// client. The document’s truth now exists where the document’s uri points to (e.g. if the document’s uri is /// a file uri the truth now exists on disk). As with the open notification the close notification is about /// managing the document’s content. Receiving a close notification doesn't mean that the document was open in /// an editor before. A close notification requires a previous open notification to be sent. - abstract member TextDocumentDidClose: DidCloseTextDocumentParams -> Async - + abstract member ``textDocument/didClose``: EventHandler /// The folding range request is sent from the client to the server to return all folding ranges found in a given text document. - abstract member TextDocumentFoldingRange: FoldingRangeParams -> AsyncLspResult + abstract member ``textDocument/foldingRange``: + FoldingRangeParams * IProgress * CancellationToken -> Task /// The selection range request is sent from the client to the server to return suggested selection ranges at an array of given positions. /// A selection range is a range around the cursor position which the user might be interested in selecting. - abstract member TextDocumentSelectionRange: SelectionRangeParams -> AsyncLspResult + abstract member ``textDocument/selectionRange``: + SelectionRangeParams * IProgress * CancellationToken -> Task - abstract member TextDocumentSemanticTokensFull: SemanticTokensParams -> AsyncLspResult + abstract member ``textDocument/semanticTokensFull``: + SemanticTokensParams * IProgress * CancellationToken -> Task - abstract member TextDocumentSemanticTokensFullDelta: - SemanticTokensDeltaParams -> AsyncLspResult option> + abstract member ``textDocument/semanticTokensFullDelta``: + SemanticTokensDeltaParams * CancellationToken -> Task option> - abstract member TextDocumentSemanticTokensRange: SemanticTokensRangeParams -> AsyncLspResult + abstract member ``textDocument/semanticTokensRange``: + SemanticTokensRangeParams * IProgress * CancellationToken -> + Task /// The inlay hints request is sent from the client to the server to compute inlay hints for a given [text document, range] tuple /// that may be rendered in the editor in place with other text. - abstract member TextDocumentInlayHint: InlayHintParams -> AsyncLspResult - - /// The request is sent from the client to the server to resolve additional information for a given inlay hint. - /// This is usually used to compute the `tooltip`, `location` or `command` properties of a inlay hint’s label part - /// to avoid its unnecessary computation during the `textDocument/inlayHint` request. - /// - /// Consider the clients announces the `label.location` property as a property that can be resolved lazy using the client capability - /// ```typescript - /// textDocument.inlayHint.resolveSupport = { properties: ['label.location'] }; - /// ``` - /// then an inlay hint with a label part without a location needs to be resolved using the `inlayHint/resolve` request before it can be used. - abstract member InlayHintResolve: InlayHint -> AsyncLspResult - -[] -type LspServer() = - abstract member Dispose: unit -> unit - - /// The initialize request is sent as the first request from the client to the server. - /// The initialize request may only be sent once. - abstract member Initialize: InitializeParams -> AsyncLspResult - - default __.Initialize(_) = notImplemented - - /// The initialized notification is sent from the client to the server after the client received the result - /// of the initialize request but before the client is sending any other request or notification to the server. - /// The server can use the initialized notification for example to dynamically register capabilities. - /// The initialized notification may only be sent once. - abstract member Initialized: InitializedParams -> Async - - default __.Initialized(_) = ignoreNotification - - /// The shutdown request is sent from the client to the server. It asks the server to shut down, but to not - /// exit (otherwise the response might not be delivered correctly to the client). There is a separate exit - /// notification that asks the server to exit. - abstract member Shutdown: unit -> Async - - default __.Shutdown() = ignoreNotification - - /// A notification to ask the server to exit its process. - abstract member Exit: unit -> Async - default __.Exit() = ignoreNotification - - /// The hover request is sent from the client to the server to request hover information at a given text - /// document position. - abstract member TextDocumentHover: TextDocumentPositionParams -> AsyncLspResult - - default __.TextDocumentHover(_) = notImplemented - - /// The document open notification is sent from the client to the server to signal newly opened text - /// documents. - /// - /// The document’s truth is now managed by the client and the server must not try to read the document’s - /// truth using the document’s uri. Open in this sense means it is managed by the client. It doesn't - /// necessarily mean that its content is presented in an editor. An open notification must not be sent - /// more than once without a corresponding close notification send before. This means open and close - /// notification must be balanced and the max open count for a particular textDocument is one. - abstract member TextDocumentDidOpen: DidOpenTextDocumentParams -> Async - - default __.TextDocumentDidOpen(_) = ignoreNotification - - /// The document change notification is sent from the client to the server to signal changes to a text document. - abstract member TextDocumentDidChange: DidChangeTextDocumentParams -> Async - default __.TextDocumentDidChange(_) = ignoreNotification - - /// The Completion request is sent from the client to the server to compute completion items at a given - /// cursor position. Completion items are presented in the IntelliSense user interface. - /// - /// If computing full completion items is expensive, servers can additionally provide a handler for the - /// completion item resolve request (‘completionItem/resolve’). This request is sent when a completion - /// item is selected in the user interface. A typical use case is for example: the ‘textDocument/completion’ - /// request doesn’t fill in the documentation property for returned completion items since it is expensive - /// to compute. When the item is selected in the user interface then a ‘completionItem/resolve’ request is - /// sent with the selected completion item as a param. The returned completion item should have the - /// documentation property filled in. The request can delay the computation of the detail and documentation - /// properties. However, properties that are needed for the initial sorting and filtering, like sortText, - /// filterText, insertText, and textEdit must be provided in the textDocument/completion request and must - /// not be changed during resolve. - abstract member TextDocumentCompletion: CompletionParams -> AsyncLspResult - - default __.TextDocumentCompletion(_) = notImplemented - - /// The request is sent from the client to the server to resolve additional information for a given - /// completion item. - abstract member CompletionItemResolve: CompletionItem -> AsyncLspResult - - default __.CompletionItemResolve(_) = notImplemented - - /// The rename request is sent from the client to the server to perform a workspace-wide rename of a symbol. - abstract member TextDocumentRename: RenameParams -> AsyncLspResult - default __.TextDocumentRename(_) = notImplemented - - /// The prepare rename request is sent from the client to the server to setup and test the validity of a rename operation at a given location. - /// If None is returned then it is deemed that a ‘textDocument/rename’ request is not valid at the given position. - abstract member TextDocumentPrepareRename: PrepareRenameParams -> AsyncLspResult - - default __.TextDocumentPrepareRename(_) = - AsyncLspResult.success (Some(PrepareRenameResult.Default { DefaultBehavior = true })) - - /// The goto definition request is sent from the client to the server to resolve the definition location of - /// a symbol at a given text document position. - abstract member TextDocumentDefinition: TextDocumentPositionParams -> AsyncLspResult - - default __.TextDocumentDefinition(_) = notImplemented - - /// The references request is sent from the client to the server to resolve project-wide references for - /// the symbol denoted by the given text document position. - abstract member TextDocumentReferences: ReferenceParams -> AsyncLspResult - - default __.TextDocumentReferences(_) = notImplemented - - /// The document highlight request is sent from the client to the server to resolve a document highlights - /// for a given text document position. For programming languages this usually highlights all references - /// to the symbol scoped to this file. - /// - /// However we kept `textDocument/documentHighlight` and `textDocument/references` separate requests since - /// the first one is allowed to be more fuzzy. Symbol matches usually have a DocumentHighlightKind of Read - /// or Write whereas fuzzy or textual matches use Text as the kind. - abstract member TextDocumentDocumentHighlight: - TextDocumentPositionParams -> AsyncLspResult - - default __.TextDocumentDocumentHighlight(_) = notImplemented - - /// The document links request is sent from the client to the server to request the location of links - /// in a document. - abstract member TextDocumentDocumentLink: DocumentLinkParams -> AsyncLspResult - - default __.TextDocumentDocumentLink(_) = notImplemented - - /// The goto type definition request is sent from the client to the server to resolve the type definition - /// location of a symbol at a given text document position. - abstract member TextDocumentTypeDefinition: TextDocumentPositionParams -> AsyncLspResult - - default __.TextDocumentTypeDefinition(_) = notImplemented - - /// The goto implementation request is sent from the client to the server to resolve the implementation - /// location of a symbol at a given text document position. - abstract member TextDocumentImplementation: TextDocumentPositionParams -> AsyncLspResult - - default __.TextDocumentImplementation(_) = notImplemented - - /// The code action request is sent from the client to the server to compute commands for a given text - /// document and range. These commands are typically code fixes to either fix problems or to - /// beautify/refactor code. The result of a textDocument/codeAction request is an array of Command literals - /// which are typically presented in the user interface. When the command is selected the server should be - /// contacted again (via the workspace/executeCommand) request to execute the command. - abstract member TextDocumentCodeAction: CodeActionParams -> AsyncLspResult - - default __.TextDocumentCodeAction(_) = notImplemented - - /// The code action request is sent from the client to the server to compute commands for a given text - /// document and range. These commands are typically code fixes to either fix problems or to - /// beautify/refactor code. The result of a textDocument/codeAction request is an array of Command literals - /// which are typically presented in the user interface. When the command is selected the server should be - /// contacted again (via the workspace/executeCommand) request to execute the command. - abstract member CodeActionResolve: CodeAction -> AsyncLspResult - - default __.CodeActionResolve(_) = notImplemented - - /// The code lens request is sent from the client to the server to compute code lenses for a given - /// text document. - abstract member TextDocumentCodeLens: CodeLensParams -> AsyncLspResult - - default __.TextDocumentCodeLens(_) = notImplemented - - /// The code lens resolve request is sent from the client to the server to resolve the command for - /// a given code lens item. - abstract member CodeLensResolve: CodeLens -> AsyncLspResult - - default __.CodeLensResolve(_) = notImplemented - - /// The signature help request is sent from the client to the server to request signature information at - /// a given cursor position. - abstract member TextDocumentSignatureHelp: SignatureHelpParams -> AsyncLspResult - - default __.TextDocumentSignatureHelp(_) = notImplemented - - /// The document link resolve request is sent from the client to the server to resolve the target of - /// a given document link. - abstract member DocumentLinkResolve: DocumentLink -> AsyncLspResult - - default __.DocumentLinkResolve(_) = notImplemented - - /// The document color request is sent from the client to the server to list all color references - /// found in a given text document. Along with the range, a color value in RGB is returned. - abstract member TextDocumentDocumentColor: DocumentColorParams -> AsyncLspResult - - default __.TextDocumentDocumentColor(_) = notImplemented - - /// The color presentation request is sent from the client to the server to obtain a list of - /// presentations for a color value at a given location. Clients can use the result to - abstract member TextDocumentColorPresentation: ColorPresentationParams -> AsyncLspResult - - default __.TextDocumentColorPresentation(_) = notImplemented - - /// The document formatting request is sent from the client to the server to format a whole document. - abstract member TextDocumentFormatting: DocumentFormattingParams -> AsyncLspResult - default __.TextDocumentFormatting(_) = notImplemented - - /// The document range formatting request is sent from the client to the server to format a given - /// range in a document. - abstract member TextDocumentRangeFormatting: DocumentRangeFormattingParams -> AsyncLspResult - - default __.TextDocumentRangeFormatting(_) = notImplemented - - /// The document on type formatting request is sent from the client to the server to format parts - /// of the document during typing. - abstract member TextDocumentOnTypeFormatting: DocumentOnTypeFormattingParams -> AsyncLspResult - - default __.TextDocumentOnTypeFormatting(_) = notImplemented - - /// The document symbol request is sent from the client to the server to return a flat list of all symbols - /// found in a given text document. Neither the symbol’s location range nor the symbol’s container name - /// should be used to infer a hierarchy. - abstract member TextDocumentDocumentSymbol: - DocumentSymbolParams -> AsyncLspResult option> - - default __.TextDocumentDocumentSymbol(_) = notImplemented - - /// The watched files notification is sent from the client to the server when the client detects changes - /// to files watched by the language client. It is recommended that servers register for these file - /// events using the registration mechanism. In former implementations clients pushed file events without - /// the server actively asking for it. - abstract member WorkspaceDidChangeWatchedFiles: DidChangeWatchedFilesParams -> Async - - default __.WorkspaceDidChangeWatchedFiles(_) = ignoreNotification - - /// The `workspace/didChangeWorkspaceFolders` notification is sent from the client to the server to inform - /// the server about workspace folder configuration changes. The notification is sent by default if both - /// *ServerCapabilities/workspace/workspaceFolders* and *ClientCapabilities/workapce/workspaceFolders* are - /// true; or if the server has registered to receive this notification it first. - abstract member WorkspaceDidChangeWorkspaceFolders: DidChangeWorkspaceFoldersParams -> Async - - default __.WorkspaceDidChangeWorkspaceFolders(_) = ignoreNotification - - /// A notification sent from the client to the server to signal the change of configuration settings. - abstract member WorkspaceDidChangeConfiguration: DidChangeConfigurationParams -> Async - default __.WorkspaceDidChangeConfiguration(_) = ignoreNotification - - /// The will create files request is sent from the client to the server before files are actually created - /// as long as the creation is triggered from within the client either by a user action or by applying a - /// workspace edit - abstract member WorkspaceWillCreateFiles: CreateFilesParams -> AsyncLspResult - - default __.WorkspaceWillCreateFiles(_) = notImplemented - - /// The did create files notification is sent from the client to the server when files were created - /// from within the client. - abstract member WorkspaceDidCreateFiles: CreateFilesParams -> Async - - default __.WorkspaceDidCreateFiles(_) = ignoreNotification - - /// The will rename files request is sent from the client to the server before files are actually renamed - /// as long as the rename is triggered from within the client either by a user action or by applying a - /// workspace edit. - abstract member WorkspaceWillRenameFiles: RenameFilesParams -> AsyncLspResult - - default __.WorkspaceWillRenameFiles(_) = notImplemented - - /// The did rename files notification is sent from the client to the server when files were renamed from - /// within the client. - abstract member WorkspaceDidRenameFiles: RenameFilesParams -> Async - - default __.WorkspaceDidRenameFiles(_) = ignoreNotification - - /// The will delete files request is sent from the client to the server before files are actually deleted - /// as long as the deletion is triggered from within the client either by a user action or by applying a - /// workspace edit. - abstract member WorkspaceWillDeleteFiles: DeleteFilesParams -> AsyncLspResult - - default __.WorkspaceWillDeleteFiles(_) = notImplemented - - /// The did delete files notification is sent from the client to the server when files were deleted from - /// within the client. - abstract member WorkspaceDidDeleteFiles: DeleteFilesParams -> Async - - default __.WorkspaceDidDeleteFiles(_) = ignoreNotification - - /// The workspace symbol request is sent from the client to the server to list project-wide symbols matching - /// the query string. - abstract member WorkspaceSymbol: WorkspaceSymbolParams -> AsyncLspResult - - default __.WorkspaceSymbol(_) = notImplemented - - /// The `workspace/executeCommand` request is sent from the client to the server to trigger command execution - /// on the server. In most cases the server creates a `WorkspaceEdit` structure and applies the changes to the - /// workspace using the request `workspace/applyEdit` which is sent from the server to the client. - abstract member WorkspaceExecuteCommand: ExecuteCommandParams -> AsyncLspResult - - default __.WorkspaceExecuteCommand(_) = notImplemented - - /// The document will save notification is sent from the client to the server before the document is - /// actually saved. - abstract member TextDocumentWillSave: WillSaveTextDocumentParams -> Async - - default __.TextDocumentWillSave(_) = ignoreNotification - - /// The document will save request is sent from the client to the server before the document is actually saved. - /// The request can return an array of TextEdits which will be applied to the text document before it is saved. - /// Please note that clients might drop results if computing the text edits took too long or if a server - /// constantly fails on this request. This is done to keep the save fast and reliable. - abstract member TextDocumentWillSaveWaitUntil: WillSaveTextDocumentParams -> AsyncLspResult - - default __.TextDocumentWillSaveWaitUntil(_) = notImplemented - - /// The document save notification is sent from the client to the server when the document was saved - /// in the client. - abstract member TextDocumentDidSave: DidSaveTextDocumentParams -> Async - - default __.TextDocumentDidSave(_) = ignoreNotification - - /// The document close notification is sent from the client to the server when the document got closed in the - /// client. The document’s truth now exists where the document’s uri points to (e.g. if the document’s uri is - /// a file uri the truth now exists on disk). As with the open notification the close notification is about - /// managing the document’s content. Receiving a close notification doesn't mean that the document was open in - /// an editor before. A close notification requires a previous open notification to be sent. - abstract member TextDocumentDidClose: DidCloseTextDocumentParams -> Async - - default __.TextDocumentDidClose(_) = ignoreNotification - - /// The folding range request is sent from the client to the server to return all folding ranges found in a given text document. - abstract member TextDocumentFoldingRange: FoldingRangeParams -> AsyncLspResult - default __.TextDocumentFoldingRange(_) = notImplemented - - /// The selection range request is sent from the client to the server to return suggested selection ranges at an array of given positions. - /// A selection range is a range around the cursor position which the user might be interested in selecting. - abstract member TextDocumentSelectionRange: SelectionRangeParams -> AsyncLspResult - - default __.TextDocumentSelectionRange(_) = notImplemented - - abstract member TextDocumentSemanticTokensFull: SemanticTokensParams -> AsyncLspResult - default __.TextDocumentSemanticTokensFull(_) = notImplemented - - abstract member TextDocumentSemanticTokensFullDelta: - SemanticTokensDeltaParams -> AsyncLspResult option> - - default __.TextDocumentSemanticTokensFullDelta(_) = notImplemented - - abstract member TextDocumentSemanticTokensRange: SemanticTokensRangeParams -> AsyncLspResult - default __.TextDocumentSemanticTokensRange(_) = notImplemented - - /// The inlay hints request is sent from the client to the server to compute inlay hints for a given [text document, range] tuple - /// that may be rendered in the editor in place with other text. - abstract member TextDocumentInlayHint: InlayHintParams -> AsyncLspResult - - default __.TextDocumentInlayHint(_) = notImplemented + abstract member ``textDocument/inlayHint``: InlayHintParams * CancellationToken -> Task /// The request is sent from the client to the server to resolve additional information for a given inlay hint. /// This is usually used to compute the `tooltip`, `location` or `command` properties of a inlay hint’s label part @@ -642,68 +283,4 @@ type LspServer() = /// textDocument.inlayHint.resolveSupport = { properties: ['label.location'] }; /// ``` /// then an inlay hint with a label part without a location needs to be resolved using the `inlayHint/resolve` request before it can be used. - abstract member InlayHintResolve: InlayHint -> AsyncLspResult - - default __.InlayHintResolve(_) = notImplemented - - interface ILspServer with - member this.Dispose() = this.Dispose() - member this.Initialize(p: InitializeParams) = this.Initialize(p) - member this.Initialized(p: InitializedParams) = this.Initialized(p) - member this.Shutdown() = this.Shutdown() - member this.Exit() = this.Exit() - member this.TextDocumentHover(p: TextDocumentPositionParams) = this.TextDocumentHover(p) - member this.TextDocumentDidOpen(p: DidOpenTextDocumentParams) = this.TextDocumentDidOpen(p) - member this.TextDocumentDidChange(p: DidChangeTextDocumentParams) = this.TextDocumentDidChange(p) - member this.TextDocumentCompletion(p: CompletionParams) = this.TextDocumentCompletion(p) - member this.CompletionItemResolve(p: CompletionItem) = this.CompletionItemResolve(p) - member this.TextDocumentRename(p: RenameParams) = this.TextDocumentRename(p) - member this.TextDocumentPrepareRename(p: PrepareRenameParams) = this.TextDocumentPrepareRename(p) - member this.TextDocumentDefinition(p: TextDocumentPositionParams) = this.TextDocumentDefinition(p) - member this.TextDocumentReferences(p: ReferenceParams) = this.TextDocumentReferences(p) - member this.TextDocumentDocumentHighlight(p: TextDocumentPositionParams) = this.TextDocumentDocumentHighlight(p) - member this.TextDocumentDocumentLink(p: DocumentLinkParams) = this.TextDocumentDocumentLink(p) - member this.TextDocumentTypeDefinition(p: TextDocumentPositionParams) = this.TextDocumentTypeDefinition(p) - member this.TextDocumentImplementation(p: TextDocumentPositionParams) = this.TextDocumentImplementation(p) - member this.TextDocumentCodeAction(p: CodeActionParams) = this.TextDocumentCodeAction(p) - member this.CodeActionResolve(p: CodeAction) = this.CodeActionResolve(p) - member this.TextDocumentCodeLens(p: CodeLensParams) = this.TextDocumentCodeLens(p) - member this.CodeLensResolve(p: CodeLens) = this.CodeLensResolve(p) - member this.TextDocumentSignatureHelp(p: SignatureHelpParams) = this.TextDocumentSignatureHelp(p) - member this.DocumentLinkResolve(p: DocumentLink) = this.DocumentLinkResolve(p) - member this.TextDocumentDocumentColor(p: DocumentColorParams) = this.TextDocumentDocumentColor(p) - member this.TextDocumentColorPresentation(p: ColorPresentationParams) = this.TextDocumentColorPresentation(p) - member this.TextDocumentFormatting(p: DocumentFormattingParams) = this.TextDocumentFormatting(p) - member this.TextDocumentRangeFormatting(p: DocumentRangeFormattingParams) = this.TextDocumentRangeFormatting(p) - member this.TextDocumentOnTypeFormatting(p: DocumentOnTypeFormattingParams) = this.TextDocumentOnTypeFormatting(p) - member this.TextDocumentDocumentSymbol(p: DocumentSymbolParams) = this.TextDocumentDocumentSymbol(p) - member this.WorkspaceDidChangeWatchedFiles(p: DidChangeWatchedFilesParams) = this.WorkspaceDidChangeWatchedFiles(p) - - member this.WorkspaceDidChangeWorkspaceFolders(p: DidChangeWorkspaceFoldersParams) = - this.WorkspaceDidChangeWorkspaceFolders(p) - - member this.WorkspaceDidChangeConfiguration(p: DidChangeConfigurationParams) = - this.WorkspaceDidChangeConfiguration(p) - - member this.WorkspaceWillCreateFiles(p: CreateFilesParams) = this.WorkspaceWillCreateFiles(p) - member this.WorkspaceDidCreateFiles(p: CreateFilesParams) = this.WorkspaceDidCreateFiles(p) - member this.WorkspaceWillRenameFiles(p: RenameFilesParams) = this.WorkspaceWillRenameFiles(p) - member this.WorkspaceDidRenameFiles(p: RenameFilesParams) = this.WorkspaceDidRenameFiles(p) - member this.WorkspaceWillDeleteFiles(p: DeleteFilesParams) = this.WorkspaceWillDeleteFiles(p) - member this.WorkspaceDidDeleteFiles(p: DeleteFilesParams) = this.WorkspaceDidDeleteFiles(p) - member this.WorkspaceSymbol(p: WorkspaceSymbolParams) = this.WorkspaceSymbol(p) - member this.WorkspaceExecuteCommand(p: ExecuteCommandParams) = this.WorkspaceExecuteCommand(p) - member this.TextDocumentWillSave(p: WillSaveTextDocumentParams) = this.TextDocumentWillSave(p) - member this.TextDocumentWillSaveWaitUntil(p: WillSaveTextDocumentParams) = this.TextDocumentWillSaveWaitUntil(p) - member this.TextDocumentDidSave(p: DidSaveTextDocumentParams) = this.TextDocumentDidSave(p) - member this.TextDocumentDidClose(p: DidCloseTextDocumentParams) = this.TextDocumentDidClose(p) - member this.TextDocumentFoldingRange(p: FoldingRangeParams) = this.TextDocumentFoldingRange(p) - member this.TextDocumentSelectionRange(p: SelectionRangeParams) = this.TextDocumentSelectionRange(p) - member this.TextDocumentSemanticTokensFull(p: SemanticTokensParams) = this.TextDocumentSemanticTokensFull(p) - - member this.TextDocumentSemanticTokensFullDelta(p: SemanticTokensDeltaParams) = - this.TextDocumentSemanticTokensFullDelta(p) - - member this.TextDocumentSemanticTokensRange(p: SemanticTokensRangeParams) = this.TextDocumentSemanticTokensRange(p) - member this.TextDocumentInlayHint(p: InlayHintParams) = this.TextDocumentInlayHint(p) - member this.InlayHintResolve(p: InlayHint) = this.InlayHintResolve(p) \ No newline at end of file + abstract member ``inlayHint/resolve``: InlayHint * CancellationToken -> Task \ No newline at end of file diff --git a/src/Types.fs b/src/Types.fs index bff70b5..ee654a7 100644 --- a/src/Types.fs +++ b/src/Types.fs @@ -15,39 +15,17 @@ type U2<'a, 'b> = | First of 'a | Second of 'b -type LspResult<'t> = Result<'t, JsonRpc.Error> -type AsyncLspResult<'t> = Async> - -module LspResult = - open Ionide.LanguageServerProtocol - - let success x : LspResult<_> = Result.Ok x - - let invalidParams s : LspResult<_> = Result.Error(JsonRpc.Error.Create(JsonRpc.ErrorCodes.invalidParams, s)) - - let internalError<'a> (s: string) : LspResult<'a> = - Result.Error(JsonRpc.Error.Create(JsonRpc.ErrorCodes.internalError, s)) - - let notImplemented<'a> : LspResult<'a> = Result.Error(JsonRpc.Error.MethodNotFound) - - let requestCancelled<'a> : LspResult<'a> = Result.Error(JsonRpc.Error.RequestCancelled) - -module AsyncLspResult = - open Ionide.LanguageServerProtocol - - let success x : AsyncLspResult<_> = async.Return(Result.Ok x) - - let invalidParams s : AsyncLspResult<_> = - async.Return(Result.Error(JsonRpc.Error.Create(JsonRpc.ErrorCodes.invalidParams, s))) - - let internalError s : AsyncLspResult<_> = - async.Return(Result.Error(JsonRpc.Error.Create(JsonRpc.ErrorCodes.internalError, s))) - - let notImplemented<'a> : AsyncLspResult<'a> = async.Return(Result.Error(JsonRpc.Error.MethodNotFound)) - /// The LSP any type type LSPAny = JToken +module LspError = + open StreamJsonRpc.Protocol + open StreamJsonRpc + + let invalidParams message = LocalRpcException(message, ErrorCode = int JsonRpcErrorCode.InvalidParams) + let internalError message = LocalRpcException(message, ErrorCode = int JsonRpcErrorCode.InternalError) + let notImplemented () = NotImplementedException() + type TextDocumentSyncKind = | None = 0 | Full = 1 @@ -333,16 +311,15 @@ type InlayHintWorkspaceClientCapabilities = /// change that requires such a calculation. RefreshSupport: bool option } -type CodeLensWorkspaceClientCapabilities = { - /// Whether the client implementation supports a refresh request sent from the - /// server to the client. - /// - /// Note that this event is global and will force the client to refresh all - /// code lenses currently shown. It should be used with absolute care and is - /// useful for situation where a server for example detect a project wide - /// change that requires such a calculation. - RefreshSupport: bool option -} +type CodeLensWorkspaceClientCapabilities = + { /// Whether the client implementation supports a refresh request sent from the + /// server to the client. + /// + /// Note that this event is global and will force the client to refresh all + /// code lenses currently shown. It should be used with absolute care and is + /// useful for situation where a server for example detect a project wide + /// change that requires such a calculation. + RefreshSupport: bool option } /// Workspace specific client capabilities. type WorkspaceClientCapabilities = @@ -832,7 +809,7 @@ type InitializeParams = RootPath: string option RootUri: string option InitializationOptions: JToken option - Capabilities: ClientCapabilities option + Capabilities: ClientCapabilities trace: string option /// The workspace folders configured in the client when the server starts. /// This property is only available if the client supports workspace folders. @@ -2117,6 +2094,8 @@ type SemanticTokens = ResultId: string option Data: uint32 [] } +type SemanticTokensPartialResult = { data: uint32 [] } + type SemanticTokensEdit = { /// The start offset of the edit. Start: uint32 @@ -2133,6 +2112,8 @@ type SemanticTokensDelta = /// The semantic token edits to transform a previous result into a new result. Edits: SemanticTokensEdit [] } +type SemanticTokensDeltaPartialResult = { Edits: SemanticTokensEdit [] } + /// Represents information on a file/folder create. ///@since 3.16.0 type FileCreate = diff --git a/tests/Benchmarks.fs b/tests/Benchmarks.fs index 894d057..bb5ded8 100644 --- a/tests/Benchmarks.fs +++ b/tests/Benchmarks.fs @@ -11,6 +11,9 @@ open System open System.Collections.Concurrent open System.Collections.Generic open BenchmarkDotNet.Order +open System.Text +open Newtonsoft.Json +open System.IO let inline private memorise (f: 'a -> 'b) : 'a -> 'b = let d = ConcurrentDictionary<'a, 'b>() @@ -514,15 +517,23 @@ type MultipleTypesBenchmarks() = /// Some complex data which covers all converters let example = Example.createData (1234, 9, 5) let option = {| Some = Some 123; None = (None: int option) |} + let formatter = jsonRpcFormatter () + + let serialize item = + let sb = new StringBuilder() + use writer = new JsonTextWriter(new StringWriter(sb)) + formatter.JsonSerializer.Serialize(writer, item) + JToken.Parse(sb.ToString()) let withCounts (counts) data = data |> Array.collect (fun data -> counts |> Array.map (fun count -> [| box count; box data |])) member _.AllLsp_Roundtrip() = + for o in allLsp do let json = inlayHint |> serialize - let res = json.ToObject(o.GetType(), jsonRpcFormatter.JsonSerializer) + let res = json.ToObject(o.GetType(), formatter.JsonSerializer) () [] @@ -534,7 +545,7 @@ type MultipleTypesBenchmarks() = member _.Example_Roundtrip() = let json = example |> serialize - let res = json.ToObject(example.GetType(), jsonRpcFormatter.JsonSerializer) + let res = json.ToObject(example.GetType(), formatter.JsonSerializer) () [] @@ -550,7 +561,7 @@ type MultipleTypesBenchmarks() = member _.Option_Roundtrips(count: int) = for _ in 1..count do let json = option |> serialize - let _ = json.ToObject(option.GetType(), jsonRpcFormatter.JsonSerializer) + let _ = json.ToObject(option.GetType(), formatter.JsonSerializer) () member _.SingleCaseUnion_ArgumentsSource() = @@ -562,7 +573,7 @@ type MultipleTypesBenchmarks() = member _.SingleCaseUnion_Roundtrips(count: int, data: Example.SingleCaseUnion) = for _ in 1..count do let json = data |> serialize - let _ = json.ToObject(typeof, jsonRpcFormatter.JsonSerializer) + let _ = json.ToObject(typeof, formatter.JsonSerializer) () member _.ErasedUnion_ArgumentsSource() = @@ -582,7 +593,7 @@ type MultipleTypesBenchmarks() = member _.ErasedUnion_Roundtrips(count: int, data: Example.ErasedUnionData) = for _ in 1..count do let json = data |> serialize - let _ = json.ToObject(typeof, jsonRpcFormatter.JsonSerializer) + let _ = json.ToObject(typeof, formatter.JsonSerializer) () diff --git a/tests/Shotgun.fs b/tests/Shotgun.fs index fbfe813..ab0fe99 100644 --- a/tests/Shotgun.fs +++ b/tests/Shotgun.fs @@ -14,6 +14,9 @@ open Newtonsoft.Json.Linq open Expecto open Ionide.LanguageServerProtocol.Server open Ionide.LanguageServerProtocol.Types +open System.Text +open Newtonsoft.Json +open System.IO // must be public type Gens = @@ -85,8 +88,22 @@ type Gens = let private fsCheckConfig = { FsCheckConfig.defaultConfig with arbitrary = [ typeof ] } -type private Roundtripper = - static member ThereAndBackAgain(input: 'a) = input |> serialize |> deserialize<'a> +type private Roundtripper() = + + static let formatter = jsonRpcFormatter () + + static member serialize item = + let sb = new StringBuilder() + use writer = new JsonTextWriter(new StringWriter(sb)) + formatter.JsonSerializer.Serialize(writer, item) + JToken.Parse(sb.ToString()) + + static member deserialize<'T>(json: JToken) = json.ToObject<'T>(formatter.JsonSerializer) + + static member ThereAndBackAgain(input: 'a) = + input + |> Roundtripper.serialize + |> Roundtripper.deserialize<'a> static member TestThereAndBackAgain(input: 'a) = let output = Roundtripper.ThereAndBackAgain input diff --git a/tests/Tests.fs b/tests/Tests.fs index a571dcc..1f958ea 100644 --- a/tests/Tests.fs +++ b/tests/Tests.fs @@ -10,6 +10,8 @@ open Newtonsoft.Json open Ionide.LanguageServerProtocol.JsonRpc open System.Collections.Generic open System.Runtime.Serialization +open System.Text +open System.IO type Record1 = { Name: string; Value: int } type Record2 = { Name: string; Position: int } @@ -88,6 +90,12 @@ type ExtensionDataField = if isNull x.AdditionalData then x.AdditionalData <- Map.empty +let private formatter = jsonRpcFormatter () + +let serialize item = JToken.FromObject(item, formatter.JsonSerializer) + +let deserialize<'T> (json: JToken) = json.ToObject<'T>(formatter.JsonSerializer) + let private serializationTests = testList "(de)serialization" @@ -674,71 +682,7 @@ let private serializationTests = testCase "can deserialize null Version in VersionedTextDocumentIdentifier" <| fun _ -> let textDoc = { VersionedTextDocumentIdentifier.Uri = "..."; Version = None } - testThereAndBackAgain textDoc - - testCase "serialize to name specified in JsonProperty in Response" - <| fun _ -> - let response: Response = { Version = "123"; Id = None; Error = None; Result = None } - let json = response |> serialize - // Version -> jsonrpc - Expect.isNone - (json |> tryGetProperty (nameof response.Version)) - "Version should exist, but instead as jsonrpc" - - Expect.isSome (json |> tryGetProperty "jsonrpc") "jsonrcp should exist because of Version" - // Id & Error optional -> not in json - Expect.isNone (json |> tryGetProperty (nameof response.Id)) "None Id shouldn't be in json" - Expect.isNone (json |> tryGetProperty (nameof response.Error)) "None Error shouldn't be in json" - // Result even when null/None - let prop = - json - |> tryGetProperty (nameof response.Result) - |> Flip.Expect.wantSome "Result should exist even when null/None" - - Expect.equal prop.Value.Type (JTokenType.Null) "Result should be null" - testCase "can (de)serialize empty response" - <| fun _ -> - let response: Response = { Version = "123"; Id = None; Error = None; Result = None } - testThereAndBackAgain response - testCase "can (de)serialize Response.Result" - <| fun _ -> - let response: Response = - { Version = "123" - Id = None - Error = None - Result = Some(JToken.Parse "\"some result\"") } - - testThereAndBackAgain response - testCase "can (de)serialize Result when Error is None" - <| fun _ -> - // Note: It's either `Error` or `Result`, but not both together - let response: Response = - { Version = "123" - Id = Some 42 - Error = None - Result = Some(JToken.Parse "\"some result\"") } - - testThereAndBackAgain response - testCase "can (de)serialize Error when error is Some" - <| fun _ -> - let response: Response = - { Version = "123" - Id = Some 42 - Error = Some { Code = 13; Message = "oh no"; Data = Some(JToken.Parse "\"some data\"") } - Result = None } - - testThereAndBackAgain response - testCase "doesn't serialize Result when Error is Some" - <| fun _ -> - let response: Response = - { Version = "123" - Id = Some 42 - Error = Some { Code = 13; Message = "oh no"; Data = Some(JToken.Parse "\"some data\"") } - Result = Some(JToken.Parse "\"some result\"") } - - let output = thereAndBackAgain response - Expect.isSome output.Error "Error should be serialized" - Expect.isNone output.Result "Result should not be serialized when Error is Some" ] + testThereAndBackAgain textDoc ] testList (nameof InlayHint) diff --git a/tests/Utils.fs b/tests/Utils.fs index ee58ec9..5b6d1fd 100644 --- a/tests/Utils.fs +++ b/tests/Utils.fs @@ -246,7 +246,7 @@ let tests = testCase "Client isn't lsp" <| fun _ -> let isLsp = - typeof + typeof |> isLspType [] Expect.isFalse isLsp "Client isn't lsp" ]