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}