Files
kjol/tools/tsgo/internal/lsp/server.go
2026-07-09 16:50:43 -04:00

1884 lines
67 KiB
Go

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
}