lsp.go 4.4 KB

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