lsp.go

  1package app
  2
  3import (
  4	"context"
  5	"time"
  6
  7	"github.com/opencode-ai/opencode/internal/config"
  8	"github.com/opencode-ai/opencode/internal/logging"
  9	"github.com/opencode-ai/opencode/internal/lsp"
 10	"github.com/opencode-ai/opencode/internal/lsp/protocol"
 11	"github.com/opencode-ai/opencode/internal/lsp/setup"
 12	"github.com/opencode-ai/opencode/internal/lsp/watcher"
 13)
 14
 15func (app *App) initLSPClients(ctx context.Context) {
 16	cfg := config.Get()
 17
 18	for name, clientConfig := range cfg.LSP {
 19		if clientConfig.Disabled {
 20			logging.Info("Skipping disabled LSP client", "name", name)
 21			continue
 22		}
 23		
 24		go app.createAndStartLSPClient(ctx, name, clientConfig.Command, clientConfig.Args...)
 25	}
 26	logging.Info("LSP clients initialization started in background")
 27}
 28
 29// CheckAndSetupLSP checks if LSP is configured and returns true if setup is needed
 30func (app *App) CheckAndSetupLSP(ctx context.Context) bool {
 31	if config.IsLSPConfigured() {
 32		return false
 33	}
 34	
 35	logging.Info("LSP not configured, setup needed")
 36	return true
 37}
 38
 39// ConfigureLSP configures LSP with the provided servers
 40func (app *App) ConfigureLSP(ctx context.Context, servers map[protocol.LanguageKind]setup.LSPServerInfo) error {
 41	// Save the configuration using the LSP setup service
 42	err := app.LSPSetup.SaveConfiguration(ctx, servers)
 43	if err != nil {
 44		logging.Error("Failed to save LSP configuration", err)
 45		return err
 46	}
 47	
 48	// Initialize LSP clients with the new configuration
 49	app.InitLSPClients(ctx)
 50	
 51	logging.Info("LSP configuration updated successfully")
 52	return nil
 53}
 54
 55// createAndStartLSPClient creates a new LSP client, initializes it, and starts its workspace watcher
 56func (app *App) createAndStartLSPClient(ctx context.Context, name string, command string, args ...string) {
 57	logging.Info("Creating LSP client", "name", name, "command", command, "args", args)
 58	
 59	lspClient, err := lsp.NewClient(ctx, command, args...)
 60	if err != nil {
 61		logging.Error("Failed to create LSP client for", name, err)
 62		return
 63	}
 64
 65	// Create a longer timeout for initialization (some servers take time to start)
 66	initCtx, cancel := context.WithTimeout(ctx, 30*time.Second)
 67	defer cancel()
 68	
 69	_, err = lspClient.InitializeLSPClient(initCtx, config.WorkingDirectory())
 70	if err != nil {
 71		logging.Error("Initialize failed", "name", name, "error", err)
 72		lspClient.Close()
 73		return
 74	}
 75
 76	// Wait for the server to be ready
 77	if err := lspClient.WaitForServerReady(initCtx); err != nil {
 78		logging.Error("Server failed to become ready", "name", name, "error", err)
 79		// We'll continue anyway, as some functionality might still work
 80		lspClient.SetServerState(lsp.StateError)
 81	} else {
 82		logging.Info("LSP server is ready", "name", name)
 83		lspClient.SetServerState(lsp.StateReady)
 84	}
 85
 86	logging.Info("LSP client initialized", "name", name)
 87	
 88	// Create a child context that can be canceled when the app is shutting down
 89	watchCtx, cancelFunc := context.WithCancel(ctx)
 90	
 91	// Create a context with the server name for better identification
 92	watchCtx = context.WithValue(watchCtx, "serverName", name)
 93	
 94	// Create the workspace watcher
 95	workspaceWatcher := watcher.NewWorkspaceWatcher(lspClient)
 96
 97	// Store the cancel function to be called during cleanup
 98	app.cancelFuncsMutex.Lock()
 99	app.watcherCancelFuncs = append(app.watcherCancelFuncs, cancelFunc)
100	app.cancelFuncsMutex.Unlock()
101
102	// Add the watcher to a WaitGroup to track active goroutines
103	app.watcherWG.Add(1)
104
105	// Add to map with mutex protection before starting goroutine
106	app.clientsMutex.Lock()
107	app.LSPClients[name] = lspClient
108	app.clientsMutex.Unlock()
109
110	go app.runWorkspaceWatcher(watchCtx, name, workspaceWatcher)
111}
112
113// runWorkspaceWatcher executes the workspace watcher for an LSP client
114func (app *App) runWorkspaceWatcher(ctx context.Context, name string, workspaceWatcher *watcher.WorkspaceWatcher) {
115	defer app.watcherWG.Done()
116	defer logging.RecoverPanic("LSP-"+name, func() {
117		// Try to restart the client
118		app.restartLSPClient(ctx, name)
119	})
120
121	workspaceWatcher.WatchWorkspace(ctx, config.WorkingDirectory())
122	logging.Info("Workspace watcher stopped", "client", name)
123}
124
125// restartLSPClient attempts to restart a crashed or failed LSP client
126func (app *App) restartLSPClient(ctx context.Context, name string) {
127	// Get the original configuration
128	cfg := config.Get()
129	clientConfig, exists := cfg.LSP[name]
130	if !exists {
131		logging.Error("Cannot restart client, configuration not found", "client", name)
132		return
133	}
134
135	// Clean up the old client if it exists
136	app.clientsMutex.Lock()
137	oldClient, exists := app.LSPClients[name]
138	if exists {
139		delete(app.LSPClients, name) // Remove from map before potentially slow shutdown
140	}
141	app.clientsMutex.Unlock()
142
143	if exists && oldClient != nil {
144		// Try to shut it down gracefully, but don't block on errors
145		shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
146		_ = oldClient.Shutdown(shutdownCtx)
147		cancel()
148	}
149
150	// Create a new client using the shared function
151	app.createAndStartLSPClient(ctx, name, clientConfig.Command, clientConfig.Args...)
152	logging.Info("Successfully restarted LSP client", "client", name)
153}