internal/langserver/handlers/service.go (206 lines of code) (raw):
package handlers
import (
"context"
"errors"
"fmt"
"io"
"log"
lsctx "github.com/Azure/azurerm-lsp/internal/context"
"github.com/Azure/azurerm-lsp/internal/filesystem"
"github.com/Azure/azurerm-lsp/internal/langserver/diagnostics"
"github.com/Azure/azurerm-lsp/internal/langserver/session"
ilsp "github.com/Azure/azurerm-lsp/internal/lsp"
lsp "github.com/Azure/azurerm-lsp/internal/protocol"
"github.com/Azure/azurerm-lsp/internal/telemetry"
"github.com/creachadair/jrpc2"
"github.com/creachadair/jrpc2/code"
rpch "github.com/creachadair/jrpc2/handler"
)
type service struct {
logger *log.Logger
srvCtx context.Context
sessCtx context.Context
stopSession context.CancelFunc
fs filesystem.Filesystem
telemetry telemetry.Sender
server session.Server
diagsNotifier *diagnostics.Notifier
clientCaller session.ClientCaller
clientNotifier session.ClientNotifier
additionalHandlers map[string]rpch.Func
}
var discardLogs = log.New(io.Discard, "", 0)
func NewSession(srvCtx context.Context) session.Session {
fs := filesystem.NewFilesystem()
sessCtx, stopSession := context.WithCancel(srvCtx)
return &service{
logger: discardLogs,
fs: fs,
srvCtx: srvCtx,
sessCtx: sessCtx,
stopSession: stopSession,
telemetry: &telemetry.NoopSender{},
}
}
func (svc *service) SetLogger(logger *log.Logger) {
svc.logger = logger
}
// Assigner builds out the jrpc2.Map according to the LSP protocol
// and passes related dependencies to handlers via context
func (svc *service) Assigner() (jrpc2.Assigner, error) {
svc.logger.Println("Preparing new session ...")
session := session.NewSession(svc.stopSession)
err := session.Prepare()
if err != nil {
return nil, fmt.Errorf("Unable to prepare session: %w", err)
}
svc.telemetry = &telemetry.NoopSender{Logger: svc.logger}
svc.fs.SetLogger(svc.logger)
lh := LogHandler(svc.logger)
cc := &lsp.ClientCapabilities{}
clientName := ""
m := map[string]rpch.Func{
"initialize": func(ctx context.Context, req *jrpc2.Request) (interface{}, error) {
err := session.Initialize(req)
if err != nil {
return nil, err
}
ctx = ilsp.WithClientCapabilities(ctx, cc)
ctx = ilsp.ContextWithClientName(ctx, &clientName)
version, ok := lsctx.LanguageServerVersion(svc.srvCtx)
if ok {
ctx = lsctx.WithLanguageServerVersion(ctx, version)
}
return handle(ctx, req, svc.Initialize)
},
"initialized": func(ctx context.Context, req *jrpc2.Request) (interface{}, error) {
err := session.ConfirmInitialization(req)
if err != nil {
return nil, err
}
return handle(ctx, req, Initialized)
},
"textDocument/didChange": func(ctx context.Context, req *jrpc2.Request) (interface{}, error) {
err := session.CheckInitializationIsConfirmed()
if err != nil {
return nil, err
}
ctx = lsctx.WithDocumentStorage(ctx, svc.fs)
ctx = lsctx.WithDiagnosticsNotifier(ctx, svc.diagsNotifier)
return handle(ctx, req, TextDocumentDidChange)
},
"textDocument/didOpen": func(ctx context.Context, req *jrpc2.Request) (interface{}, error) {
err := session.CheckInitializationIsConfirmed()
if err != nil {
return nil, err
}
ctx = lsctx.WithDocumentStorage(ctx, svc.fs)
ctx = lsctx.WithDiagnosticsNotifier(ctx, svc.diagsNotifier)
return handle(ctx, req, lh.TextDocumentDidOpen)
},
"textDocument/didSave": func(ctx context.Context, req *jrpc2.Request) (interface{}, error) {
err := session.CheckInitializationIsConfirmed()
if err != nil {
return nil, err
}
ctx = lsctx.WithDiagnosticsNotifier(ctx, svc.diagsNotifier)
return handle(ctx, req, lh.TextDocumentDidSave)
},
"textDocument/didClose": func(ctx context.Context, req *jrpc2.Request) (interface{}, error) {
err := session.CheckInitializationIsConfirmed()
if err != nil {
return nil, err
}
ctx = lsctx.WithDocumentStorage(ctx, svc.fs)
return handle(ctx, req, TextDocumentDidClose)
},
"textDocument/completion": func(ctx context.Context, req *jrpc2.Request) (interface{}, error) {
err := session.CheckInitializationIsConfirmed()
if err != nil {
return nil, err
}
ctx = lsctx.WithDocumentStorage(ctx, svc.fs)
ctx = ilsp.WithClientCapabilities(ctx, cc)
ctx = lsctx.WithTelemetry(ctx, svc.telemetry)
return handle(ctx, req, svc.HandleComplete)
},
"textDocument/hover": func(ctx context.Context, req *jrpc2.Request) (interface{}, error) {
err := session.CheckInitializationIsConfirmed()
if err != nil {
return nil, err
}
ctx = lsctx.WithDocumentStorage(ctx, svc.fs)
ctx = ilsp.WithClientCapabilities(ctx, cc)
ctx = ilsp.ContextWithClientName(ctx, &clientName)
ctx = lsctx.WithTelemetry(ctx, svc.telemetry)
return handle(ctx, req, svc.HandleHover)
},
"shutdown": func(ctx context.Context, req *jrpc2.Request) (interface{}, error) {
err := session.Shutdown(req)
if err != nil {
return nil, err
}
ctx = lsctx.WithDocumentStorage(ctx, svc.fs)
svc.shutdown()
return handle(ctx, req, Shutdown)
},
"exit": func(ctx context.Context, req *jrpc2.Request) (interface{}, error) {
err := session.Exit()
if err != nil {
return nil, err
}
svc.stopSession()
return nil, nil
},
"$/cancelRequest": func(ctx context.Context, req *jrpc2.Request) (interface{}, error) {
err := session.CheckInitializationIsConfirmed()
if err != nil {
return nil, err
}
return handle(ctx, req, CancelRequest)
},
}
// For use in tests, e.g. to test request cancellation
if len(svc.additionalHandlers) > 0 {
for methodName, handlerFunc := range svc.additionalHandlers {
m[methodName] = handlerFunc
}
}
return convertMap(m), nil
}
func (svc *service) configureSessionDependencies() error {
svc.diagsNotifier = diagnostics.NewNotifier(svc.server, svc.logger)
svc.clientCaller = svc.server
svc.clientNotifier = svc.server
return nil
}
func (svc *service) setupTelemetry(version int, notifier session.ClientNotifier) error {
t, err := telemetry.NewSender(version, notifier)
if err != nil {
return err
}
svc.telemetry = t
return nil
}
func (svc *service) Finish(_ jrpc2.Assigner, status jrpc2.ServerStatus) {
if status.Closed || status.Err != nil {
svc.logger.Printf("session stopped unexpectedly (err: %v)", status.Err)
}
svc.shutdown()
svc.stopSession()
}
func (svc *service) shutdown() {
}
// convertMap is a helper function allowing us to omit the jrpc2.Func
// signature from the method definitions
func convertMap(m map[string]rpch.Func) rpch.Map {
hm := make(rpch.Map, len(m))
for method, fun := range m {
hm[method] = rpch.New(fun)
}
return hm
}
const requestCancelled code.Code = -32800
// handle calls a jrpc2.Func compatible function
func handle(ctx context.Context, req *jrpc2.Request, fn interface{}) (interface{}, error) {
f := rpch.New(fn)
result, err := f.Handle(ctx, req)
if ctx.Err() != nil && errors.Is(ctx.Err(), context.Canceled) {
err = fmt.Errorf("%w: %s", requestCancelled.Err(), err)
}
return result, err
}