lsp.go 4.4 KB

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