lsp.go

  1package app
  2
  3import (
  4	"context"
  5	"time"
  6
  7	"github.com/kujtimiihoxha/termai/internal/config"
  8	"github.com/kujtimiihoxha/termai/internal/logging"
  9	"github.com/kujtimiihoxha/termai/internal/lsp"
 10	"github.com/kujtimiihoxha/termai/internal/lsp/watcher"
 11)
 12
 13func (app *App) initLSPClients(ctx context.Context) {
 14	cfg := config.Get()
 15
 16	// Initialize LSP clients
 17	for name, clientConfig := range cfg.LSP {
 18		app.createAndStartLSPClient(ctx, name, clientConfig.Command, clientConfig.Args...)
 19	}
 20}
 21
 22// createAndStartLSPClient creates a new LSP client, initializes it, and starts its workspace watcher
 23func (app *App) createAndStartLSPClient(ctx context.Context, name string, command string, args ...string) {
 24	// Create a specific context for initialization with a timeout
 25	initCtx, initCancel := context.WithTimeout(context.Background(), 30*time.Second)
 26	defer initCancel()
 27
 28	// Create the LSP client
 29	lspClient, err := lsp.NewClient(initCtx, command, args...)
 30	if err != nil {
 31		logging.Error("Failed to create LSP client for", name, err)
 32		return
 33	}
 34
 35	// Initialize with the initialization context
 36	_, err = lspClient.InitializeLSPClient(initCtx, config.WorkingDirectory())
 37	if err != nil {
 38		logging.Error("Initialize failed", "name", name, "error", err)
 39		// Clean up the client to prevent resource leaks
 40		lspClient.Close()
 41		return
 42	}
 43
 44	// Create a child context that can be canceled when the app is shutting down
 45	watchCtx, cancelFunc := context.WithCancel(ctx)
 46	workspaceWatcher := watcher.NewWorkspaceWatcher(lspClient)
 47
 48	// Store the cancel function to be called during cleanup
 49	app.cancelFuncsMutex.Lock()
 50	app.watcherCancelFuncs = append(app.watcherCancelFuncs, cancelFunc)
 51	app.cancelFuncsMutex.Unlock()
 52
 53	// Add the watcher to a WaitGroup to track active goroutines
 54	app.watcherWG.Add(1)
 55
 56	// Add to map with mutex protection before starting goroutine
 57	app.clientsMutex.Lock()
 58	app.LSPClients[name] = lspClient
 59	app.clientsMutex.Unlock()
 60
 61	go app.runWorkspaceWatcher(watchCtx, name, workspaceWatcher)
 62}
 63
 64// runWorkspaceWatcher executes the workspace watcher for an LSP client
 65func (app *App) runWorkspaceWatcher(ctx context.Context, name string, workspaceWatcher *watcher.WorkspaceWatcher) {
 66	defer app.watcherWG.Done()
 67	defer func() {
 68		if r := recover(); r != nil {
 69			logging.Error("LSP client crashed", "client", name, "panic", r)
 70
 71			// Try to restart the client
 72			app.restartLSPClient(ctx, name)
 73		}
 74	}()
 75
 76	workspaceWatcher.WatchWorkspace(ctx, config.WorkingDirectory())
 77	logging.Info("Workspace watcher stopped", "client", name)
 78}
 79
 80// restartLSPClient attempts to restart a crashed or failed LSP client
 81func (app *App) restartLSPClient(ctx context.Context, name string) {
 82	// Get the original configuration
 83	cfg := config.Get()
 84	clientConfig, exists := cfg.LSP[name]
 85	if !exists {
 86		logging.Error("Cannot restart client, configuration not found", "client", name)
 87		return
 88	}
 89
 90	// Clean up the old client if it exists
 91	app.clientsMutex.Lock()
 92	oldClient, exists := app.LSPClients[name]
 93	if exists {
 94		delete(app.LSPClients, name) // Remove from map before potentially slow shutdown
 95	}
 96	app.clientsMutex.Unlock()
 97
 98	if exists && oldClient != nil {
 99		// Try to shut it down gracefully, but don't block on errors
100		shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
101		_ = oldClient.Shutdown(shutdownCtx)
102		cancel()
103	}
104
105	// Create a new client using the shared function
106	app.createAndStartLSPClient(ctx, name, clientConfig.Command, clientConfig.Args...)
107	logging.Info("Successfully restarted LSP client", "client", name)
108}