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}