// SPDX-FileCopyrightText: 2019 The Go Language Server Authors // SPDX-License-Identifier: BSD-3-Clause package protocol import ( "bytes" "context" "fmt" "github.com/segmentio/encoding/json" "go.uber.org/zap" "go.lsp.dev/jsonrpc2" "go.lsp.dev/pkg/xcontext" ) // ClientDispatcher returns a Client that dispatches LSP requests across the // given jsonrpc2 connection. func ClientDispatcher(conn jsonrpc2.Conn, logger *zap.Logger) Client { return &client{ Conn: conn, logger: logger, } } // ClientHandler handler of LSP client. func ClientHandler(client Client, handler jsonrpc2.Handler) jsonrpc2.Handler { h := func(ctx context.Context, reply jsonrpc2.Replier, req jsonrpc2.Request) error { if ctx.Err() != nil { xctx := xcontext.Detach(ctx) return reply(xctx, nil, ErrRequestCancelled) } handled, err := clientDispatch(ctx, client, reply, req) if handled || err != nil { return err } return handler(ctx, reply, req) } return h } // clientDispatch implements jsonrpc2.Handler. //nolint:funlen,cyclop func clientDispatch(ctx context.Context, client Client, reply jsonrpc2.Replier, req jsonrpc2.Request) (handled bool, err error) { if ctx.Err() != nil { return true, reply(ctx, nil, ErrRequestCancelled) } dec := json.NewDecoder(bytes.NewReader(req.Params())) logger := LoggerFromContext(ctx) switch req.Method() { case MethodProgress: // notification defer logger.Debug(MethodProgress, zap.Error(err)) var params ProgressParams if err := dec.Decode(¶ms); err != nil { return true, replyParseError(ctx, reply, err) } err := client.Progress(ctx, ¶ms) return true, reply(ctx, nil, err) case MethodWorkDoneProgressCreate: // request defer logger.Debug(MethodWorkDoneProgressCreate, zap.Error(err)) var params WorkDoneProgressCreateParams if err := dec.Decode(¶ms); err != nil { return true, replyParseError(ctx, reply, err) } err := client.WorkDoneProgressCreate(ctx, ¶ms) return true, reply(ctx, nil, err) case MethodWindowLogMessage: // notification defer logger.Debug(MethodWindowLogMessage, zap.Error(err)) var params LogMessageParams if err := dec.Decode(¶ms); err != nil { return true, replyParseError(ctx, reply, err) } err := client.LogMessage(ctx, ¶ms) return true, reply(ctx, nil, err) case MethodTextDocumentPublishDiagnostics: // notification defer logger.Debug(MethodTextDocumentPublishDiagnostics, zap.Error(err)) var params PublishDiagnosticsParams if err := dec.Decode(¶ms); err != nil { return true, replyParseError(ctx, reply, err) } err := client.PublishDiagnostics(ctx, ¶ms) return true, reply(ctx, nil, err) case MethodWindowShowMessage: // notification defer logger.Debug(MethodWindowShowMessage, zap.Error(err)) var params ShowMessageParams if err := dec.Decode(¶ms); err != nil { return true, replyParseError(ctx, reply, err) } err := client.ShowMessage(ctx, ¶ms) return true, reply(ctx, nil, err) case MethodWindowShowMessageRequest: // request defer logger.Debug(MethodWindowShowMessageRequest, zap.Error(err)) var params ShowMessageRequestParams if err := dec.Decode(¶ms); err != nil { return true, replyParseError(ctx, reply, err) } resp, err := client.ShowMessageRequest(ctx, ¶ms) return true, reply(ctx, resp, err) case MethodTelemetryEvent: // notification defer logger.Debug(MethodTelemetryEvent, zap.Error(err)) var params interface{} if err := dec.Decode(¶ms); err != nil { return true, replyParseError(ctx, reply, err) } err := client.Telemetry(ctx, ¶ms) return true, reply(ctx, nil, err) case MethodClientRegisterCapability: // request defer logger.Debug(MethodClientRegisterCapability, zap.Error(err)) var params RegistrationParams if err := dec.Decode(¶ms); err != nil { return true, replyParseError(ctx, reply, err) } err := client.RegisterCapability(ctx, ¶ms) return true, reply(ctx, nil, err) case MethodClientUnregisterCapability: // request defer logger.Debug(MethodClientUnregisterCapability, zap.Error(err)) var params UnregistrationParams if err := dec.Decode(¶ms); err != nil { return true, replyParseError(ctx, reply, err) } err := client.UnregisterCapability(ctx, ¶ms) return true, reply(ctx, nil, err) case MethodWorkspaceApplyEdit: // request defer logger.Debug(MethodWorkspaceApplyEdit, zap.Error(err)) var params ApplyWorkspaceEditParams if err := dec.Decode(¶ms); err != nil { return true, replyParseError(ctx, reply, err) } resp, err := client.ApplyEdit(ctx, ¶ms) return true, reply(ctx, resp, err) case MethodWorkspaceConfiguration: // request defer logger.Debug(MethodWorkspaceConfiguration, zap.Error(err)) var params ConfigurationParams if err := dec.Decode(¶ms); err != nil { return true, replyParseError(ctx, reply, err) } resp, err := client.Configuration(ctx, ¶ms) return true, reply(ctx, resp, err) case MethodWorkspaceWorkspaceFolders: // request defer logger.Debug(MethodWorkspaceWorkspaceFolders, zap.Error(err)) if len(req.Params()) > 0 { return true, reply(ctx, nil, fmt.Errorf("expected no params: %w", jsonrpc2.ErrInvalidParams)) } resp, err := client.WorkspaceFolders(ctx) return true, reply(ctx, resp, err) default: return false, nil } } // Client represents a Language Server Protocol client. type Client interface { Progress(ctx context.Context, params *ProgressParams) (err error) WorkDoneProgressCreate(ctx context.Context, params *WorkDoneProgressCreateParams) (err error) LogMessage(ctx context.Context, params *LogMessageParams) (err error) PublishDiagnostics(ctx context.Context, params *PublishDiagnosticsParams) (err error) ShowMessage(ctx context.Context, params *ShowMessageParams) (err error) ShowMessageRequest(ctx context.Context, params *ShowMessageRequestParams) (result *MessageActionItem, err error) Telemetry(ctx context.Context, params interface{}) (err error) RegisterCapability(ctx context.Context, params *RegistrationParams) (err error) UnregisterCapability(ctx context.Context, params *UnregistrationParams) (err error) ApplyEdit(ctx context.Context, params *ApplyWorkspaceEditParams) (result bool, err error) Configuration(ctx context.Context, params *ConfigurationParams) (result []interface{}, err error) WorkspaceFolders(ctx context.Context) (result []WorkspaceFolder, err error) } // list of client methods. const ( // MethodProgress method name of "$/progress". MethodProgress = "$/progress" // MethodWorkDoneProgressCreate method name of "window/workDoneProgress/create". MethodWorkDoneProgressCreate = "window/workDoneProgress/create" // MethodWindowShowMessage method name of "window/showMessage". MethodWindowShowMessage = "window/showMessage" // MethodWindowShowMessageRequest method name of "window/showMessageRequest. MethodWindowShowMessageRequest = "window/showMessageRequest" // MethodWindowLogMessage method name of "window/logMessage. MethodWindowLogMessage = "window/logMessage" // MethodTelemetryEvent method name of "telemetry/event. MethodTelemetryEvent = "telemetry/event" // MethodClientRegisterCapability method name of "client/registerCapability. MethodClientRegisterCapability = "client/registerCapability" // MethodClientUnregisterCapability method name of "client/unregisterCapability. MethodClientUnregisterCapability = "client/unregisterCapability" // MethodTextDocumentPublishDiagnostics method name of "textDocument/publishDiagnostics. MethodTextDocumentPublishDiagnostics = "textDocument/publishDiagnostics" // MethodWorkspaceApplyEdit method name of "workspace/applyEdit. MethodWorkspaceApplyEdit = "workspace/applyEdit" // MethodWorkspaceConfiguration method name of "workspace/configuration. MethodWorkspaceConfiguration = "workspace/configuration" // MethodWorkspaceWorkspaceFolders method name of "workspace/workspaceFolders". MethodWorkspaceWorkspaceFolders = "workspace/workspaceFolders" ) // client implements a Language Server Protocol client. type client struct { jsonrpc2.Conn logger *zap.Logger } // compiler time check whether the Client implements ClientInterface interface. var _ Client = (*client)(nil) // Progress is the base protocol offers also support to report progress in a generic fashion. // // This mechanism can be used to report any kind of progress including work done progress (usually used to report progress in the user interface using a progress bar) and // partial result progress to support streaming of results. // // @since 3.16.0. func (c *client) Progress(ctx context.Context, params *ProgressParams) (err error) { c.logger.Debug("call " + MethodProgress) defer c.logger.Debug("end "+MethodProgress, zap.Error(err)) return c.Conn.Notify(ctx, MethodProgress, params) } // WorkDoneProgressCreate sends the request is sent from the server to the client to ask the client to create a work done progress. // // @since 3.16.0. func (c *client) WorkDoneProgressCreate(ctx context.Context, params *WorkDoneProgressCreateParams) (err error) { c.logger.Debug("call " + MethodWorkDoneProgressCreate) defer c.logger.Debug("end "+MethodWorkDoneProgressCreate, zap.Error(err)) return Call(ctx, c.Conn, MethodWorkDoneProgressCreate, params, nil) } // LogMessage sends the notification from the server to the client to ask the client to log a particular message. func (c *client) LogMessage(ctx context.Context, params *LogMessageParams) (err error) { c.logger.Debug("call " + MethodWindowLogMessage) defer c.logger.Debug("end "+MethodWindowLogMessage, zap.Error(err)) return c.Conn.Notify(ctx, MethodWindowLogMessage, params) } // PublishDiagnostics sends the notification 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. func (c *client) PublishDiagnostics(ctx context.Context, params *PublishDiagnosticsParams) (err error) { c.logger.Debug("call " + MethodTextDocumentPublishDiagnostics) defer c.logger.Debug("end "+MethodTextDocumentPublishDiagnostics, zap.Error(err)) return c.Conn.Notify(ctx, MethodTextDocumentPublishDiagnostics, params) } // ShowMessage sends the notification from a server to a client to ask the // client to display a particular message in the user interface. func (c *client) ShowMessage(ctx context.Context, params *ShowMessageParams) (err error) { return c.Conn.Notify(ctx, MethodWindowShowMessage, params) } // ShowMessageRequest sends the request 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. func (c *client) ShowMessageRequest(ctx context.Context, params *ShowMessageRequestParams) (_ *MessageActionItem, err error) { c.logger.Debug("call " + MethodWindowShowMessageRequest) defer c.logger.Debug("end "+MethodWindowShowMessageRequest, zap.Error(err)) var result *MessageActionItem if err := Call(ctx, c.Conn, MethodWindowShowMessageRequest, params, &result); err != nil { return nil, err } return result, nil } // Telemetry sends the notification from the server to the client to ask the client to log a telemetry event. func (c *client) Telemetry(ctx context.Context, params interface{}) (err error) { c.logger.Debug("call " + MethodTelemetryEvent) defer c.logger.Debug("end "+MethodTelemetryEvent, zap.Error(err)) return c.Conn.Notify(ctx, MethodTelemetryEvent, params) } // RegisterCapability sends the request 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 (see TextDocumentClientCapabilities as an example). func (c *client) RegisterCapability(ctx context.Context, params *RegistrationParams) (err error) { c.logger.Debug("call " + MethodClientRegisterCapability) defer c.logger.Debug("end "+MethodClientRegisterCapability, zap.Error(err)) return Call(ctx, c.Conn, MethodClientRegisterCapability, params, nil) } // UnregisterCapability sends the request from the server to the client to unregister a previously registered capability. func (c *client) UnregisterCapability(ctx context.Context, params *UnregistrationParams) (err error) { c.logger.Debug("call " + MethodClientUnregisterCapability) defer c.logger.Debug("end "+MethodClientUnregisterCapability, zap.Error(err)) return Call(ctx, c.Conn, MethodClientUnregisterCapability, params, nil) } // ApplyEdit sends the request from the server to the client to modify resource on the client side. func (c *client) ApplyEdit(ctx context.Context, params *ApplyWorkspaceEditParams) (result bool, err error) { c.logger.Debug("call " + MethodWorkspaceApplyEdit) defer c.logger.Debug("end "+MethodWorkspaceApplyEdit, zap.Error(err)) if err := Call(ctx, c.Conn, MethodWorkspaceApplyEdit, params, &result); err != nil { return false, err } return result, nil } // Configuration sends the request from the server to the client to fetch configuration settings from the client. // // The request can fetch several 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). func (c *client) Configuration(ctx context.Context, params *ConfigurationParams) (_ []interface{}, err error) { c.logger.Debug("call " + MethodWorkspaceConfiguration) defer c.logger.Debug("end "+MethodWorkspaceConfiguration, zap.Error(err)) var result []interface{} if err := Call(ctx, c.Conn, MethodWorkspaceConfiguration, params, &result); err != nil { return nil, err } return result, nil } // WorkspaceFolders sends the request 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. // // @since 3.6.0. func (c *client) WorkspaceFolders(ctx context.Context) (result []WorkspaceFolder, err error) { c.logger.Debug("call " + MethodWorkspaceWorkspaceFolders) defer c.logger.Debug("end "+MethodWorkspaceWorkspaceFolders, zap.Error(err)) if err := Call(ctx, c.Conn, MethodWorkspaceWorkspaceFolders, nil, &result); err != nil { return nil, err } return result, nil }