feat: add configurable timeout for LSP initialization (#2075)

huaiyuWangh created

* feat: add configurable timeout for LSP initialization

Add a timeout field to LSPConfig to allow users to customize
the initialization timeout for LSP servers. This is particularly
useful for slow-starting servers like kotlin-language-server that
may require more than the default 30 seconds to initialize.

Fixes #1865

* refactor: simplify timeout logic with cmp.Or

Simplified the timeout handling by using Go's cmp.Or() function instead
of manual conditional checks, reducing code from 5 lines to 1 line while
maintaining the same functionality.

Change summary

internal/app/lsp.go       | 10 +++++++---
internal/config/config.go |  1 +
schema.json               |  9 +++++++++
3 files changed, 17 insertions(+), 3 deletions(-)

Detailed changes

internal/app/lsp.go 🔗

@@ -1,6 +1,7 @@
 package app
 
 import (
+	"cmp"
 	"context"
 	"log/slog"
 	"os/exec"
@@ -69,7 +70,7 @@ func (app *App) initLSPClients(ctx context.Context) {
 		wg.Go(func() {
 			app.createAndStartLSPClient(
 				ctx, name,
-				toOurConfig(server),
+				toOurConfig(server, app.config.LSP[name]),
 				slices.Contains(userConfiguredLSPs, name),
 			)
 		})
@@ -83,7 +84,9 @@ func (app *App) initLSPClients(ctx context.Context) {
 	}
 }
 
-func toOurConfig(in *powernapconfig.ServerConfig) config.LSPConfig {
+// toOurConfig merges powernap default config with user config.
+// If user config is zero value, it means no user override exists.
+func toOurConfig(in *powernapconfig.ServerConfig, user config.LSPConfig) config.LSPConfig {
 	return config.LSPConfig{
 		Command:     in.Command,
 		Args:        in.Args,
@@ -92,6 +95,7 @@ func toOurConfig(in *powernapconfig.ServerConfig) config.LSPConfig {
 		RootMarkers: in.RootMarkers,
 		InitOptions: in.InitOptions,
 		Options:     in.Settings,
+		Timeout:     user.Timeout,
 	}
 }
 
@@ -126,7 +130,7 @@ func (app *App) createAndStartLSPClient(ctx context.Context, name string, config
 	lspClient.SetDiagnosticsCallback(updateLSPDiagnostics)
 
 	// Increase initialization timeout as some servers take more time to start.
-	initCtx, cancel := context.WithTimeout(ctx, 30*time.Second)
+	initCtx, cancel := context.WithTimeout(ctx, time.Duration(cmp.Or(config.Timeout, 30))*time.Second)
 	defer cancel()
 
 	// Initialize LSP client.

internal/config/config.go 🔗

@@ -194,6 +194,7 @@ type LSPConfig struct {
 	RootMarkers []string          `json:"root_markers,omitempty" jsonschema:"description=Files or directories that indicate the project root,example=go.mod,example=package.json,example=Cargo.toml"`
 	InitOptions map[string]any    `json:"init_options,omitempty" jsonschema:"description=Initialization options passed to the LSP server during initialize request"`
 	Options     map[string]any    `json:"options,omitempty" jsonschema:"description=LSP server-specific settings passed during initialization"`
+	Timeout     int               `json:"timeout,omitempty" jsonschema:"description=Timeout in seconds for LSP server initialization,default=30,example=60,example=120"`
 }
 
 type TUIOptions struct {

schema.json 🔗

@@ -156,6 +156,15 @@
         "options": {
           "type": "object",
           "description": "LSP server-specific settings passed during initialization"
+        },
+        "timeout": {
+          "type": "integer",
+          "description": "Timeout in seconds for LSP server initialization",
+          "default": 30,
+          "examples": [
+            60,
+            120
+          ]
         }
       },
       "additionalProperties": false,