package lsp import ( "context" "errors" "fmt" "io" "iter" "math/rand/v2" "runtime/debug" "slices" "strings" "sync" "sync/atomic" "time" "github.com/microsoft/typescript-go/internal/api" "github.com/microsoft/typescript-go/internal/collections" "github.com/microsoft/typescript-go/internal/core" "github.com/microsoft/typescript-go/internal/diagnostics" "github.com/microsoft/typescript-go/internal/fswatch" "github.com/microsoft/typescript-go/internal/json" "github.com/microsoft/typescript-go/internal/jsonrpc" "github.com/microsoft/typescript-go/internal/locale" "github.com/microsoft/typescript-go/internal/ls" "github.com/microsoft/typescript-go/internal/ls/lsconv" "github.com/microsoft/typescript-go/internal/ls/lsutil" "github.com/microsoft/typescript-go/internal/lsp/lsproto" "github.com/microsoft/typescript-go/internal/lsp/lspwatcher" "github.com/microsoft/typescript-go/internal/pprof" "github.com/microsoft/typescript-go/internal/project" "github.com/microsoft/typescript-go/internal/project/ata" "github.com/microsoft/typescript-go/internal/tspath" "github.com/microsoft/typescript-go/internal/vfs" "golang.org/x/sync/errgroup" ) type ServerOptions struct { In Reader Out Writer Err io.Writer Cwd string FS vfs.FS DefaultLibraryPath string TypingsLocation string ParseCache *project.ParseCache NpmInstall func(cwd string, args []string) ([]byte, error) ProgressDelay time.Duration // delay before showing progress UI; 0 means no delay SetParentProcessID func(parentPID int) } func NewServer(opts *ServerOptions) *Server { if opts.Cwd == "" { panic("Cwd is required") } s := &Server{ r: opts.In, w: opts.Out, stderr: opts.Err, requestQueue: newDynamicQueue[*lsproto.RequestMessage](), outgoingQueue: newDynamicQueue[*lsproto.Message](), pendingClientRequests: make(map[jsonrpc.ID]pendingClientRequest), pendingServerRequests: make(map[jsonrpc.ID]chan *lsproto.ResponseMessage), cwd: opts.Cwd, fs: opts.FS, defaultLibraryPath: opts.DefaultLibraryPath, typingsLocation: opts.TypingsLocation, parseCache: opts.ParseCache, npmInstall: opts.NpmInstall, startWatchdog: opts.SetParentProcessID, initComplete: make(chan struct{}), progressDelay: opts.ProgressDelay, } s.logger = newLogger(s) return s } var ( fileRenameFilters = []*lsproto.FileOperationFilter{ { Scheme: new("file"), Pattern: &lsproto.FileOperationPattern{ Glob: "**/*.{ts,tsx,js,jsx,cts,cjs,mts,mjs,json}", }, }, } _ 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 { if errors.Is(err, lsproto.ErrorCodeInvalidParams) { return req, fmt.Errorf("%w: %w", lsproto.ErrorCodeInvalidParams, err) } return nil, fmt.Errorf("%w: %w", lsproto.ErrorCodeInvalidRequest, 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 backgroundCtx context.Context stderr io.Writer logger *logger initStarted atomic.Bool clientSeq atomic.Int32 requestQueue *dynamicQueue[*lsproto.RequestMessage] outgoingQueue *dynamicQueue[*lsproto.Message] pendingClientRequests map[jsonrpc.ID]pendingClientRequest pendingClientRequestsMu sync.Mutex pendingServerRequests map[jsonrpc.ID]chan *lsproto.ResponseMessage pendingServerRequestsMu sync.Mutex cwd string fs vfs.FS defaultLibraryPath string typingsLocation string initializeParams *lsproto.InitializeParams initializationOptions *lsproto.InitializationOptions clientCapabilities lsproto.ResolvedClientCapabilities positionEncoding lsproto.PositionEncodingKind locale locale.Locale watchEnabled bool telemetryEnabled bool watcherID atomic.Uint32 watchers collections.SyncSet[project.WatcherID] // builtinWatcher is non-nil when the server is running its own // in-process file watcher instead of using LSP-based watching. It // is enabled when the client lacks DynamicRegistration for // workspace/didChangeWatchedFiles and the builtin watcher backend // supports efficient recursive watching (Windows or FSEvents). builtinWatcher *lspwatcher.Watcher lastRequestTimeMs atomic.Int64 session *project.Session // apiSessions holds active API sessions keyed by their ID apiSessions map[string]*api.Session apiSessionsMu sync.Mutex // Test options for initializing session client project.Client // initComplete is closed when handleInitialized completes. // Used by tests to wait for full initialization. initComplete chan struct{} // !!! 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 npmInstall func(cwd string, args []string) ([]byte, error) cpuProfiler pprof.CPUProfiler progressDelay time.Duration projectProgress *projectLoadingProgress startWatchdog func(parentPID int) } func (s *Server) Session() *project.Session { return s.session } // InitComplete returns a channel that is closed when the server has finished // processing the initialized notification, including the initial configuration // exchange with the client. func (s *Server) InitComplete() <-chan struct{} { return s.initComplete } // WatchFiles implements project.Client. func (s *Server) WatchFiles(ctx context.Context, id project.WatcherID, watchers []*lsproto.FileSystemWatcher) error { if s.builtinWatcher != nil { if err := s.builtinWatcher.WatchFiles(string(id), watchers); err != nil { return fmt.Errorf("failed to register file watcher: %w", err) } s.watchers.Add(id) return nil } _, err := sendClientRequest(ctx, s, lsproto.ClientRegisterCapabilityInfo, &lsproto.RegistrationParams{ Registrations: []*lsproto.Registration{ { Id: string(id), RegisterOptions: &lsproto.RegisterOptions{ WorkspaceDidChangeWatchedFiles: &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.builtinWatcher != nil { if !s.watchers.Has(id) { return fmt.Errorf("no file watcher exists with ID %s", id) } if err := s.builtinWatcher.UnwatchFiles(string(id)); err != nil { return fmt.Errorf("failed to unregister file watcher: %w", err) } s.watchers.Delete(id) return nil } if s.watchers.Has(id) { _, err := sendClientRequest(ctx, s, lsproto.ClientUnregisterCapabilityInfo, &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.clientCapabilities.Workspace.Diagnostics.RefreshSupport { return nil } if err := ctx.Err(); err != nil { return err } // Fire-and-forget: the client always returns null, and waiting for the response // can cause the server to hang if the client is slow or unresponsive. // Any response from the client will be silently ignored by the read loop. if err := sendClientRequestFireAndForget(s, lsproto.WorkspaceDiagnosticRefreshInfo, lsproto.NoParams{}); err != nil { return fmt.Errorf("failed to refresh diagnostics: %w", err) } return nil } // PublishDiagnostics implements project.Client. func (s *Server) PublishDiagnostics(ctx context.Context, params *lsproto.PublishDiagnosticsParams) error { return sendNotification(s, lsproto.TextDocumentPublishDiagnosticsInfo, params) } // SendTelemetry implements project.Client. func (s *Server) SendTelemetry(ctx context.Context, telemetry lsproto.TelemetryEvent) error { if !s.telemetryEnabled { panic("SendTelemetry called with telemetry disabled") } return sendNotification(s, lsproto.TelemetryEventInfo, telemetry) } // IsActive implements project.Client. func (s *Server) IsActive() bool { last := s.lastRequestTimeMs.Load() return last == 0 || time.Since(time.UnixMilli(last)) <= time.Minute } func (s *Server) RefreshInlayHints(ctx context.Context) error { if !s.clientCapabilities.Workspace.InlayHint.RefreshSupport { return nil } if err := sendClientRequestFireAndForget(s, lsproto.WorkspaceInlayHintRefreshInfo, lsproto.NoParams{}); err != nil { return fmt.Errorf("failed to refresh inlay hints: %w", err) } return nil } func (s *Server) RefreshCodeLens(ctx context.Context) error { if !s.clientCapabilities.Workspace.CodeLens.RefreshSupport { return nil } if err := sendClientRequestFireAndForget(s, lsproto.WorkspaceCodeLensRefreshInfo, lsproto.NoParams{}); err != nil { return fmt.Errorf("failed to refresh code lens: %w", err) } return nil } // ProgressStart implements project.Client. func (s *Server) ProgressStart(message *diagnostics.Message, args ...any) { if s.projectProgress != nil { s.projectProgress.start(message, args...) } } // ProgressFinish implements project.Client. func (s *Server) ProgressFinish(message *diagnostics.Message, args ...any) { if s.projectProgress != nil { s.projectProgress.finish(message, args...) } } func (s *Server) RequestConfiguration(ctx context.Context) (lsutil.UserPreferences, error) { caps := lsproto.GetClientCapabilities(ctx) if !caps.Workspace.Configuration { if opts := s.initializationOptions; opts.UserPreferences != nil { userPrefs := *opts.UserPreferences s.logger.Logf( "received formatting options from initialization: %T\n%+v", userPrefs, userPrefs, ) if config, ok := userPrefs.(map[string]any); ok { return lsutil.ParseUserPreferences(map[string]any{"js/ts": config}), nil } } return lsutil.NewDefaultUserPreferences(), nil } configs, err := sendClientRequest(ctx, s, lsproto.WorkspaceConfigurationInfo, &lsproto.ConfigurationParams{ Items: []*lsproto.ConfigurationItem{ { Section: new("js/ts"), }, { Section: new("typescript"), }, { Section: new("javascript"), }, { Section: new("editor"), }, }, }) if err != nil { return lsutil.UserPreferences{}, fmt.Errorf("configure request failed: %w", err) } configMap := map[string]any{} for i, config := range configs { switch i { case 0: configMap["js/ts"] = config case 1: configMap["typescript"] = config case 2: configMap["javascript"] = config case 3: configMap["editor"] = config } } s.logger.Logf( "received options from workspace/configuration request:\njs/ts: %+v\n\ntypescript: %+v\n\njavascript: %+v\n\neditor: %+v\n", configMap["js/ts"], configMap["typescript"], configMap["javascript"], configMap["editor"], ) return lsutil.ParseUserPreferences(configMap), nil } func (s *Server) Run(ctx context.Context) error { g, ctx := errgroup.WithContext(ctx) s.backgroundCtx = 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.ErrorCodeInvalidRequest) || errors.Is(err, lsproto.ErrorCodeInvalidParams) { var id *jsonrpc.ID if errors.Is(err, lsproto.ErrorCodeInvalidParams) { if msg != nil && msg.Kind == jsonrpc.MessageKindRequest { id = msg.AsRequest().ID } } if err := s.sendError(id, err); err != nil { return err } continue } return err } if s.initializeParams == nil && msg.Kind == jsonrpc.MessageKindRequest { req := msg.AsRequest() if req.Method == lsproto.MethodInitialize { params, err := lsproto.UnmarshalParams[*lsproto.InitializeParams](req) if err != nil { if err := s.sendError(req.ID, err); err != nil { return err } continue } resp, err := s.handleInitialize(ctx, params, req) if err != nil { return err } if err := s.sendResult(req.ID, resp); err != nil { return err } } else { if err := s.sendError(req.ID, lsproto.ErrorCodeServerNotInitialized); err != nil { return err } } continue } if msg.Kind == jsonrpc.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 { if params, err := lsproto.UnmarshalParams[*lsproto.CancelParams](req); err == nil && params != nil { s.cancelRequest(params.Id) } } else { if err := s.requestQueue.Put(ctx, req); err != nil { return err } } } } } 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.WithCancelCause(ctx) defer lspExit(nil) for { req, err := s.requestQueue.Get(ctx) if err != nil { return err } s.lastRequestTimeMs.Store(time.Now().UnixMilli()) requestCtx := locale.WithLocale(ctx, s.locale) var cancel context.CancelFunc if req.ID != nil { 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() } handleError := func(err error) { if errors.Is(err, context.Canceled) { if err := s.sendError(req.ID, lsproto.ErrorCodeRequestCancelled); err != nil { lspExit(err) } } else if errors.Is(err, io.EOF) { lspExit(nil) } else { if err := s.sendError(req.ID, err); err != nil { lspExit(err) } } } removeRequest := func() { if req.ID != nil { defer cancel() s.pendingClientRequestsMu.Lock() defer s.pendingClientRequestsMu.Unlock() delete(s.pendingClientRequests, *req.ID) } } if doAsyncWork, err := s.handleRequestOrNotification(requestCtx, req); err != nil { handleError(err) removeRequest() } else if doAsyncWork != nil { go func() { if lsError := doAsyncWork(); lsError != nil { handleError(lsError) } removeRequest() }() } else { removeRequest() } } } func (s *Server) writeLoop(ctx context.Context) error { for { msg, err := s.outgoingQueue.Get(ctx) if err != nil { return err } if err := s.w.Write(msg); err != nil { return fmt.Errorf("failed to write message: %w", err) } } } // WARNING: this should only be called in the async portion of a request handler, // otherwise a deadlock can occur. func sendClientRequest[Req, Resp any](ctx context.Context, s *Server, info lsproto.RequestInfo[Req, Resp], params Req) (Resp, error) { id := jsonrpc.NewIDString(fmt.Sprintf("ts%d", s.clientSeq.Add(1))) req := info.NewRequestMessage(id, params) responseChan := make(chan *lsproto.ResponseMessage, 1) s.pendingServerRequestsMu.Lock() s.pendingServerRequests[*id] = responseChan s.pendingServerRequestsMu.Unlock() defer func() { s.pendingServerRequestsMu.Lock() defer s.pendingServerRequestsMu.Unlock() if respChan, ok := s.pendingServerRequests[*id]; ok { close(respChan) delete(s.pendingServerRequests, *id) } }() if err := s.send(req.Message()); err != nil { return *new(Resp), err } select { case <-ctx.Done(): return *new(Resp), ctx.Err() case resp := <-responseChan: if resp.Error != nil { return *new(Resp), fmt.Errorf("request failed: %s", resp.Error.String()) } return info.UnmarshalResult(resp.Result) } } // sendClientRequestFireAndForget sends a request to the client without waiting for a response. // The response, if any, will be silently ignored by the read loop since no pending channel is registered. // This means any error returned by the client will not be observed. Use only for requests where the // response value is not needed (e.g., the client always returns null). func sendClientRequestFireAndForget[Req, Resp any](s *Server, info lsproto.RequestInfo[Req, Resp], params Req) error { id := jsonrpc.NewIDString(fmt.Sprintf("ts%d", s.clientSeq.Add(1))) req := info.NewRequestMessage(id, params) return s.send(req.Message()) } func (s *Server) sendResult(id *jsonrpc.ID, result any) error { return s.sendResponse(&lsproto.ResponseMessage{ ID: id, Result: result, }) } type userFacingRequestFailedError string func (e userFacingRequestFailedError) Error() string { return string(e) } func (e userFacingRequestFailedError) Unwrap() error { return lsproto.ErrorCodeRequestFailed } func (s *Server) sendError(id *jsonrpc.ID, err error) error { // Do not send error response for notifications, // except for parse errors which may occur before determining if the message is a request or notification. if id == nil && !errors.Is(err, lsproto.ErrorCodeInvalidRequest) { s.logger.Errorf("error handling notification: %s", err) return nil } code := lsproto.ErrorCodeInternalError if errCode, ok := errors.AsType[lsproto.ErrorCode](err); ok { code = errCode } // TODO(jakebailey): error data return s.sendResponse(&lsproto.ResponseMessage{ ID: id, Error: &jsonrpc.ResponseError{ Code: int32(code), Message: err.Error(), }, }) } func sendNotification[Params any](s *Server, info lsproto.NotificationInfo[Params], params Params) error { return s.send(info.NewNotificationMessage(params).Message()) } func (s *Server) sendResponse(resp *lsproto.ResponseMessage) error { return s.send(resp.Message()) } // send writes a message to the outgoing queue, respecting context cancellation. func (s *Server) send(msg *lsproto.Message) error { return s.outgoingQueue.Put(s.backgroundCtx, msg) } // handleRequestOrNotification looks up the handler for the given request or notification, executes its synchronous work // and returns any asynchronous work as a function to be executed by the caller. func (s *Server) handleRequestOrNotification(ctx context.Context, req *lsproto.RequestMessage) (func() error, error) { ctx = lsproto.WithClientCapabilities(ctx, &s.clientCapabilities) if handler := handlers()[req.Method]; handler != nil { start := time.Now() doAsyncWork, err := handler(s, ctx, req) idStr := "" if req.ID != nil { idStr = " (" + req.ID.String() + ")" } if err != nil { if _, ok := errors.AsType[userFacingRequestFailedError](err); !ok { s.logger.Error("error handling method '", req.Method, "'", idStr, ": ", err) } else if !s.logger.IsTracing() { s.logger.Info("handled method '", req.Method, "'", idStr, " in ", time.Since(start)) } return nil, err } if doAsyncWork != nil { return func() error { // note: ctx.Err() has to be checked in the async work to allow async handlers to cleanup resources correctly asyncWorkErr := doAsyncWork() _, isUserFacing := errors.AsType[userFacingRequestFailedError](asyncWorkErr) isRealError := asyncWorkErr != nil && !isUserFacing if isRealError { s.logger.Info("error handling method '", req.Method, "'", idStr, " in ", time.Since(start)) } else if !s.logger.IsTracing() { s.logger.Info("handled method '", req.Method, "'", idStr, " in ", time.Since(start)) } return asyncWorkErr }, nil } if !s.logger.IsTracing() { s.logger.Info("handled method '", req.Method, "'", idStr, " in ", time.Since(start)) } return nil, nil } s.logger.Warn("unknown method '", req.Method, "'") if req.ID != nil { return nil, s.sendError(req.ID, lsproto.ErrorCodeInvalidRequest) } return nil, nil } // handlerMap maps LSP method to a handler function. The handler function executes any work that must be done synchronously // before other requests/notifications can be processed, and returns any additional work as a function to be executed // asynchronously after the synchronous work is complete. type handlerMap map[lsproto.Method]func(*Server, context.Context, *lsproto.RequestMessage) (func() error, 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.WorkspaceDidChangeConfigurationInfo, (*Server).handleDidChangeWorkspaceConfiguration) 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) registerNotificationHandler(handlers, lsproto.CustomSetLogVerbosityInfo, (*Server).handleSetLogVerbosity) registerRequestHandler(handlers, lsproto.WorkspaceWillRenameFilesInfo, (*Server).handleWillRenameFiles) registerLanguageServiceDocumentRequestHandler(handlers, lsproto.TextDocumentDiagnosticInfo, (*Server).handleDocumentDiagnostic) registerLanguageServiceDocumentRequestHandler(handlers, lsproto.TextDocumentHoverInfo, (*Server).handleHover) registerLanguageServiceDocumentRequestHandler(handlers, lsproto.TextDocumentDefinitionInfo, (*Server).handleDefinition) registerLanguageServiceDocumentRequestHandler(handlers, lsproto.CustomTextDocumentSourceDefinitionInfo, (*Server).handleSourceDefinition) registerLanguageServiceDocumentRequestHandler(handlers, lsproto.TextDocumentTypeDefinitionInfo, (*Server).handleTypeDefinition) 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.TextDocumentDocumentHighlightInfo, (*Server).handleDocumentHighlight) registerLanguageServiceDocumentRequestHandler(handlers, lsproto.CustomTextDocumentMultiDocumentHighlightInfo, (*Server).handleMultiDocumentHighlight) registerLanguageServiceDocumentRequestHandler(handlers, lsproto.TextDocumentSelectionRangeInfo, (*Server).handleSelectionRange) registerLanguageServiceDocumentRequestHandler(handlers, lsproto.TextDocumentInlayHintInfo, (*Server).handleInlayHint) registerLanguageServiceDocumentRequestHandler(handlers, lsproto.TextDocumentCodeLensInfo, (*Server).handleCodeLens) registerLanguageServiceDocumentRequestHandler(handlers, lsproto.TextDocumentCodeActionInfo, (*Server).handleCodeAction) registerLanguageServiceDocumentRequestHandler(handlers, lsproto.TextDocumentPrepareCallHierarchyInfo, (*Server).handlePrepareCallHierarchy) registerLanguageServiceDocumentRequestHandler(handlers, lsproto.TextDocumentFoldingRangeInfo, (*Server).handleFoldingRange) registerLanguageServiceDocumentRequestHandler(handlers, lsproto.TextDocumentPrepareRenameInfo, (*Server).handlePrepareRename) registerLanguageServiceDocumentRequestHandler(handlers, lsproto.TextDocumentLinkedEditingRangeInfo, (*Server).handleLinkedEditingRange) registerLanguageServiceWithAutoImportsRequestHandler(handlers, lsproto.TextDocumentCompletionInfo, (*Server).handleCompletion) registerLanguageServiceWithAutoImportsRequestHandler(handlers, lsproto.TextDocumentCodeActionInfo, (*Server).handleCodeAction) registerLanguageServiceDocumentRequestHandler(handlers, lsproto.TextDocumentVSOnAutoInsertInfo, (*Server).handleVSOnAutoInsert) registerMultiProjectReferenceRequestHandler(handlers, lsproto.TextDocumentReferencesInfo, (*ls.LanguageService).ProvideReferences) registerMultiProjectReferenceRequestHandler(handlers, lsproto.TextDocumentVSReferencesInfo, (*ls.LanguageService).ProvideVSReferences) registerRequestHandler(handlers, lsproto.TextDocumentRenameInfo, (*Server).handleRename) registerMultiProjectReferenceRequestHandler(handlers, lsproto.TextDocumentImplementationInfo, (*ls.LanguageService).ProvideImplementations) registerRequestHandler(handlers, lsproto.CallHierarchyIncomingCallsInfo, (*Server).handleCallHierarchyIncomingCalls) registerRequestHandler(handlers, lsproto.CallHierarchyOutgoingCallsInfo, (*Server).handleCallHierarchyOutgoingCalls) registerRequestHandler(handlers, lsproto.WorkspaceSymbolInfo, (*Server).handleWorkspaceSymbol) registerRequestHandler(handlers, lsproto.CompletionItemResolveInfo, (*Server).handleCompletionItemResolve) registerRequestHandler(handlers, lsproto.CodeLensResolveInfo, (*Server).handleCodeLensResolve) registerLanguageServiceDocumentRequestHandler(handlers, lsproto.TextDocumentSemanticTokensFullInfo, (*Server).handleSemanticTokensFull) registerLanguageServiceDocumentRequestHandler(handlers, lsproto.TextDocumentSemanticTokensRangeInfo, (*Server).handleSemanticTokensRange) // Developer/debugging commands registerRequestHandler(handlers, lsproto.CustomRunGCInfo, (*Server).handleRunGC) registerRequestHandler(handlers, lsproto.CustomSaveHeapProfileInfo, (*Server).handleSaveHeapProfile) registerRequestHandler(handlers, lsproto.CustomSaveAllocProfileInfo, (*Server).handleSaveAllocProfile) registerRequestHandler(handlers, lsproto.CustomStartCPUProfileInfo, (*Server).handleStartCPUProfile) registerRequestHandler(handlers, lsproto.CustomStopCPUProfileInfo, (*Server).handleStopCPUProfile) registerRequestHandler(handlers, lsproto.CustomInitializeAPISessionInfo, (*Server).handleInitializeAPISession) registerRequestHandler(handlers, lsproto.CustomProjectInfoInfo, (*Server).handleProjectInfo) 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) (func() error, error) { if s.session == nil && req.Method != lsproto.MethodInitialized { return nil, lsproto.ErrorCodeServerNotInitialized } params, err := lsproto.UnmarshalParams[Req](req) if err != nil { return nil, err } if err := fn(s, ctx, params); err != nil { return nil, err } return nil, 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) (func() error, error) { if s.session == nil && req.Method != lsproto.MethodInitialize { return nil, lsproto.ErrorCodeServerNotInitialized } params, err := lsproto.UnmarshalParams[Req](req) if err != nil { return nil, err } resp, err := fn(s, ctx, params, req) if err != nil { return nil, err } if ctx.Err() != nil { return nil, ctx.Err() } return nil, s.sendResult(req.ID, resp) } } 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) (func() error, error) { params, err := lsproto.UnmarshalParams[Req](req) if err != nil { return nil, err } ls, err := s.session.GetLanguageService(ctx, params.TextDocumentURI()) if err != nil { return nil, err } return func() error { defer s.recover(req) resp, lsErr := fn(s, ctx, ls, params) // After any language service request, check if new global diagnostics were // discovered during checking and push updated tsconfig diagnostics if so. s.session.EnqueuePublishGlobalDiagnostics() if lsErr != nil { return lsErr } if ctx.Err() != nil { return ctx.Err() } return s.sendResult(req.ID, resp) }, nil } } func registerLanguageServiceWithAutoImportsRequestHandler[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) (func() error, error) { params, err := lsproto.UnmarshalParams[Req](req) if err != nil { return nil, err } return s.session.WithLanguageServiceAndSnapshot(ctx, params.TextDocumentURI(), func(languageService *ls.LanguageService, snapshot *project.Snapshot) (func() error, error) { return func() error { defer s.recover(req) resp, lsErr := fn(s, ctx, languageService, params) if errors.Is(lsErr, ls.ErrNeedsAutoImports) { languageService, lsErr = s.session.GetLanguageServiceWithAutoImports(ctx, snapshot, params.TextDocumentURI()) if lsErr != nil { return lsErr } if ctx.Err() != nil { return ctx.Err() } resp, lsErr = fn(s, ctx, languageService, params) if errors.Is(lsErr, ls.ErrNeedsAutoImports) { panic(info.Method + " returned ErrNeedsAutoImports even after enabling auto imports") } } if lsErr != nil { return lsErr } if ctx.Err() != nil { return ctx.Err() } return s.sendResult(req.ID, resp) }, nil }) } } func registerMultiProjectReferenceRequestHandler[Req lsproto.HasTextDocumentPosition, Resp any]( handlers handlerMap, info lsproto.RequestInfo[Req, Resp], fn func(*ls.LanguageService, context.Context, Req, ls.CrossProjectOrchestrator) (Resp, error), ) { handlers[info.Method] = func(s *Server, ctx context.Context, req *lsproto.RequestMessage) (func() error, error) { params, err := lsproto.UnmarshalParams[Req](req) if err != nil { return nil, err } // !!! sheetal: multiple projects that contain the file through symlinks defaultLs, orchestrator, err := s.getLanguageServiceAndCrossProjectOrchestrator(ctx, params.TextDocumentURI(), req) if err != nil { return nil, err } return func() error { defer s.recover(req) resp, lsErr := fn(defaultLs, ctx, params, orchestrator) if lsErr != nil { return lsErr } if ctx.Err() != nil { return ctx.Err() } return s.sendResult(req.ID, resp) }, nil } } type crossProjectOrchestrator struct { server *Server req *lsproto.RequestMessage defaultProject *project.Project allProjects []ls.Project } var _ ls.CrossProjectOrchestrator = (*crossProjectOrchestrator)(nil) func (c *crossProjectOrchestrator) GetDefaultProject() ls.Project { return c.defaultProject } func (c *crossProjectOrchestrator) GetAllProjectsForInitialRequest() []ls.Project { return c.allProjects } func (c *crossProjectOrchestrator) GetLanguageServiceForProjectWithFile(ctx context.Context, p ls.Project, uri lsproto.DocumentUri) *ls.LanguageService { return c.server.session.GetLanguageServiceForProjectWithFile(ctx, p.(*project.Project), uri) } func (c *crossProjectOrchestrator) GetProjectsForFile(ctx context.Context, uri lsproto.DocumentUri) ([]ls.Project, error) { return c.server.session.GetProjectsForFile(ctx, uri) } func (c *crossProjectOrchestrator) GetProjectsLoadingProjectTree(ctx context.Context, requestedProjectTrees *collections.Set[tspath.Path]) iter.Seq[ls.Project] { return func(yield func(ls.Project) bool) { c.server.session.WithSnapshotLoadingProjectTree(ctx, requestedProjectTrees, func(snapshot *project.Snapshot) { for _, p := range snapshot.ProjectCollection.Projects() { if !yield(p) { return } } }) } } func (s *Server) getLanguageServiceAndCrossProjectOrchestrator(ctx context.Context, uri lsproto.DocumentUri, req *lsproto.RequestMessage) (*ls.LanguageService, ls.CrossProjectOrchestrator, error) { defaultProject, defaultLs, allProjects, err := s.session.GetLanguageServiceAndProjectsForFile(ctx, uri) var orchestrator ls.CrossProjectOrchestrator if err == nil { orchestrator = &crossProjectOrchestrator{s, req, defaultProject, allProjects} } return defaultLs, orchestrator, err } func (s *Server) recover(req *lsproto.RequestMessage) { if r := recover(); r != nil { stack := debug.Stack() s.logger.Errorf("panic handling request %s: %v\n%s", req.Method, r, string(stack)) if req.ID != nil { _ = s.sendError(req.ID, fmt.Errorf("%w: panic handling request %s: %v", lsproto.ErrorCodeInternalError, req.Method, r)) } else { s.logger.Error("unhandled panic in notification", req.Method, r) } if s.telemetryEnabled { _ = sendNotification(s, lsproto.TelemetryEventInfo, lsproto.TelemetryEvent{ RequestFailureTelemetryEvent: &lsproto.RequestFailureTelemetryEvent{ Properties: &lsproto.RequestFailureTelemetryProperties{ ErrorCode: lsproto.ErrorCodeInternalError.String(), RequestMethod: strings.ReplaceAll(string(req.Method), "/", "."), Stack: sanitizeStackTrace(string(stack)), }, }, }) } } } func (s *Server) handleInitialize(ctx context.Context, params *lsproto.InitializeParams, _ *lsproto.RequestMessage) (lsproto.InitializeResponse, error) { if s.initializeParams != nil { return nil, lsproto.ErrorCodeInvalidRequest } s.initStarted.Store(true) s.initializeParams = params // The spec types initializationOptions as nullable; treat both null and an // absent value as empty options so the rest of the server can read fields // off s.initializationOptions without nil-checking the container. if params.InitializationOptions != nil && params.InitializationOptions.InitializationOptions != nil { s.initializationOptions = params.InitializationOptions.InitializationOptions } else { s.initializationOptions = &lsproto.InitializationOptions{} } if s.initializationOptions.LogVerbosity != nil { if v := *s.initializationOptions.LogVerbosity; isValidLogVerbosity(v) { s.logger.SetVerbosity(v) } } s.clientCapabilities = params.Capabilities.Resolve() if s.clientCapabilities.Window.WorkDoneProgress { s.projectProgress = newProjectLoadingProgress(s, s.progressDelay) } capabilitiesJSON, err := json.MarshalIndent(&s.clientCapabilities, "", "\t") if err != nil { return nil, err } s.logger.Info("Resolved client capabilities: " + string(capabilitiesJSON)) s.positionEncoding = lsproto.PositionEncodingKindUTF16 if slices.Contains(s.clientCapabilities.General.PositionEncodings, lsproto.PositionEncodingKindUTF8) { s.positionEncoding = lsproto.PositionEncodingKindUTF8 } if s.initializeParams.Locale != nil { s.locale, _ = locale.Parse(*s.initializeParams.Locale) } if s.startWatchdog != nil && params.ProcessId.Integer != nil { s.startWatchdog(int(*params.ProcessId.Integer)) } response := &lsproto.InitializeResult{ ServerInfo: &lsproto.ServerInfo{ Name: "typescript-go", Version: new(core.Version()), }, Capabilities: &lsproto.ServerCapabilities{ PositionEncoding: new(s.positionEncoding), TextDocumentSync: &lsproto.TextDocumentSyncOptionsOrKind{ Options: &lsproto.TextDocumentSyncOptions{ OpenClose: new(true), Change: new(lsproto.TextDocumentSyncKindIncremental), Save: &lsproto.BooleanOrSaveOptions{ Boolean: new(true), }, }, }, HoverProvider: &lsproto.BooleanOrHoverOptions{ Boolean: new(true), }, DefinitionProvider: &lsproto.BooleanOrDefinitionOptions{ Boolean: new(true), }, TypeDefinitionProvider: &lsproto.BooleanOrTypeDefinitionOptionsOrTypeDefinitionRegistrationOptions{ Boolean: new(true), }, ReferencesProvider: &lsproto.BooleanOrReferenceOptions{ Boolean: new(true), }, ImplementationProvider: &lsproto.BooleanOrImplementationOptionsOrImplementationRegistrationOptions{ Boolean: new(true), }, DiagnosticProvider: &lsproto.DiagnosticOptionsOrRegistrationOptions{ Options: &lsproto.DiagnosticOptions{ Identifier: new("typescript"), InterFileDependencies: true, }, }, CompletionProvider: &lsproto.CompletionOptions{ TriggerCharacters: &ls.TriggerCharacters, ResolveProvider: new(true), CompletionItem: &lsproto.ServerCompletionItemOptions{ LabelDetailsSupport: new(true), }, }, SignatureHelpProvider: &lsproto.SignatureHelpOptions{ TriggerCharacters: &[]string{"(", ",", "<"}, RetriggerCharacters: &[]string{")"}, }, DocumentFormattingProvider: &lsproto.BooleanOrDocumentFormattingOptions{ Boolean: new(true), }, DocumentRangeFormattingProvider: &lsproto.BooleanOrDocumentRangeFormattingOptions{ Boolean: new(true), }, DocumentOnTypeFormattingProvider: &lsproto.DocumentOnTypeFormattingOptions{ FirstTriggerCharacter: "{", MoreTriggerCharacter: &[]string{"}", ";", "\n"}, }, WorkspaceSymbolProvider: &lsproto.BooleanOrWorkspaceSymbolOptions{ Boolean: new(true), }, DocumentSymbolProvider: &lsproto.BooleanOrDocumentSymbolOptions{ Boolean: new(true), }, FoldingRangeProvider: &lsproto.BooleanOrFoldingRangeOptionsOrFoldingRangeRegistrationOptions{ Boolean: new(true), }, RenameProvider: &lsproto.BooleanOrRenameOptions{ RenameOptions: &lsproto.RenameOptions{ PrepareProvider: new(true), }, }, DocumentHighlightProvider: &lsproto.BooleanOrDocumentHighlightOptions{ Boolean: new(true), }, SelectionRangeProvider: &lsproto.BooleanOrSelectionRangeOptionsOrSelectionRangeRegistrationOptions{ Boolean: new(true), }, LinkedEditingRangeProvider: &lsproto.BooleanOrLinkedEditingRangeOptionsOrLinkedEditingRangeRegistrationOptions{ Boolean: new(true), }, InlayHintProvider: &lsproto.BooleanOrInlayHintOptionsOrInlayHintRegistrationOptions{ Boolean: new(true), }, CodeLensProvider: &lsproto.CodeLensOptions{ ResolveProvider: new(true), }, CodeActionProvider: &lsproto.BooleanOrCodeActionOptions{ CodeActionOptions: &lsproto.CodeActionOptions{ CodeActionKinds: &[]lsproto.CodeActionKind{ lsproto.CodeActionKindQuickFix, lsproto.CodeActionKindSourceOrganizeImports, lsproto.CodeActionKindSourceRemoveUnusedImports, lsproto.CodeActionKindSourceSortImports, lsproto.CodeActionKindSourceFixAll, }, }, }, CallHierarchyProvider: &lsproto.BooleanOrCallHierarchyOptionsOrCallHierarchyRegistrationOptions{ Boolean: new(true), }, Experimental: &lsproto.ExperimentalServerCapabilities{ CustomSourceDefinitionProvider: new(true), CustomMultiDocumentHighlightProvider: new(true), }, VSReferencesProvider: new(true), VSOnAutoInsertProvider: &lsproto.VSOnAutoInsertOptions{ VSTriggerCharacters: []string{">"}, }, Workspace: &lsproto.WorkspaceOptions{ FileOperations: &lsproto.FileOperationOptions{ WillRename: &lsproto.FileOperationRegistrationOptions{ Filters: fileRenameFilters, }, }, }, SemanticTokensProvider: &lsproto.SemanticTokensOptionsOrRegistrationOptions{ Options: &lsproto.SemanticTokensOptions{ Legend: ls.SemanticTokensLegend(s.clientCapabilities.TextDocument.SemanticTokens), Full: &lsproto.BooleanOrSemanticTokensFullDelta{ Boolean: new(true), }, Range: &lsproto.BooleanOrEmptyObject{ Boolean: new(true), }, }, }, }, } return response, nil } func (s *Server) handleInitialized(ctx context.Context, params *lsproto.InitializedParams) error { var disablePushDiagnostics bool var enableTelemetry bool if s.initializationOptions.DisablePushDiagnostics != nil { disablePushDiagnostics = *s.initializationOptions.DisablePushDiagnostics } if s.initializationOptions.EnableTelemetry != nil { enableTelemetry = *s.initializationOptions.EnableTelemetry } hasDynamicWatchRegistration := s.clientCapabilities.Workspace.DidChangeWatchedFiles.DynamicRegistration if hasDynamicWatchRegistration { s.logger.Logf("file watching: using LSP client-side watching (client supports dynamic registration)") s.watchEnabled = true } else if fswatch.Default().HasFastRecursiveBackend() { // The client cannot watch files itself, but the builtin watcher has a // backend with efficient recursive watching (Windows or FSEvents), so // fall back to watching files in-process. s.logger.Logf("file watching: using builtin in-process watcher (client lacks dynamic watch registration)") s.watchEnabled = true s.builtinWatcher = lspwatcher.New(s.fs, func(changes []*lsproto.FileEvent) { if s.session != nil { s.session.DidChangeWatchedFiles(s.backgroundCtx, changes) } }, s.logger) } else { // The client cannot watch files and the builtin watcher backend lacks // efficient recursive watching, so file watching is disabled. s.logger.Logf("file watching: disabled (client lacks dynamic watch registration and builtin watcher backend is not fast-recursive)") } cwd := s.cwd if s.clientCapabilities.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.telemetryEnabled = enableTelemetry s.session = project.NewSession(&project.SessionInit{ BackgroundCtx: lsproto.WithClientCapabilities(s.backgroundCtx, &s.clientCapabilities), Options: &project.SessionOptions{ CurrentDirectory: cwd, DefaultLibraryPath: s.defaultLibraryPath, TypingsLocation: s.typingsLocation, PositionEncoding: s.positionEncoding, WatchEnabled: s.watchEnabled, LoggingEnabled: true, TelemetryEnabled: enableTelemetry, DebounceDelay: 500 * time.Millisecond, PushDiagnosticsEnabled: !disablePushDiagnostics, Locale: s.locale, }, FS: s.fs, Logger: s.logger, Client: s, NpmExecutor: s, ParseCache: s.parseCache, }) userPreferences, err := s.RequestConfiguration(ctx) if err != nil { return err } s.session.InitializeWithUserConfig(userPreferences) _, err = sendClientRequest(ctx, s, lsproto.ClientRegisterCapabilityInfo, &lsproto.RegistrationParams{ Registrations: []*lsproto.Registration{ { Id: "typescript-config-watch-id", RegisterOptions: &lsproto.RegisterOptions{ WorkspaceDidChangeConfiguration: &lsproto.DidChangeConfigurationRegistrationOptions{ Section: &lsproto.StringOrStrings{ Strings: &[]string{"js/ts", "typescript", "javascript", "editor"}, }, }, }, }, }, }) if err != nil { return fmt.Errorf("failed to register configuration change watcher: %w", err) } // !!! temporary. // Remove when we have `handleDidChangeConfiguration`/implicit project config support // derived from 'js/ts.implicitProjectConfig.*'. if s.compilerOptionsForInferredProjects != nil { s.session.DidChangeCompilerOptionsForInferredProjects(ctx, s.compilerOptionsForInferredProjects) } s.session.StartPerformanceTelemetry() close(s.initComplete) return nil } func (s *Server) handleShutdown(ctx context.Context, _ lsproto.NoParams, _ *lsproto.RequestMessage) (lsproto.ShutdownResponse, error) { if s.builtinWatcher != nil { s.builtinWatcher.Close() } s.session.Close() return lsproto.ShutdownResponse{}, nil } func (s *Server) handleExit(ctx context.Context, _ lsproto.NoParams) error { return io.EOF } func (s *Server) handleDidChangeWorkspaceConfiguration(ctx context.Context, params *lsproto.DidChangeConfigurationParams) error { if params.Settings == nil { return nil } else if settings, ok := params.Settings.(map[string]any); ok { s.session.Configure(lsutil.ParseUserPreferences(settings)) } return nil } 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(_ context.Context, _ *lsproto.SetTraceParams) error { // $/setTrace is sent by vscode-languageclient when trace settings change. // Server log verbosity is controlled separately by custom/setLogVerbosity, // so this handler is intentionally a no-op. return nil } func (s *Server) handleSetLogVerbosity(_ context.Context, params *lsproto.SetLogVerbosityParams) error { if !isValidLogVerbosity(params.Verbosity) { return fmt.Errorf("%w: invalid log verbosity %d", lsproto.ErrorCodeInvalidParams, params.Verbosity) } s.logger.SetVerbosity(params.Verbosity) return nil } func (s *Server) handleDocumentDiagnostic(ctx context.Context, ls *ls.LanguageService, params *lsproto.DocumentDiagnosticParams) (lsproto.DocumentDiagnosticResponse, error) { ctx = core.WithCheckerLifetime(ctx, core.CheckerLifetimeDiagnostics) 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) } func (s *Server) handlePrepareRename(ctx context.Context, languageService *ls.LanguageService, params *lsproto.PrepareRenameParams) (lsproto.PrepareRenameResponse, error) { info := languageService.GetRenameInfo(ctx, "" /*newName*/, params.TextDocument.Uri, params.Position) if !info.CanRename { return lsproto.PrepareRenameResponse{}, userFacingRequestFailedError(info.LocalizedErrorMessage) } return lsproto.PrepareRenameResponse{ PrepareRenamePlaceholder: &lsproto.PrepareRenamePlaceholder{ Range: info.TriggerSpan, Placeholder: info.DisplayName, }, }, nil } func (s *Server) handleRename(ctx context.Context, params *lsproto.RenameParams, req *lsproto.RequestMessage) (lsproto.RenameResponse, error) { defaultLs, orchestrator, err := s.getLanguageServiceAndCrossProjectOrchestrator(ctx, params.TextDocument.Uri, req) if err != nil { return lsproto.RenameResponse{}, err } info := defaultLs.GetRenameInfo(ctx, params.NewName, params.TextDocument.Uri, params.Position) if info.CanRename && info.FileToRename != "" { // We send a `willRenameFiles` request if the client allows; // otherwise we directly compute the edits for renaming the file. if ls.ClientSupportsWillRenameFiles(ctx) { documentChanges := []lsproto.TextDocumentEditOrCreateFileOrRenameFileOrDeleteFile{ { RenameFile: &lsproto.RenameFile{ Kind: lsproto.StringLiteralRename{}, OldUri: lsconv.FileNameToDocumentURI(info.FileToRename), NewUri: lsconv.FileNameToDocumentURI(info.NewFileName), }, }, } return lsproto.WorkspaceEditOrNull{ WorkspaceEdit: &lsproto.WorkspaceEdit{ DocumentChanges: &documentChanges, }, }, nil } renameFilesParams := &lsproto.RenameFilesParams{ Files: []*lsproto.FileRename{{ OldUri: string(lsconv.FileNameToDocumentURI(info.FileToRename)), NewUri: string(lsconv.FileNameToDocumentURI(info.NewFileName)), }}, } return s.handleWillRenameFilesWorker(ctx, renameFilesParams, req, true /*sendRenameFile*/) } return defaultLs.ProvideRename(ctx, params, orchestrator) } func (s *Server) handleWillRenameFiles(ctx context.Context, params *lsproto.RenameFilesParams, msg *lsproto.RequestMessage) (lsproto.WillRenameFilesResponse, error) { return s.handleWillRenameFilesWorker(ctx, params, msg, false /*sendRenameFile*/) } // If `sendRenameFile` is true, the original `willRenameFiles` request is being handled as part of a rename operation // where the client doesn't support `willRenameFiles`, // so we should include the file rename in the edits we return func (s *Server) handleWillRenameFilesWorker(ctx context.Context, params *lsproto.RenameFilesParams, _ *lsproto.RequestMessage, sendRenameFile bool) (lsproto.WillRenameFilesResponse, error) { if len(params.Files) == 0 { return lsproto.WillRenameFilesResponse{}, nil } uris := make([]lsproto.DocumentUri, 0, len(params.Files)) for _, file := range params.Files { uris = append(uris, lsproto.DocumentUri(file.OldUri)) } if len(uris) == 0 { return lsproto.WillRenameFilesResponse{}, nil } services := s.session.GetLanguageServicesForDocuments(ctx, uris) type editKey struct { uri lsproto.DocumentUri range_ lsproto.Range } seenEdits := make(map[editKey]string) seenRenames := make(map[lsproto.DocumentUri]bool) var documentChanges []lsproto.TextDocumentEditOrCreateFileOrRenameFileOrDeleteFile for _, languageService := range services { for _, file := range params.Files { changes := languageService.GetEditsForFileRename(ctx, lsproto.DocumentUri(file.OldUri), lsproto.DocumentUri(file.NewUri)) for _, change := range changes { if change.RenameFile != nil { if !seenRenames[change.RenameFile.OldUri] { seenRenames[change.RenameFile.OldUri] = true documentChanges = append(documentChanges, change) } } else if change.TextDocumentEdit != nil { uri := change.TextDocumentEdit.TextDocument.Uri var deduped []lsproto.TextEditOrAnnotatedTextEditOrSnippetTextEdit for _, edit := range change.TextDocumentEdit.Edits { if edit.TextEdit != nil { key := editKey{uri: uri, range_: edit.TextEdit.Range} if prev, ok := seenEdits[key]; ok && prev == edit.TextEdit.NewText { continue } seenEdits[key] = edit.TextEdit.NewText } deduped = append(deduped, edit) } if len(deduped) > 0 { documentChanges = append(documentChanges, lsproto.TextDocumentEditOrCreateFileOrRenameFileOrDeleteFile{ TextDocumentEdit: &lsproto.TextDocumentEdit{ TextDocument: change.TextDocumentEdit.TextDocument, Edits: deduped, }, }) } } } } } if sendRenameFile { for _, file := range params.Files { documentChanges = append(documentChanges, lsproto.TextDocumentEditOrCreateFileOrRenameFileOrDeleteFile{ RenameFile: &lsproto.RenameFile{ Kind: lsproto.StringLiteralRename{}, OldUri: lsproto.DocumentUri(file.OldUri), NewUri: lsproto.DocumentUri(file.NewUri), }, }) } } if len(documentChanges) == 0 { return lsproto.WillRenameFilesResponse{}, nil } if ls.ClientSupportsDocumentChanges(ctx) { return lsproto.WillRenameFilesResponse{ WorkspaceEdit: &lsproto.WorkspaceEdit{ DocumentChanges: &documentChanges, }, }, nil } changes := make(map[lsproto.DocumentUri][]*lsproto.TextEdit) for _, change := range documentChanges { if change.TextDocumentEdit != nil { uri := change.TextDocumentEdit.TextDocument.Uri for _, edit := range change.TextDocumentEdit.Edits { if edit.TextEdit != nil { changes[uri] = append(changes[uri], edit.TextEdit) } } } } return lsproto.WillRenameFilesResponse{ WorkspaceEdit: &lsproto.WorkspaceEdit{ Changes: new(changes), }, }, nil } 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, ) } func (s *Server) handleFoldingRange(ctx context.Context, ls *ls.LanguageService, params *lsproto.FoldingRangeParams) (lsproto.FoldingRangeResponse, error) { return ls.ProvideFoldingRange(ctx, params.TextDocument.Uri) } func (s *Server) handleVSOnAutoInsert(ctx context.Context, ls *ls.LanguageService, params *lsproto.VSOnAutoInsertParams) (lsproto.VSOnAutoInsertResponse, error) { return ls.ProvideOnAutoInsert(ctx, params) } func (s *Server) handleLinkedEditingRange(ctx context.Context, ls *ls.LanguageService, params *lsproto.LinkedEditingRangeParams) (lsproto.LinkedEditingRangeResponse, error) { return ls.ProvideLinkedEditingRange(ctx, params) } 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) handleSourceDefinition(ctx context.Context, ls *ls.LanguageService, params *lsproto.TextDocumentPositionParams) (lsproto.CustomTextDocumentSourceDefinitionResponse, error) { resp, err := ls.ProvideSourceDefinition(ctx, params.TextDocument.Uri, params.Position) if err != nil { return nil, err } return &resp, nil } 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) handleCompletion(ctx context.Context, languageService *ls.LanguageService, params *lsproto.CompletionParams) (lsproto.CompletionResponse, error) { return languageService.ProvideCompletion( ctx, params.TextDocument.Uri, params.Position, params.Context, ) } func (s *Server) handleCompletionItemResolve(ctx context.Context, params *lsproto.CompletionItem, reqMsg *lsproto.RequestMessage) (lsproto.CompletionResolveResponse, error) { data := params.Data languageService, err := s.session.GetLanguageService(ctx, lsconv.FileNameToDocumentURI(data.FileName)) if err != nil { return nil, err } defer s.recover(reqMsg) return languageService.ResolveCompletionItem( ctx, params, data, ) } 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) { var resp lsproto.WorkspaceSymbolResponse var lsErr error s.session.WithSnapshotLoadingProjectTree(ctx, nil, func(snapshot *project.Snapshot) { defer s.recover(reqMsg) programs := core.Map(snapshot.ProjectCollection.Projects(), (*project.Project).GetProgram) resp, lsErr = ls.ProvideWorkspaceSymbols( ctx, programs, snapshot.Converters(), snapshot.UserPreferences(), params.Query, ) }) return resp, lsErr } 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) 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) handleMultiDocumentHighlight(ctx context.Context, ls *ls.LanguageService, params *lsproto.MultiDocumentHighlightParams) (lsproto.CustomMultiDocumentHighlightResponse, error) { return ls.ProvideMultiDocumentHighlights(ctx, params.TextDocument.Uri, params.Position, params.FilesToSearch) } func (s *Server) handleSelectionRange(ctx context.Context, ls *ls.LanguageService, params *lsproto.SelectionRangeParams) (lsproto.SelectionRangeResponse, error) { return ls.ProvideSelectionRanges(ctx, params) } func (s *Server) handleCodeAction(ctx context.Context, ls *ls.LanguageService, params *lsproto.CodeActionParams) (lsproto.CodeActionResponse, error) { return ls.ProvideCodeActions(ctx, params) } func (s *Server) handleInlayHint( ctx context.Context, languageService *ls.LanguageService, params *lsproto.InlayHintParams, ) (lsproto.InlayHintResponse, error) { return languageService.ProvideInlayHint(ctx, params) } func (s *Server) handleCodeLens(ctx context.Context, ls *ls.LanguageService, params *lsproto.CodeLensParams) (lsproto.CodeLensResponse, error) { return ls.ProvideCodeLenses(ctx, params.TextDocument.Uri) } func (s *Server) handleCodeLensResolve(ctx context.Context, codeLens *lsproto.CodeLens, reqMsg *lsproto.RequestMessage) (*lsproto.CodeLens, error) { defaultLs, orchestrator, err := s.getLanguageServiceAndCrossProjectOrchestrator(ctx, codeLens.Data.Uri, reqMsg) if ctx.Err() != nil { return nil, ctx.Err() } if err != nil { // This can happen if a codeLens/resolve request comes in after a program change. // While it's true that handlers should latch onto a specific snapshot // while processing requests, we just set `Data.Uri` based on // some older snapshot's contents. The content could have been modified, // or the file itself could have been removed from the session entirely. // Note this won't bail out on every change, but will prevent crashing // based on non-existent files and line maps from shortened files. return codeLens, lsproto.ErrorCodeContentModified } defer s.recover(reqMsg) return defaultLs.ResolveCodeLens( ctx, codeLens, s.initializationOptions.CodeLensShowLocationsCommandName, orchestrator, ) } func (s *Server) handlePrepareCallHierarchy( ctx context.Context, languageService *ls.LanguageService, params *lsproto.CallHierarchyPrepareParams, ) (lsproto.CallHierarchyPrepareResponse, error) { return languageService.ProvidePrepareCallHierarchy(ctx, params.TextDocument.Uri, params.Position) } func (s *Server) handleCallHierarchyIncomingCalls( ctx context.Context, params *lsproto.CallHierarchyIncomingCallsParams, reqMsg *lsproto.RequestMessage, ) (lsproto.CallHierarchyIncomingCallsResponse, error) { defaultLs, orchestrator, err := s.getLanguageServiceAndCrossProjectOrchestrator(ctx, params.Item.Uri, reqMsg) if err != nil { return lsproto.CallHierarchyIncomingCallsOrNull{}, err } return defaultLs.ProvideCallHierarchyIncomingCalls(ctx, params.Item, orchestrator) } func (s *Server) handleCallHierarchyOutgoingCalls( ctx context.Context, params *lsproto.CallHierarchyOutgoingCallsParams, _ *lsproto.RequestMessage, ) (lsproto.CallHierarchyOutgoingCallsResponse, error) { languageService, err := s.session.GetLanguageService(ctx, params.Item.Uri) if err != nil { return lsproto.CallHierarchyOutgoingCallsOrNull{}, err } return languageService.ProvideCallHierarchyOutgoingCalls(ctx, params.Item) } func (s *Server) handleSemanticTokensFull(ctx context.Context, ls *ls.LanguageService, params *lsproto.SemanticTokensParams) (lsproto.SemanticTokensResponse, error) { return ls.ProvideSemanticTokens(ctx, params.TextDocument.Uri) } func (s *Server) handleSemanticTokensRange(ctx context.Context, ls *ls.LanguageService, params *lsproto.SemanticTokensRangeParams) (lsproto.SemanticTokensRangeResponse, error) { return ls.ProvideSemanticTokensRange(ctx, params.TextDocument.Uri, params.Range) } func (s *Server) handleInitializeAPISession(ctx context.Context, params *lsproto.InitializeAPISessionParams, _ *lsproto.RequestMessage) (lsproto.CustomInitializeAPISessionResponse, error) { s.apiSessionsMu.Lock() defer s.apiSessionsMu.Unlock() if s.apiSessions == nil { s.apiSessions = make(map[string]*api.Session) } var apiSession *api.Session apiSession = api.NewSession(s.session, nil) // Use provided pipe path or generate a unique one var pipePath string if params.Pipe != nil && *params.Pipe != "" { pipePath = *params.Pipe } else { pipePath = s.generateAPIPipePath() } transport, err := api.NewPipeTransport(pipePath) if err != nil { return nil, fmt.Errorf("failed to create API transport: %w", err) } // Start accepting connections in the background go func() { defer func() { apiSession.Close() s.removeAPISession(apiSession.ID()) }() rwc, acceptErr := transport.Accept() _ = transport.Close() if acceptErr != nil { s.logger.Errorf("API session %s: failed to accept connection: %v", apiSession.ID(), acceptErr) return } // Create a cancellable context for the API connection apiCtx, apiCancel := context.WithCancel(s.backgroundCtx) defer apiCancel() // Run the connection with panic recovery defer func() { if r := recover(); r != nil { stack := debug.Stack() s.logger.Errorf("API session %s: panic: %v\n%s", apiSession.ID(), r, string(stack)) // Cancel the context to shut down the connection apiCancel() // Close the underlying connection rwc.Close() } }() conn := api.NewAsyncConn(rwc, apiSession) if apiErr := conn.Run(apiCtx); apiErr != nil { s.logger.Errorf("API session %s: %v", apiSession.ID(), apiErr) } }() s.apiSessions[apiSession.ID()] = apiSession return &lsproto.InitializeAPISessionResult{ SessionId: apiSession.ID(), Pipe: pipePath, }, nil } func (s *Server) generateAPIPipePath() string { // Generate a high-entropy path using time and random source now := time.Now().UnixNano() rnd := rand.Uint64() return api.GeneratePipePath(fmt.Sprintf("tsgo-api-%x-%x", now, rnd)) } func (s *Server) removeAPISession(id string) { s.apiSessionsMu.Lock() defer s.apiSessionsMu.Unlock() delete(s.apiSessions, id) } // !!! 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) { return s.npmInstall(cwd, args) } // Developer/debugging command handlers func (s *Server) handleRunGC(_ context.Context, _ lsproto.NoParams, _ *lsproto.RequestMessage) (lsproto.RunGCResponse, error) { pprof.RunGC() s.logger.Info("GC triggered") return lsproto.Null{}, nil } func (s *Server) handleSaveHeapProfile(_ context.Context, params *lsproto.ProfileParams, _ *lsproto.RequestMessage) (*lsproto.ProfileResult, error) { filePath, err := pprof.SaveHeapProfile(params.Dir) if err != nil { return nil, err } s.logger.Info("Heap profile saved to: ", filePath) return &lsproto.ProfileResult{File: filePath}, nil } func (s *Server) handleSaveAllocProfile(_ context.Context, params *lsproto.ProfileParams, _ *lsproto.RequestMessage) (*lsproto.ProfileResult, error) { filePath, err := pprof.SaveAllocProfile(params.Dir) if err != nil { return nil, err } s.logger.Info("Allocation profile saved to: ", filePath) return &lsproto.ProfileResult{File: filePath}, nil } func (s *Server) handleStartCPUProfile(_ context.Context, params *lsproto.ProfileParams, _ *lsproto.RequestMessage) (lsproto.StartCPUProfileResponse, error) { err := s.cpuProfiler.StartCPUProfile(params.Dir) if err != nil { return lsproto.Null{}, err } s.logger.Info("CPU profiling started, will save to: ", params.Dir) return lsproto.Null{}, nil } func (s *Server) handleStopCPUProfile(_ context.Context, _ lsproto.NoParams, _ *lsproto.RequestMessage) (*lsproto.ProfileResult, error) { filePath, err := s.cpuProfiler.StopCPUProfile() if err != nil { return nil, err } s.logger.Info("CPU profile saved to: ", filePath) return &lsproto.ProfileResult{File: filePath}, nil } func (s *Server) handleProjectInfo(ctx context.Context, params *lsproto.ProjectInfoParams, _ *lsproto.RequestMessage) (lsproto.CustomProjectInfoResponse, error) { uri := params.TextDocument.Uri defaultProject, _, _, err := s.session.GetLanguageServiceAndProjectsForFile(ctx, uri) if err != nil { return nil, err } configFilePath := "" if defaultProject != nil && defaultProject.Kind == project.KindConfigured { configFilePath = defaultProject.Name() } return &lsproto.ProjectInfoResult{ ConfigFilePath: configFilePath, }, nil }