924 lines
29 KiB
Go
924 lines
29 KiB
Go
package lsp
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"os/exec"
|
|
"os/signal"
|
|
"runtime/debug"
|
|
"slices"
|
|
"sync"
|
|
"sync/atomic"
|
|
"syscall"
|
|
"time"
|
|
|
|
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/collections"
|
|
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/core"
|
|
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/ls"
|
|
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/lsp/lsproto"
|
|
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/project"
|
|
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/project/ata"
|
|
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/project/logging"
|
|
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/tspath"
|
|
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/vfs"
|
|
"github.com/go-json-experiment/json"
|
|
"golang.org/x/sync/errgroup"
|
|
"golang.org/x/text/language"
|
|
)
|
|
|
|
type ServerOptions struct {
|
|
In Reader
|
|
Out Writer
|
|
Err io.Writer
|
|
|
|
Cwd string
|
|
FS vfs.FS
|
|
DefaultLibraryPath string
|
|
TypingsLocation string
|
|
ParseCache *project.ParseCache
|
|
}
|
|
|
|
func NewServer(opts *ServerOptions) *Server {
|
|
if opts.Cwd == "" {
|
|
panic("Cwd is required")
|
|
}
|
|
return &Server{
|
|
r: opts.In,
|
|
w: opts.Out,
|
|
stderr: opts.Err,
|
|
logger: logging.NewLogger(opts.Err),
|
|
requestQueue: make(chan *lsproto.RequestMessage, 100),
|
|
outgoingQueue: make(chan *lsproto.Message, 100),
|
|
pendingClientRequests: make(map[lsproto.ID]pendingClientRequest),
|
|
pendingServerRequests: make(map[lsproto.ID]chan *lsproto.ResponseMessage),
|
|
cwd: opts.Cwd,
|
|
fs: opts.FS,
|
|
defaultLibraryPath: opts.DefaultLibraryPath,
|
|
typingsLocation: opts.TypingsLocation,
|
|
parseCache: opts.ParseCache,
|
|
}
|
|
}
|
|
|
|
var (
|
|
_ ata.NpmExecutor = (*Server)(nil)
|
|
_ project.Client = (*Server)(nil)
|
|
)
|
|
|
|
type pendingClientRequest struct {
|
|
req *lsproto.RequestMessage
|
|
cancel context.CancelFunc
|
|
}
|
|
|
|
type Reader interface {
|
|
Read() (*lsproto.Message, error)
|
|
}
|
|
|
|
type Writer interface {
|
|
Write(msg *lsproto.Message) error
|
|
}
|
|
|
|
type lspReader struct {
|
|
r *lsproto.BaseReader
|
|
}
|
|
|
|
type lspWriter struct {
|
|
w *lsproto.BaseWriter
|
|
}
|
|
|
|
func (r *lspReader) Read() (*lsproto.Message, error) {
|
|
data, err := r.r.Read()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
req := &lsproto.Message{}
|
|
if err := json.Unmarshal(data, req); err != nil {
|
|
return nil, fmt.Errorf("%w: %w", lsproto.ErrInvalidRequest, err)
|
|
}
|
|
|
|
return req, nil
|
|
}
|
|
|
|
func ToReader(r io.Reader) Reader {
|
|
return &lspReader{r: lsproto.NewBaseReader(r)}
|
|
}
|
|
|
|
func (w *lspWriter) Write(msg *lsproto.Message) error {
|
|
data, err := json.Marshal(msg)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to marshal message: %w", err)
|
|
}
|
|
return w.w.Write(data)
|
|
}
|
|
|
|
func ToWriter(w io.Writer) Writer {
|
|
return &lspWriter{w: lsproto.NewBaseWriter(w)}
|
|
}
|
|
|
|
var (
|
|
_ Reader = (*lspReader)(nil)
|
|
_ Writer = (*lspWriter)(nil)
|
|
)
|
|
|
|
type Server struct {
|
|
r Reader
|
|
w Writer
|
|
|
|
stderr io.Writer
|
|
|
|
logger logging.Logger
|
|
clientSeq atomic.Int32
|
|
requestQueue chan *lsproto.RequestMessage
|
|
outgoingQueue chan *lsproto.Message
|
|
pendingClientRequests map[lsproto.ID]pendingClientRequest
|
|
pendingClientRequestsMu sync.Mutex
|
|
pendingServerRequests map[lsproto.ID]chan *lsproto.ResponseMessage
|
|
pendingServerRequestsMu sync.Mutex
|
|
|
|
cwd string
|
|
fs vfs.FS
|
|
defaultLibraryPath string
|
|
typingsLocation string
|
|
|
|
initializeParams *lsproto.InitializeParams
|
|
positionEncoding lsproto.PositionEncodingKind
|
|
locale language.Tag
|
|
|
|
watchEnabled bool
|
|
watcherID atomic.Uint32
|
|
watchers collections.SyncSet[project.WatcherID]
|
|
|
|
session *project.Session
|
|
|
|
// !!! temporary; remove when we have `handleDidChangeConfiguration`/implicit project config support
|
|
compilerOptionsForInferredProjects *core.CompilerOptions
|
|
// parseCache can be passed in so separate tests can share ASTs
|
|
parseCache *project.ParseCache
|
|
}
|
|
|
|
// WatchFiles implements project.Client.
|
|
func (s *Server) WatchFiles(ctx context.Context, id project.WatcherID, watchers []*lsproto.FileSystemWatcher) error {
|
|
_, err := s.sendRequest(ctx, lsproto.MethodClientRegisterCapability, &lsproto.RegistrationParams{
|
|
Registrations: []*lsproto.Registration{
|
|
{
|
|
Id: string(id),
|
|
Method: string(lsproto.MethodWorkspaceDidChangeWatchedFiles),
|
|
RegisterOptions: ptrTo(any(lsproto.DidChangeWatchedFilesRegistrationOptions{
|
|
Watchers: watchers,
|
|
})),
|
|
},
|
|
},
|
|
})
|
|
if err != nil {
|
|
return fmt.Errorf("failed to register file watcher: %w", err)
|
|
}
|
|
|
|
s.watchers.Add(id)
|
|
return nil
|
|
}
|
|
|
|
// UnwatchFiles implements project.Client.
|
|
func (s *Server) UnwatchFiles(ctx context.Context, id project.WatcherID) error {
|
|
if s.watchers.Has(id) {
|
|
_, err := s.sendRequest(ctx, lsproto.MethodClientUnregisterCapability, &lsproto.UnregistrationParams{
|
|
Unregisterations: []*lsproto.Unregistration{
|
|
{
|
|
Id: string(id),
|
|
Method: string(lsproto.MethodWorkspaceDidChangeWatchedFiles),
|
|
},
|
|
},
|
|
})
|
|
if err != nil {
|
|
return fmt.Errorf("failed to unregister file watcher: %w", err)
|
|
}
|
|
|
|
s.watchers.Delete(id)
|
|
return nil
|
|
}
|
|
|
|
return fmt.Errorf("no file watcher exists with ID %s", id)
|
|
}
|
|
|
|
// RefreshDiagnostics implements project.Client.
|
|
func (s *Server) RefreshDiagnostics(ctx context.Context) error {
|
|
if s.initializeParams.Capabilities == nil ||
|
|
s.initializeParams.Capabilities.Workspace == nil ||
|
|
s.initializeParams.Capabilities.Workspace.Diagnostics == nil ||
|
|
!ptrIsTrue(s.initializeParams.Capabilities.Workspace.Diagnostics.RefreshSupport) {
|
|
return nil
|
|
}
|
|
|
|
if _, err := s.sendRequest(ctx, lsproto.MethodWorkspaceDiagnosticRefresh, nil); err != nil {
|
|
return fmt.Errorf("failed to refresh diagnostics: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *Server) Run() error {
|
|
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
|
|
defer stop()
|
|
|
|
g, ctx := errgroup.WithContext(ctx)
|
|
g.Go(func() error { return s.dispatchLoop(ctx) })
|
|
g.Go(func() error { return s.writeLoop(ctx) })
|
|
|
|
// Don't run readLoop in the group, as it blocks on stdin read and cannot be cancelled.
|
|
readLoopErr := make(chan error, 1)
|
|
g.Go(func() error {
|
|
select {
|
|
case <-ctx.Done():
|
|
return ctx.Err()
|
|
case err := <-readLoopErr:
|
|
return err
|
|
}
|
|
})
|
|
go func() { readLoopErr <- s.readLoop(ctx) }()
|
|
|
|
if err := g.Wait(); err != nil && !errors.Is(err, io.EOF) && ctx.Err() != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *Server) readLoop(ctx context.Context) error {
|
|
for {
|
|
if err := ctx.Err(); err != nil {
|
|
return err
|
|
}
|
|
msg, err := s.read()
|
|
if err != nil {
|
|
if errors.Is(err, lsproto.ErrInvalidRequest) {
|
|
s.sendError(nil, err)
|
|
continue
|
|
}
|
|
return err
|
|
}
|
|
|
|
if s.initializeParams == nil && msg.Kind == lsproto.MessageKindRequest {
|
|
req := msg.AsRequest()
|
|
if req.Method == lsproto.MethodInitialize {
|
|
resp, err := s.handleInitialize(ctx, req.Params.(*lsproto.InitializeParams), req)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
s.sendResult(req.ID, resp)
|
|
} else {
|
|
s.sendError(req.ID, lsproto.ErrServerNotInitialized)
|
|
}
|
|
continue
|
|
}
|
|
|
|
if msg.Kind == lsproto.MessageKindResponse {
|
|
resp := msg.AsResponse()
|
|
s.pendingServerRequestsMu.Lock()
|
|
if respChan, ok := s.pendingServerRequests[*resp.ID]; ok {
|
|
respChan <- resp
|
|
close(respChan)
|
|
delete(s.pendingServerRequests, *resp.ID)
|
|
}
|
|
s.pendingServerRequestsMu.Unlock()
|
|
} else {
|
|
req := msg.AsRequest()
|
|
if req.Method == lsproto.MethodCancelRequest {
|
|
s.cancelRequest(req.Params.(*lsproto.CancelParams).Id)
|
|
} else {
|
|
s.requestQueue <- req
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func (s *Server) cancelRequest(rawID lsproto.IntegerOrString) {
|
|
id := lsproto.NewID(rawID)
|
|
s.pendingClientRequestsMu.Lock()
|
|
defer s.pendingClientRequestsMu.Unlock()
|
|
if pendingReq, ok := s.pendingClientRequests[*id]; ok {
|
|
pendingReq.cancel()
|
|
delete(s.pendingClientRequests, *id)
|
|
}
|
|
}
|
|
|
|
func (s *Server) read() (*lsproto.Message, error) {
|
|
return s.r.Read()
|
|
}
|
|
|
|
func (s *Server) dispatchLoop(ctx context.Context) error {
|
|
ctx, lspExit := context.WithCancel(ctx)
|
|
defer lspExit()
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
return ctx.Err()
|
|
case req := <-s.requestQueue:
|
|
requestCtx := core.WithLocale(ctx, s.locale)
|
|
if req.ID != nil {
|
|
var cancel context.CancelFunc
|
|
requestCtx, cancel = context.WithCancel(core.WithRequestID(requestCtx, req.ID.String()))
|
|
s.pendingClientRequestsMu.Lock()
|
|
s.pendingClientRequests[*req.ID] = pendingClientRequest{
|
|
req: req,
|
|
cancel: cancel,
|
|
}
|
|
s.pendingClientRequestsMu.Unlock()
|
|
}
|
|
|
|
handle := func() {
|
|
if err := s.handleRequestOrNotification(requestCtx, req); err != nil {
|
|
if errors.Is(err, context.Canceled) {
|
|
s.sendError(req.ID, lsproto.ErrRequestCancelled)
|
|
} else if errors.Is(err, io.EOF) {
|
|
lspExit()
|
|
} else {
|
|
s.sendError(req.ID, err)
|
|
}
|
|
}
|
|
|
|
if req.ID != nil {
|
|
s.pendingClientRequestsMu.Lock()
|
|
delete(s.pendingClientRequests, *req.ID)
|
|
s.pendingClientRequestsMu.Unlock()
|
|
}
|
|
}
|
|
|
|
if isBlockingMethod(req.Method) {
|
|
handle()
|
|
} else {
|
|
go handle()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func (s *Server) writeLoop(ctx context.Context) error {
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
return ctx.Err()
|
|
case msg := <-s.outgoingQueue:
|
|
if err := s.w.Write(msg); err != nil {
|
|
return fmt.Errorf("failed to write message: %w", err)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func (s *Server) sendRequest(ctx context.Context, method lsproto.Method, params any) (any, error) {
|
|
id := lsproto.NewIDString(fmt.Sprintf("ts%d", s.clientSeq.Add(1)))
|
|
req := lsproto.NewRequestMessage(method, id, params)
|
|
|
|
responseChan := make(chan *lsproto.ResponseMessage, 1)
|
|
s.pendingServerRequestsMu.Lock()
|
|
s.pendingServerRequests[*id] = responseChan
|
|
s.pendingServerRequestsMu.Unlock()
|
|
|
|
s.outgoingQueue <- req.Message()
|
|
|
|
select {
|
|
case <-ctx.Done():
|
|
s.pendingServerRequestsMu.Lock()
|
|
defer s.pendingServerRequestsMu.Unlock()
|
|
if respChan, ok := s.pendingServerRequests[*id]; ok {
|
|
close(respChan)
|
|
delete(s.pendingServerRequests, *id)
|
|
}
|
|
return nil, ctx.Err()
|
|
case resp := <-responseChan:
|
|
if resp.Error != nil {
|
|
return nil, fmt.Errorf("request failed: %s", resp.Error.String())
|
|
}
|
|
return resp.Result, nil
|
|
}
|
|
}
|
|
|
|
func (s *Server) sendResult(id *lsproto.ID, result any) {
|
|
s.sendResponse(&lsproto.ResponseMessage{
|
|
ID: id,
|
|
Result: result,
|
|
})
|
|
}
|
|
|
|
func (s *Server) sendError(id *lsproto.ID, err error) {
|
|
code := lsproto.ErrInternalError.Code
|
|
if errCode := (*lsproto.ErrorCode)(nil); errors.As(err, &errCode) {
|
|
code = errCode.Code
|
|
}
|
|
// TODO(jakebailey): error data
|
|
s.sendResponse(&lsproto.ResponseMessage{
|
|
ID: id,
|
|
Error: &lsproto.ResponseError{
|
|
Code: code,
|
|
Message: err.Error(),
|
|
},
|
|
})
|
|
}
|
|
|
|
func (s *Server) sendResponse(resp *lsproto.ResponseMessage) {
|
|
s.outgoingQueue <- resp.Message()
|
|
}
|
|
|
|
func (s *Server) handleRequestOrNotification(ctx context.Context, req *lsproto.RequestMessage) error {
|
|
if handler := handlers()[req.Method]; handler != nil {
|
|
return handler(s, ctx, req)
|
|
}
|
|
s.Log("unknown method", req.Method)
|
|
if req.ID != nil {
|
|
s.sendError(req.ID, lsproto.ErrInvalidRequest)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
type handlerMap map[lsproto.Method]func(*Server, context.Context, *lsproto.RequestMessage) error
|
|
|
|
var handlers = sync.OnceValue(func() handlerMap {
|
|
handlers := make(handlerMap)
|
|
|
|
registerRequestHandler(handlers, lsproto.InitializeInfo, (*Server).handleInitialize)
|
|
registerNotificationHandler(handlers, lsproto.InitializedInfo, (*Server).handleInitialized)
|
|
registerRequestHandler(handlers, lsproto.ShutdownInfo, (*Server).handleShutdown)
|
|
registerNotificationHandler(handlers, lsproto.ExitInfo, (*Server).handleExit)
|
|
|
|
registerNotificationHandler(handlers, lsproto.TextDocumentDidOpenInfo, (*Server).handleDidOpen)
|
|
registerNotificationHandler(handlers, lsproto.TextDocumentDidChangeInfo, (*Server).handleDidChange)
|
|
registerNotificationHandler(handlers, lsproto.TextDocumentDidSaveInfo, (*Server).handleDidSave)
|
|
registerNotificationHandler(handlers, lsproto.TextDocumentDidCloseInfo, (*Server).handleDidClose)
|
|
registerNotificationHandler(handlers, lsproto.WorkspaceDidChangeWatchedFilesInfo, (*Server).handleDidChangeWatchedFiles)
|
|
registerNotificationHandler(handlers, lsproto.SetTraceInfo, (*Server).handleSetTrace)
|
|
|
|
registerLanguageServiceDocumentRequestHandler(handlers, lsproto.TextDocumentDiagnosticInfo, (*Server).handleDocumentDiagnostic)
|
|
registerLanguageServiceDocumentRequestHandler(handlers, lsproto.TextDocumentHoverInfo, (*Server).handleHover)
|
|
registerLanguageServiceDocumentRequestHandler(handlers, lsproto.TextDocumentDefinitionInfo, (*Server).handleDefinition)
|
|
registerLanguageServiceDocumentRequestHandler(handlers, lsproto.TextDocumentTypeDefinitionInfo, (*Server).handleTypeDefinition)
|
|
registerLanguageServiceDocumentRequestHandler(handlers, lsproto.TextDocumentCompletionInfo, (*Server).handleCompletion)
|
|
registerLanguageServiceDocumentRequestHandler(handlers, lsproto.TextDocumentReferencesInfo, (*Server).handleReferences)
|
|
registerLanguageServiceDocumentRequestHandler(handlers, lsproto.TextDocumentImplementationInfo, (*Server).handleImplementations)
|
|
registerLanguageServiceDocumentRequestHandler(handlers, lsproto.TextDocumentSignatureHelpInfo, (*Server).handleSignatureHelp)
|
|
registerLanguageServiceDocumentRequestHandler(handlers, lsproto.TextDocumentFormattingInfo, (*Server).handleDocumentFormat)
|
|
registerLanguageServiceDocumentRequestHandler(handlers, lsproto.TextDocumentRangeFormattingInfo, (*Server).handleDocumentRangeFormat)
|
|
registerLanguageServiceDocumentRequestHandler(handlers, lsproto.TextDocumentOnTypeFormattingInfo, (*Server).handleDocumentOnTypeFormat)
|
|
registerLanguageServiceDocumentRequestHandler(handlers, lsproto.TextDocumentDocumentSymbolInfo, (*Server).handleDocumentSymbol)
|
|
registerLanguageServiceDocumentRequestHandler(handlers, lsproto.TextDocumentRenameInfo, (*Server).handleRename)
|
|
registerLanguageServiceDocumentRequestHandler(handlers, lsproto.TextDocumentDocumentHighlightInfo, (*Server).handleDocumentHighlight)
|
|
registerRequestHandler(handlers, lsproto.WorkspaceSymbolInfo, (*Server).handleWorkspaceSymbol)
|
|
registerRequestHandler(handlers, lsproto.CompletionItemResolveInfo, (*Server).handleCompletionItemResolve)
|
|
|
|
return handlers
|
|
})
|
|
|
|
func registerNotificationHandler[Req any](handlers handlerMap, info lsproto.NotificationInfo[Req], fn func(*Server, context.Context, Req) error) {
|
|
handlers[info.Method] = func(s *Server, ctx context.Context, req *lsproto.RequestMessage) error {
|
|
if s.session == nil && req.Method != lsproto.MethodInitialized {
|
|
return lsproto.ErrServerNotInitialized
|
|
}
|
|
|
|
var params Req
|
|
// Ignore empty params; all generated params are either pointers or any.
|
|
if req.Params != nil {
|
|
params = req.Params.(Req)
|
|
}
|
|
if err := fn(s, ctx, params); err != nil {
|
|
return err
|
|
}
|
|
return ctx.Err()
|
|
}
|
|
}
|
|
|
|
func registerRequestHandler[Req, Resp any](
|
|
handlers handlerMap,
|
|
info lsproto.RequestInfo[Req, Resp],
|
|
fn func(*Server, context.Context, Req, *lsproto.RequestMessage) (Resp, error),
|
|
) {
|
|
handlers[info.Method] = func(s *Server, ctx context.Context, req *lsproto.RequestMessage) error {
|
|
if s.session == nil && req.Method != lsproto.MethodInitialize {
|
|
return lsproto.ErrServerNotInitialized
|
|
}
|
|
|
|
var params Req
|
|
// Ignore empty params.
|
|
if req.Params != nil {
|
|
params = req.Params.(Req)
|
|
}
|
|
resp, err := fn(s, ctx, params, req)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if ctx.Err() != nil {
|
|
return ctx.Err()
|
|
}
|
|
s.sendResult(req.ID, resp)
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func registerLanguageServiceDocumentRequestHandler[Req lsproto.HasTextDocumentURI, Resp any](handlers handlerMap, info lsproto.RequestInfo[Req, Resp], fn func(*Server, context.Context, *ls.LanguageService, Req) (Resp, error)) {
|
|
handlers[info.Method] = func(s *Server, ctx context.Context, req *lsproto.RequestMessage) error {
|
|
var params Req
|
|
// Ignore empty params.
|
|
if req.Params != nil {
|
|
params = req.Params.(Req)
|
|
}
|
|
ls, err := s.session.GetLanguageService(ctx, params.TextDocumentURI())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer s.recover(req)
|
|
resp, err := fn(s, ctx, ls, params)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if ctx.Err() != nil {
|
|
return ctx.Err()
|
|
}
|
|
s.sendResult(req.ID, resp)
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func (s *Server) recover(req *lsproto.RequestMessage) {
|
|
if r := recover(); r != nil {
|
|
stack := debug.Stack()
|
|
s.Log("panic handling request", req.Method, r, string(stack))
|
|
if req.ID != nil {
|
|
s.sendError(req.ID, fmt.Errorf("%w: panic handling request %s: %v", lsproto.ErrInternalError, req.Method, r))
|
|
} else {
|
|
s.Log("unhandled panic in notification", req.Method, r)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (s *Server) handleInitialize(ctx context.Context, params *lsproto.InitializeParams, _ *lsproto.RequestMessage) (lsproto.InitializeResponse, error) {
|
|
if s.initializeParams != nil {
|
|
return nil, lsproto.ErrInvalidRequest
|
|
}
|
|
|
|
s.initializeParams = params
|
|
|
|
s.positionEncoding = lsproto.PositionEncodingKindUTF16
|
|
if genCapabilities := s.initializeParams.Capabilities.General; genCapabilities != nil && genCapabilities.PositionEncodings != nil {
|
|
if slices.Contains(*genCapabilities.PositionEncodings, lsproto.PositionEncodingKindUTF8) {
|
|
s.positionEncoding = lsproto.PositionEncodingKindUTF8
|
|
}
|
|
}
|
|
|
|
if s.initializeParams.Locale != nil {
|
|
locale, err := language.Parse(*s.initializeParams.Locale)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
s.locale = locale
|
|
}
|
|
|
|
if s.initializeParams.Trace != nil && *s.initializeParams.Trace == "verbose" {
|
|
s.logger.SetVerbose(true)
|
|
}
|
|
|
|
response := &lsproto.InitializeResult{
|
|
ServerInfo: &lsproto.ServerInfo{
|
|
Name: "typescript-go",
|
|
Version: ptrTo(core.Version()),
|
|
},
|
|
Capabilities: &lsproto.ServerCapabilities{
|
|
PositionEncoding: ptrTo(s.positionEncoding),
|
|
TextDocumentSync: &lsproto.TextDocumentSyncOptionsOrKind{
|
|
Options: &lsproto.TextDocumentSyncOptions{
|
|
OpenClose: ptrTo(true),
|
|
Change: ptrTo(lsproto.TextDocumentSyncKindIncremental),
|
|
Save: &lsproto.BooleanOrSaveOptions{
|
|
Boolean: ptrTo(true),
|
|
},
|
|
},
|
|
},
|
|
HoverProvider: &lsproto.BooleanOrHoverOptions{
|
|
Boolean: ptrTo(true),
|
|
},
|
|
DefinitionProvider: &lsproto.BooleanOrDefinitionOptions{
|
|
Boolean: ptrTo(true),
|
|
},
|
|
TypeDefinitionProvider: &lsproto.BooleanOrTypeDefinitionOptionsOrTypeDefinitionRegistrationOptions{
|
|
Boolean: ptrTo(true),
|
|
},
|
|
ReferencesProvider: &lsproto.BooleanOrReferenceOptions{
|
|
Boolean: ptrTo(true),
|
|
},
|
|
ImplementationProvider: &lsproto.BooleanOrImplementationOptionsOrImplementationRegistrationOptions{
|
|
Boolean: ptrTo(true),
|
|
},
|
|
DiagnosticProvider: &lsproto.DiagnosticOptionsOrRegistrationOptions{
|
|
Options: &lsproto.DiagnosticOptions{
|
|
InterFileDependencies: true,
|
|
},
|
|
},
|
|
CompletionProvider: &lsproto.CompletionOptions{
|
|
TriggerCharacters: &ls.TriggerCharacters,
|
|
ResolveProvider: ptrTo(true),
|
|
// !!! other options
|
|
},
|
|
SignatureHelpProvider: &lsproto.SignatureHelpOptions{
|
|
TriggerCharacters: &[]string{"(", ","},
|
|
},
|
|
DocumentFormattingProvider: &lsproto.BooleanOrDocumentFormattingOptions{
|
|
Boolean: ptrTo(true),
|
|
},
|
|
DocumentRangeFormattingProvider: &lsproto.BooleanOrDocumentRangeFormattingOptions{
|
|
Boolean: ptrTo(true),
|
|
},
|
|
DocumentOnTypeFormattingProvider: &lsproto.DocumentOnTypeFormattingOptions{
|
|
FirstTriggerCharacter: "{",
|
|
MoreTriggerCharacter: &[]string{"}", ";", "\n"},
|
|
},
|
|
WorkspaceSymbolProvider: &lsproto.BooleanOrWorkspaceSymbolOptions{
|
|
Boolean: ptrTo(true),
|
|
},
|
|
DocumentSymbolProvider: &lsproto.BooleanOrDocumentSymbolOptions{
|
|
Boolean: ptrTo(true),
|
|
},
|
|
RenameProvider: &lsproto.BooleanOrRenameOptions{
|
|
Boolean: ptrTo(true),
|
|
},
|
|
DocumentHighlightProvider: &lsproto.BooleanOrDocumentHighlightOptions{
|
|
Boolean: ptrTo(true),
|
|
},
|
|
},
|
|
}
|
|
|
|
return response, nil
|
|
}
|
|
|
|
func (s *Server) handleInitialized(ctx context.Context, params *lsproto.InitializedParams) error {
|
|
if shouldEnableWatch(s.initializeParams) {
|
|
s.watchEnabled = true
|
|
}
|
|
|
|
cwd := s.cwd
|
|
if s.initializeParams.Capabilities != nil &&
|
|
s.initializeParams.Capabilities.Workspace != nil &&
|
|
s.initializeParams.Capabilities.Workspace.WorkspaceFolders != nil &&
|
|
ptrIsTrue(s.initializeParams.Capabilities.Workspace.WorkspaceFolders) &&
|
|
s.initializeParams.WorkspaceFolders != nil &&
|
|
s.initializeParams.WorkspaceFolders.WorkspaceFolders != nil &&
|
|
len(*s.initializeParams.WorkspaceFolders.WorkspaceFolders) == 1 {
|
|
cwd = lsproto.DocumentUri((*s.initializeParams.WorkspaceFolders.WorkspaceFolders)[0].Uri).FileName()
|
|
} else if s.initializeParams.RootUri.DocumentUri != nil {
|
|
cwd = s.initializeParams.RootUri.DocumentUri.FileName()
|
|
} else if s.initializeParams.RootPath != nil && s.initializeParams.RootPath.String != nil {
|
|
cwd = *s.initializeParams.RootPath.String
|
|
}
|
|
if !tspath.PathIsAbsolute(cwd) {
|
|
cwd = s.cwd
|
|
}
|
|
|
|
s.session = project.NewSession(&project.SessionInit{
|
|
Options: &project.SessionOptions{
|
|
CurrentDirectory: cwd,
|
|
DefaultLibraryPath: s.defaultLibraryPath,
|
|
TypingsLocation: s.typingsLocation,
|
|
PositionEncoding: s.positionEncoding,
|
|
WatchEnabled: s.watchEnabled,
|
|
LoggingEnabled: true,
|
|
DebounceDelay: 500 * time.Millisecond,
|
|
},
|
|
FS: s.fs,
|
|
Logger: s.logger,
|
|
Client: s,
|
|
NpmExecutor: s,
|
|
ParseCache: s.parseCache,
|
|
})
|
|
// !!! temporary; remove when we have `handleDidChangeConfiguration`/implicit project config support
|
|
if s.compilerOptionsForInferredProjects != nil {
|
|
s.session.DidChangeCompilerOptionsForInferredProjects(ctx, s.compilerOptionsForInferredProjects)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *Server) handleShutdown(ctx context.Context, params any, _ *lsproto.RequestMessage) (lsproto.ShutdownResponse, error) {
|
|
s.session.Close()
|
|
return lsproto.ShutdownResponse{}, nil
|
|
}
|
|
|
|
func (s *Server) handleExit(ctx context.Context, params any) error {
|
|
return io.EOF
|
|
}
|
|
|
|
func (s *Server) handleDidOpen(ctx context.Context, params *lsproto.DidOpenTextDocumentParams) error {
|
|
s.session.DidOpenFile(ctx, params.TextDocument.Uri, params.TextDocument.Version, params.TextDocument.Text, params.TextDocument.LanguageId)
|
|
return nil
|
|
}
|
|
|
|
func (s *Server) handleDidChange(ctx context.Context, params *lsproto.DidChangeTextDocumentParams) error {
|
|
s.session.DidChangeFile(ctx, params.TextDocument.Uri, params.TextDocument.Version, params.ContentChanges)
|
|
return nil
|
|
}
|
|
|
|
func (s *Server) handleDidSave(ctx context.Context, params *lsproto.DidSaveTextDocumentParams) error {
|
|
s.session.DidSaveFile(ctx, params.TextDocument.Uri)
|
|
return nil
|
|
}
|
|
|
|
func (s *Server) handleDidClose(ctx context.Context, params *lsproto.DidCloseTextDocumentParams) error {
|
|
s.session.DidCloseFile(ctx, params.TextDocument.Uri)
|
|
return nil
|
|
}
|
|
|
|
func (s *Server) handleDidChangeWatchedFiles(ctx context.Context, params *lsproto.DidChangeWatchedFilesParams) error {
|
|
s.session.DidChangeWatchedFiles(ctx, params.Changes)
|
|
return nil
|
|
}
|
|
|
|
func (s *Server) handleSetTrace(ctx context.Context, params *lsproto.SetTraceParams) error {
|
|
switch params.Value {
|
|
case "verbose":
|
|
s.logger.SetVerbose(true)
|
|
case "messages":
|
|
s.logger.SetVerbose(false)
|
|
case "off":
|
|
// !!! logging cannot be completely turned off for now
|
|
s.logger.SetVerbose(false)
|
|
default:
|
|
return fmt.Errorf("unknown trace value: %s", params.Value)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *Server) handleDocumentDiagnostic(ctx context.Context, ls *ls.LanguageService, params *lsproto.DocumentDiagnosticParams) (lsproto.DocumentDiagnosticResponse, error) {
|
|
return ls.ProvideDiagnostics(ctx, params.TextDocument.Uri)
|
|
}
|
|
|
|
func (s *Server) handleHover(ctx context.Context, ls *ls.LanguageService, params *lsproto.HoverParams) (lsproto.HoverResponse, error) {
|
|
return ls.ProvideHover(ctx, params.TextDocument.Uri, params.Position)
|
|
}
|
|
|
|
func (s *Server) handleSignatureHelp(ctx context.Context, languageService *ls.LanguageService, params *lsproto.SignatureHelpParams) (lsproto.SignatureHelpResponse, error) {
|
|
return languageService.ProvideSignatureHelp(
|
|
ctx,
|
|
params.TextDocument.Uri,
|
|
params.Position,
|
|
params.Context,
|
|
s.initializeParams.Capabilities.TextDocument.SignatureHelp,
|
|
&ls.UserPreferences{},
|
|
)
|
|
}
|
|
|
|
func (s *Server) handleDefinition(ctx context.Context, ls *ls.LanguageService, params *lsproto.DefinitionParams) (lsproto.DefinitionResponse, error) {
|
|
return ls.ProvideDefinition(ctx, params.TextDocument.Uri, params.Position)
|
|
}
|
|
|
|
func (s *Server) handleTypeDefinition(ctx context.Context, ls *ls.LanguageService, params *lsproto.TypeDefinitionParams) (lsproto.TypeDefinitionResponse, error) {
|
|
return ls.ProvideTypeDefinition(ctx, params.TextDocument.Uri, params.Position)
|
|
}
|
|
|
|
func (s *Server) handleReferences(ctx context.Context, ls *ls.LanguageService, params *lsproto.ReferenceParams) (lsproto.ReferencesResponse, error) {
|
|
// findAllReferences
|
|
return ls.ProvideReferences(ctx, params)
|
|
}
|
|
|
|
func (s *Server) handleImplementations(ctx context.Context, ls *ls.LanguageService, params *lsproto.ImplementationParams) (lsproto.ImplementationResponse, error) {
|
|
// goToImplementation
|
|
return ls.ProvideImplementations(ctx, params)
|
|
}
|
|
|
|
func (s *Server) handleCompletion(ctx context.Context, languageService *ls.LanguageService, params *lsproto.CompletionParams) (lsproto.CompletionResponse, error) {
|
|
// !!! get user preferences
|
|
return languageService.ProvideCompletion(
|
|
ctx,
|
|
params.TextDocument.Uri,
|
|
params.Position,
|
|
params.Context,
|
|
getCompletionClientCapabilities(s.initializeParams),
|
|
&ls.UserPreferences{
|
|
IncludeCompletionsForModuleExports: core.TSTrue,
|
|
IncludeCompletionsForImportStatements: core.TSTrue,
|
|
})
|
|
}
|
|
|
|
func (s *Server) handleCompletionItemResolve(ctx context.Context, params *lsproto.CompletionItem, reqMsg *lsproto.RequestMessage) (lsproto.CompletionResolveResponse, error) {
|
|
data, err := ls.GetCompletionItemData(params)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
languageService, err := s.session.GetLanguageService(ctx, ls.FileNameToDocumentURI(data.FileName))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer s.recover(reqMsg)
|
|
return languageService.ResolveCompletionItem(
|
|
ctx,
|
|
params,
|
|
data,
|
|
getCompletionClientCapabilities(s.initializeParams),
|
|
&ls.UserPreferences{
|
|
IncludeCompletionsForModuleExports: core.TSTrue,
|
|
IncludeCompletionsForImportStatements: core.TSTrue,
|
|
},
|
|
)
|
|
}
|
|
|
|
func (s *Server) handleDocumentFormat(ctx context.Context, ls *ls.LanguageService, params *lsproto.DocumentFormattingParams) (lsproto.DocumentFormattingResponse, error) {
|
|
return ls.ProvideFormatDocument(
|
|
ctx,
|
|
params.TextDocument.Uri,
|
|
params.Options,
|
|
)
|
|
}
|
|
|
|
func (s *Server) handleDocumentRangeFormat(ctx context.Context, ls *ls.LanguageService, params *lsproto.DocumentRangeFormattingParams) (lsproto.DocumentRangeFormattingResponse, error) {
|
|
return ls.ProvideFormatDocumentRange(
|
|
ctx,
|
|
params.TextDocument.Uri,
|
|
params.Options,
|
|
params.Range,
|
|
)
|
|
}
|
|
|
|
func (s *Server) handleDocumentOnTypeFormat(ctx context.Context, ls *ls.LanguageService, params *lsproto.DocumentOnTypeFormattingParams) (lsproto.DocumentOnTypeFormattingResponse, error) {
|
|
return ls.ProvideFormatDocumentOnType(
|
|
ctx,
|
|
params.TextDocument.Uri,
|
|
params.Options,
|
|
params.Position,
|
|
params.Ch,
|
|
)
|
|
}
|
|
|
|
func (s *Server) handleWorkspaceSymbol(ctx context.Context, params *lsproto.WorkspaceSymbolParams, reqMsg *lsproto.RequestMessage) (lsproto.WorkspaceSymbolResponse, error) {
|
|
snapshot, release := s.session.Snapshot()
|
|
defer release()
|
|
defer s.recover(reqMsg)
|
|
programs := core.Map(snapshot.ProjectCollection.Projects(), (*project.Project).GetProgram)
|
|
return ls.ProvideWorkspaceSymbols(ctx, programs, snapshot.Converters(), params.Query)
|
|
}
|
|
|
|
func (s *Server) handleDocumentSymbol(ctx context.Context, ls *ls.LanguageService, params *lsproto.DocumentSymbolParams) (lsproto.DocumentSymbolResponse, error) {
|
|
return ls.ProvideDocumentSymbols(ctx, params.TextDocument.Uri)
|
|
}
|
|
|
|
func (s *Server) handleRename(ctx context.Context, ls *ls.LanguageService, params *lsproto.RenameParams) (lsproto.RenameResponse, error) {
|
|
return ls.ProvideRename(ctx, params)
|
|
}
|
|
|
|
func (s *Server) handleDocumentHighlight(ctx context.Context, ls *ls.LanguageService, params *lsproto.DocumentHighlightParams) (lsproto.DocumentHighlightResponse, error) {
|
|
return ls.ProvideDocumentHighlights(ctx, params.TextDocument.Uri, params.Position)
|
|
}
|
|
|
|
func (s *Server) Log(msg ...any) {
|
|
fmt.Fprintln(s.stderr, msg...)
|
|
}
|
|
|
|
// !!! temporary; remove when we have `handleDidChangeConfiguration`/implicit project config support
|
|
func (s *Server) SetCompilerOptionsForInferredProjects(ctx context.Context, options *core.CompilerOptions) {
|
|
s.compilerOptionsForInferredProjects = options
|
|
if s.session != nil {
|
|
s.session.DidChangeCompilerOptionsForInferredProjects(ctx, options)
|
|
}
|
|
}
|
|
|
|
// NpmInstall implements ata.NpmExecutor
|
|
func (s *Server) NpmInstall(cwd string, args []string) ([]byte, error) {
|
|
cmd := exec.Command("npm", args...)
|
|
cmd.Dir = cwd
|
|
return cmd.Output()
|
|
}
|
|
|
|
func isBlockingMethod(method lsproto.Method) bool {
|
|
switch method {
|
|
case lsproto.MethodInitialize,
|
|
lsproto.MethodInitialized,
|
|
lsproto.MethodTextDocumentDidOpen,
|
|
lsproto.MethodTextDocumentDidChange,
|
|
lsproto.MethodTextDocumentDidSave,
|
|
lsproto.MethodTextDocumentDidClose,
|
|
lsproto.MethodWorkspaceDidChangeWatchedFiles:
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
func ptrTo[T any](v T) *T {
|
|
return &v
|
|
}
|
|
|
|
func ptrIsTrue(v *bool) bool {
|
|
if v == nil {
|
|
return false
|
|
}
|
|
return *v
|
|
}
|
|
|
|
func shouldEnableWatch(params *lsproto.InitializeParams) bool {
|
|
if params == nil || params.Capabilities == nil || params.Capabilities.Workspace == nil {
|
|
return false
|
|
}
|
|
return params.Capabilities.Workspace.DidChangeWatchedFiles != nil &&
|
|
ptrIsTrue(params.Capabilities.Workspace.DidChangeWatchedFiles.DynamicRegistration)
|
|
}
|
|
|
|
func getCompletionClientCapabilities(params *lsproto.InitializeParams) *lsproto.CompletionClientCapabilities {
|
|
if params == nil || params.Capabilities == nil || params.Capabilities.TextDocument == nil {
|
|
return nil
|
|
}
|
|
return params.Capabilities.TextDocument.Completion
|
|
}
|