413 lines
15 KiB
Go
413 lines
15 KiB
Go
// 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
|
||
}
|