From 2b5a33e476ae3c6b5c6345777d20792786836dda Mon Sep 17 00:00:00 2001 From: Kujtim Hoxha Date: Sat, 19 Apr 2025 15:15:29 +0200 Subject: lsp improvements --- internal/lsp/client.go | 364 ++++++++++++++++++++++++++++++++-- internal/lsp/watcher/watcher.go | 428 +++++++++++++++++++++++++++++++++++----- 2 files changed, 728 insertions(+), 64 deletions(-) (limited to 'internal/lsp') diff --git a/internal/lsp/client.go b/internal/lsp/client.go index dad07f3c0..932badc0b 100644 --- a/internal/lsp/client.go +++ b/internal/lsp/client.go @@ -8,6 +8,7 @@ import ( "io" "os" "os/exec" + "path/filepath" "strings" "sync" "sync/atomic" @@ -46,6 +47,9 @@ type Client struct { // Files are currently opened by the LSP openFiles map[string]*OpenFileInfo openFilesMu sync.RWMutex + + // Server state + serverState atomic.Value } func NewClient(ctx context.Context, command string, args ...string) (*Client, error) { @@ -80,6 +84,9 @@ func NewClient(ctx context.Context, command string, args ...string) (*Client, er openFiles: make(map[string]*OpenFileInfo), } + // Initialize server state + client.serverState.Store(StateStarting) + // Start the LSP server process if err := cmd.Start(); err != nil { return nil, fmt.Errorf("failed to start LSP server: %w", err) @@ -220,16 +227,6 @@ func (c *Client) InitializeLSPClient(ctx context.Context, workspaceDir string) ( return nil, fmt.Errorf("initialization failed: %w", err) } - // LSP sepecific Initialization - path := strings.ToLower(c.Cmd.Path) - switch { - case strings.Contains(path, "typescript-language-server"): - // err := initializeTypescriptLanguageServer(ctx, c, workspaceDir) - // if err != nil { - // return nil, err - // } - } - return &result, nil } @@ -273,10 +270,314 @@ const ( StateError ) +// GetServerState returns the current state of the LSP server +func (c *Client) GetServerState() ServerState { + if val := c.serverState.Load(); val != nil { + return val.(ServerState) + } + return StateStarting +} + +// SetServerState sets the current state of the LSP server +func (c *Client) SetServerState(state ServerState) { + c.serverState.Store(state) +} + +// WaitForServerReady waits for the server to be ready by polling the server +// with a simple request until it responds successfully or times out func (c *Client) WaitForServerReady(ctx context.Context) error { - // TODO: wait for specific messages or poll workspace/symbol - time.Sleep(time.Second * 1) - return nil + cnf := config.Get() + + // Set initial state + c.SetServerState(StateStarting) + + // Create a context with timeout + ctx, cancel := context.WithTimeout(ctx, 30*time.Second) + defer cancel() + + // Try to ping the server with a simple request + ticker := time.NewTicker(500 * time.Millisecond) + defer ticker.Stop() + + if cnf.DebugLSP { + logging.Debug("Waiting for LSP server to be ready...") + } + + // Determine server type for specialized initialization + serverType := c.detectServerType() + + // For TypeScript-like servers, we need to open some key files first + if serverType == ServerTypeTypeScript { + if cnf.DebugLSP { + logging.Debug("TypeScript-like server detected, opening key configuration files") + } + c.openKeyConfigFiles(ctx) + } + + for { + select { + case <-ctx.Done(): + c.SetServerState(StateError) + return fmt.Errorf("timeout waiting for LSP server to be ready") + case <-ticker.C: + // Try a ping method appropriate for this server type + err := c.pingServerByType(ctx, serverType) + if err == nil { + // Server responded successfully + c.SetServerState(StateReady) + if cnf.DebugLSP { + logging.Debug("LSP server is ready") + } + return nil + } else { + logging.Debug("LSP server not ready yet", "error", err, "serverType", serverType) + } + + if cnf.DebugLSP { + logging.Debug("LSP server not ready yet", "error", err, "serverType", serverType) + } + } + } +} + +// ServerType represents the type of LSP server +type ServerType int + +const ( + ServerTypeUnknown ServerType = iota + ServerTypeGo + ServerTypeTypeScript + ServerTypeRust + ServerTypePython + ServerTypeGeneric +) + +// detectServerType tries to determine what type of LSP server we're dealing with +func (c *Client) detectServerType() ServerType { + if c.Cmd == nil { + return ServerTypeUnknown + } + + cmdPath := strings.ToLower(c.Cmd.Path) + + switch { + case strings.Contains(cmdPath, "gopls"): + return ServerTypeGo + case strings.Contains(cmdPath, "typescript") || strings.Contains(cmdPath, "vtsls") || strings.Contains(cmdPath, "tsserver"): + return ServerTypeTypeScript + case strings.Contains(cmdPath, "rust-analyzer"): + return ServerTypeRust + case strings.Contains(cmdPath, "pyright") || strings.Contains(cmdPath, "pylsp") || strings.Contains(cmdPath, "python"): + return ServerTypePython + default: + return ServerTypeGeneric + } +} + +// openKeyConfigFiles opens important configuration files that help initialize the server +func (c *Client) openKeyConfigFiles(ctx context.Context) { + workDir := config.WorkingDirectory() + serverType := c.detectServerType() + + var filesToOpen []string + + switch serverType { + case ServerTypeTypeScript: + // TypeScript servers need these config files to properly initialize + filesToOpen = []string{ + filepath.Join(workDir, "tsconfig.json"), + filepath.Join(workDir, "package.json"), + filepath.Join(workDir, "jsconfig.json"), + } + + // Also find and open a few TypeScript files to help the server initialize + c.openTypeScriptFiles(ctx, workDir) + case ServerTypeGo: + filesToOpen = []string{ + filepath.Join(workDir, "go.mod"), + filepath.Join(workDir, "go.sum"), + } + case ServerTypeRust: + filesToOpen = []string{ + filepath.Join(workDir, "Cargo.toml"), + filepath.Join(workDir, "Cargo.lock"), + } + } + + // Try to open each file, ignoring errors if they don't exist + for _, file := range filesToOpen { + if _, err := os.Stat(file); err == nil { + // File exists, try to open it + if err := c.OpenFile(ctx, file); err != nil { + logging.Debug("Failed to open key config file", "file", file, "error", err) + } else { + logging.Debug("Opened key config file for initialization", "file", file) + } + } + } +} + +// pingServerByType sends a ping request appropriate for the server type +func (c *Client) pingServerByType(ctx context.Context, serverType ServerType) error { + switch serverType { + case ServerTypeTypeScript: + // For TypeScript, try a document symbol request on an open file + return c.pingTypeScriptServer(ctx) + case ServerTypeGo: + // For Go, workspace/symbol works well + return c.pingWithWorkspaceSymbol(ctx) + case ServerTypeRust: + // For Rust, workspace/symbol works well + return c.pingWithWorkspaceSymbol(ctx) + default: + // Default ping method + return c.pingWithWorkspaceSymbol(ctx) + } +} + +// pingTypeScriptServer tries to ping a TypeScript server with appropriate methods +func (c *Client) pingTypeScriptServer(ctx context.Context) error { + // First try workspace/symbol which works for many servers + if err := c.pingWithWorkspaceSymbol(ctx); err == nil { + return nil + } + + // If that fails, try to find an open file and request document symbols + c.openFilesMu.RLock() + defer c.openFilesMu.RUnlock() + + // If we have any open files, try to get document symbols for one + for uri := range c.openFiles { + filePath := strings.TrimPrefix(uri, "file://") + if strings.HasSuffix(filePath, ".ts") || strings.HasSuffix(filePath, ".js") || + strings.HasSuffix(filePath, ".tsx") || strings.HasSuffix(filePath, ".jsx") { + var symbols []protocol.DocumentSymbol + err := c.Call(ctx, "textDocument/documentSymbol", protocol.DocumentSymbolParams{ + TextDocument: protocol.TextDocumentIdentifier{ + URI: protocol.DocumentUri(uri), + }, + }, &symbols) + if err == nil { + return nil + } + } + } + + // If we have no open TypeScript files, try to find and open one + workDir := config.WorkingDirectory() + err := filepath.WalkDir(workDir, func(path string, d os.DirEntry, err error) error { + if err != nil { + return err + } + + // Skip directories and non-TypeScript files + if d.IsDir() { + return nil + } + + ext := filepath.Ext(path) + if ext == ".ts" || ext == ".js" || ext == ".tsx" || ext == ".jsx" { + // Found a TypeScript file, try to open it + if err := c.OpenFile(ctx, path); err == nil { + // Successfully opened, stop walking + return filepath.SkipAll + } + } + + return nil + }) + if err != nil { + logging.Debug("Error walking directory for TypeScript files", "error", err) + } + + // Final fallback - just try a generic capability + return c.pingWithServerCapabilities(ctx) +} + +// openTypeScriptFiles finds and opens TypeScript files to help initialize the server +func (c *Client) openTypeScriptFiles(ctx context.Context, workDir string) { + cnf := config.Get() + filesOpened := 0 + maxFilesToOpen := 5 // Limit to a reasonable number of files + + // Find and open TypeScript files + err := filepath.WalkDir(workDir, func(path string, d os.DirEntry, err error) error { + if err != nil { + return err + } + + // Skip directories and non-TypeScript files + if d.IsDir() { + // Skip common directories to avoid wasting time + if shouldSkipDir(path) { + return filepath.SkipDir + } + return nil + } + + // Check if we've opened enough files + if filesOpened >= maxFilesToOpen { + return filepath.SkipAll + } + + // Check file extension + ext := filepath.Ext(path) + if ext == ".ts" || ext == ".tsx" || ext == ".js" || ext == ".jsx" { + // Try to open the file + if err := c.OpenFile(ctx, path); err == nil { + filesOpened++ + if cnf.DebugLSP { + logging.Debug("Opened TypeScript file for initialization", "file", path) + } + } + } + + return nil + }) + + if err != nil && cnf.DebugLSP { + logging.Debug("Error walking directory for TypeScript files", "error", err) + } + + if cnf.DebugLSP { + logging.Debug("Opened TypeScript files for initialization", "count", filesOpened) + } +} + +// shouldSkipDir returns true if the directory should be skipped during file search +func shouldSkipDir(path string) bool { + dirName := filepath.Base(path) + + // Skip hidden directories + if strings.HasPrefix(dirName, ".") { + return true + } + + // Skip common directories that won't contain relevant source files + skipDirs := map[string]bool{ + "node_modules": true, + "dist": true, + "build": true, + "coverage": true, + "vendor": true, + "target": true, + } + + return skipDirs[dirName] +} + +// pingWithWorkspaceSymbol tries a workspace/symbol request +func (c *Client) pingWithWorkspaceSymbol(ctx context.Context) error { + var result []protocol.SymbolInformation + return c.Call(ctx, "workspace/symbol", protocol.WorkspaceSymbolParams{ + Query: "", + }, &result) +} + +// pingWithServerCapabilities tries to get server capabilities +func (c *Client) pingWithServerCapabilities(ctx context.Context) error { + // This is a very lightweight request that should work for most servers + return c.Notify(ctx, "$/cancelRequest", struct{ ID int }{ID: -1}) } type OpenFileInfo struct { @@ -435,6 +736,43 @@ func (c *Client) GetFileDiagnostics(uri protocol.DocumentUri) []protocol.Diagnos return c.diagnostics[uri] } +// GetDiagnostics returns all diagnostics for all files func (c *Client) GetDiagnostics() map[protocol.DocumentUri][]protocol.Diagnostic { return c.diagnostics } + +// OpenFileOnDemand opens a file only if it's not already open +// This is used for lazy-loading files when they're actually needed +func (c *Client) OpenFileOnDemand(ctx context.Context, filepath string) error { + // Check if the file is already open + if c.IsFileOpen(filepath) { + return nil + } + + // Open the file + return c.OpenFile(ctx, filepath) +} + +// GetDiagnosticsForFile ensures a file is open and returns its diagnostics +// This is useful for on-demand diagnostics when using lazy loading +func (c *Client) GetDiagnosticsForFile(ctx context.Context, filepath string) ([]protocol.Diagnostic, error) { + uri := fmt.Sprintf("file://%s", filepath) + documentUri := protocol.DocumentUri(uri) + + // Make sure the file is open + if !c.IsFileOpen(filepath) { + if err := c.OpenFile(ctx, filepath); err != nil { + return nil, fmt.Errorf("failed to open file for diagnostics: %w", err) + } + + // Give the LSP server a moment to process the file + time.Sleep(100 * time.Millisecond) + } + + // Get diagnostics + c.diagnosticsMu.RLock() + diagnostics := c.diagnostics[documentUri] + c.diagnosticsMu.RUnlock() + + return diagnostics, nil +} diff --git a/internal/lsp/watcher/watcher.go b/internal/lsp/watcher/watcher.go index 595c78db9..58dd01f70 100644 --- a/internal/lsp/watcher/watcher.go +++ b/internal/lsp/watcher/watcher.go @@ -9,6 +9,7 @@ import ( "sync" "time" + "github.com/bmatcuk/doublestar/v4" "github.com/fsnotify/fsnotify" "github.com/kujtimiihoxha/opencode/internal/config" "github.com/kujtimiihoxha/opencode/internal/logging" @@ -43,6 +44,8 @@ func NewWorkspaceWatcher(client *lsp.Client) *WorkspaceWatcher { // AddRegistrations adds file watchers to track func (w *WorkspaceWatcher) AddRegistrations(ctx context.Context, id string, watchers []protocol.FileSystemWatcher) { cnf := config.Get() + + logging.Debug("Adding file watcher registrations") w.registrationMu.Lock() defer w.registrationMu.Unlock() @@ -55,7 +58,6 @@ func (w *WorkspaceWatcher) AddRegistrations(ctx context.Context, id string, watc "id", id, "watchers", len(watchers), "total", len(w.registrations), - "watchers", watchers, ) for i, watcher := range watchers { @@ -88,66 +90,217 @@ func (w *WorkspaceWatcher) AddRegistrations(ctx context.Context, id string, watc } logging.Debug("WatchKind", "kind", watchKind) - - // Test match against some example paths - testPaths := []string{ - "/Users/phil/dev/mcp-language-server/internal/watcher/watcher.go", - "/Users/phil/dev/mcp-language-server/go.mod", - } - - for _, testPath := range testPaths { - isMatch := w.matchesPattern(testPath, watcher.GlobPattern) - logging.Debug("Test path", "path", testPath, "matches", isMatch) - } } } - // Find and open all existing files that match the newly registered patterns - // TODO: not all language servers require this, but typescript does. Make this configurable - go func() { - startTime := time.Now() - filesOpened := 0 - - err := filepath.WalkDir(w.workspacePath, func(path string, d os.DirEntry, err error) error { - if err != nil { - return err + // Determine server type for specialized handling + serverName := getServerNameFromContext(ctx) + logging.Debug("Server type detected", "serverName", serverName) + + // Check if this server has sent file watchers + hasFileWatchers := len(watchers) > 0 + + // For servers that need file preloading, we'll use a smart approach + if shouldPreloadFiles(serverName) || !hasFileWatchers { + go func() { + startTime := time.Now() + filesOpened := 0 + + // Determine max files to open based on server type + maxFilesToOpen := 50 // Default conservative limit + + switch serverName { + case "typescript", "typescript-language-server", "tsserver", "vtsls": + // TypeScript servers benefit from seeing more files + maxFilesToOpen = 100 + case "java", "jdtls": + // Java servers need to see many files for project model + maxFilesToOpen = 200 + } + + // First, open high-priority files + highPriorityFilesOpened := w.openHighPriorityFiles(ctx, serverName) + filesOpened += highPriorityFilesOpened + + if cnf.DebugLSP { + logging.Debug("Opened high-priority files", + "count", highPriorityFilesOpened, + "serverName", serverName) } + + // If we've already opened enough high-priority files, we might not need more + if filesOpened >= maxFilesToOpen { + if cnf.DebugLSP { + logging.Debug("Reached file limit with high-priority files", + "filesOpened", filesOpened, + "maxFiles", maxFilesToOpen) + } + return + } + + // For the remaining slots, walk the directory and open matching files + + err := filepath.WalkDir(w.workspacePath, func(path string, d os.DirEntry, err error) error { + if err != nil { + return err + } - // Skip directories that should be excluded - if d.IsDir() { - if path != w.workspacePath && shouldExcludeDir(path) { - if cnf.DebugLSP { - logging.Debug("Skipping excluded directory", "path", path) + // Skip directories that should be excluded + if d.IsDir() { + if path != w.workspacePath && shouldExcludeDir(path) { + if cnf.DebugLSP { + logging.Debug("Skipping excluded directory", "path", path) + } + return filepath.SkipDir + } + } else { + // Process files, but limit the total number + if filesOpened < maxFilesToOpen { + // Only process if it's not already open (high-priority files were opened earlier) + if !w.client.IsFileOpen(path) { + w.openMatchingFile(ctx, path) + filesOpened++ + + // Add a small delay after every 10 files to prevent overwhelming the server + if filesOpened%10 == 0 { + time.Sleep(50 * time.Millisecond) + } + } + } else { + // We've reached our limit, stop walking + return filepath.SkipAll } - return filepath.SkipDir } - } else { - // Process files - w.openMatchingFile(ctx, path) - filesOpened++ - // Add a small delay after every 100 files to prevent overwhelming the server - if filesOpened%100 == 0 { - time.Sleep(10 * time.Millisecond) - } + return nil + }) + + elapsedTime := time.Since(startTime) + if cnf.DebugLSP { + logging.Debug("Limited workspace scan complete", + "filesOpened", filesOpened, + "maxFiles", maxFilesToOpen, + "elapsedTime", elapsedTime.Seconds(), + "workspacePath", w.workspacePath, + ) } - return nil - }) + if err != nil && cnf.DebugLSP { + logging.Debug("Error scanning workspace for files to open", "error", err) + } + }() + } else if cnf.DebugLSP { + logging.Debug("Using on-demand file loading for server", "server", serverName) + } +} - elapsedTime := time.Since(startTime) - if cnf.DebugLSP { - logging.Debug("Workspace scan complete", - "filesOpened", filesOpened, - "elapsedTime", elapsedTime.Seconds(), - "workspacePath", w.workspacePath, - ) +// openHighPriorityFiles opens important files for the server type +// Returns the number of files opened +func (w *WorkspaceWatcher) openHighPriorityFiles(ctx context.Context, serverName string) int { + cnf := config.Get() + filesOpened := 0 + + // Define patterns for high-priority files based on server type + var patterns []string + + switch serverName { + case "typescript", "typescript-language-server", "tsserver", "vtsls": + patterns = []string{ + "**/tsconfig.json", + "**/package.json", + "**/jsconfig.json", + "**/index.ts", + "**/index.js", + "**/main.ts", + "**/main.js", } - - if err != nil && cnf.DebugLSP { - logging.Debug("Error scanning workspace for files to open", "error", err) + case "gopls": + patterns = []string{ + "**/go.mod", + "**/go.sum", + "**/main.go", + } + case "rust-analyzer": + patterns = []string{ + "**/Cargo.toml", + "**/Cargo.lock", + "**/src/lib.rs", + "**/src/main.rs", + } + case "python", "pyright", "pylsp": + patterns = []string{ + "**/pyproject.toml", + "**/setup.py", + "**/requirements.txt", + "**/__init__.py", + "**/__main__.py", + } + case "clangd": + patterns = []string{ + "**/CMakeLists.txt", + "**/Makefile", + "**/compile_commands.json", + } + case "java", "jdtls": + patterns = []string{ + "**/pom.xml", + "**/build.gradle", + "**/src/main/java/**/*.java", } - }() + default: + // For unknown servers, use common configuration files + patterns = []string{ + "**/package.json", + "**/Makefile", + "**/CMakeLists.txt", + "**/.editorconfig", + } + } + + // For each pattern, find and open matching files + for _, pattern := range patterns { + // Use doublestar.Glob to find files matching the pattern (supports ** patterns) + matches, err := doublestar.Glob(os.DirFS(w.workspacePath), pattern) + if err != nil { + if cnf.DebugLSP { + logging.Debug("Error finding high-priority files", "pattern", pattern, "error", err) + } + continue + } + + for _, match := range matches { + // Convert relative path to absolute + fullPath := filepath.Join(w.workspacePath, match) + + // Skip directories and excluded files + info, err := os.Stat(fullPath) + if err != nil || info.IsDir() || shouldExcludeFile(fullPath) { + continue + } + + // Open the file + if err := w.client.OpenFile(ctx, fullPath); err != nil { + if cnf.DebugLSP { + logging.Debug("Error opening high-priority file", "path", fullPath, "error", err) + } + } else { + filesOpened++ + if cnf.DebugLSP { + logging.Debug("Opened high-priority file", "path", fullPath) + } + } + + // Add a small delay to prevent overwhelming the server + time.Sleep(20 * time.Millisecond) + + // Limit the number of files opened per pattern + if filesOpened >= 5 && (serverName != "java" && serverName != "jdtls") { + break + } + } + } + + return filesOpened } // WatchWorkspace sets up file watching for a workspace @@ -155,6 +308,18 @@ func (w *WorkspaceWatcher) WatchWorkspace(ctx context.Context, workspacePath str cnf := config.Get() w.workspacePath = workspacePath + // Store the watcher in the context for later use + ctx = context.WithValue(ctx, "workspaceWatcher", w) + + // If the server name isn't already in the context, try to detect it + if _, ok := ctx.Value("serverName").(string); !ok { + serverName := getServerNameFromContext(ctx) + ctx = context.WithValue(ctx, "serverName", serverName) + } + + serverName := getServerNameFromContext(ctx) + logging.Debug("Starting workspace watcher", "workspacePath", workspacePath, "serverName", serverName) + // Register handler for file watcher registrations from the server lsp.RegisterFileWatchHandler(func(id string, watchers []protocol.FileSystemWatcher) { w.AddRegistrations(ctx, id, watchers) @@ -510,6 +675,57 @@ func (w *WorkspaceWatcher) notifyFileEvent(ctx context.Context, uri string, chan return w.client.DidChangeWatchedFiles(ctx, params) } +// getServerNameFromContext extracts the server name from the context +// This is a best-effort function that tries to identify which LSP server we're dealing with +func getServerNameFromContext(ctx context.Context) string { + // First check if the server name is directly stored in the context + if serverName, ok := ctx.Value("serverName").(string); ok && serverName != "" { + return strings.ToLower(serverName) + } + + // Otherwise, try to extract server name from the client command path + if w, ok := ctx.Value("workspaceWatcher").(*WorkspaceWatcher); ok && w != nil && w.client != nil && w.client.Cmd != nil { + path := strings.ToLower(w.client.Cmd.Path) + + // Extract server name from path + if strings.Contains(path, "typescript") || strings.Contains(path, "tsserver") || strings.Contains(path, "vtsls") { + return "typescript" + } else if strings.Contains(path, "gopls") { + return "gopls" + } else if strings.Contains(path, "rust-analyzer") { + return "rust-analyzer" + } else if strings.Contains(path, "pyright") || strings.Contains(path, "pylsp") || strings.Contains(path, "python") { + return "python" + } else if strings.Contains(path, "clangd") { + return "clangd" + } else if strings.Contains(path, "jdtls") || strings.Contains(path, "java") { + return "java" + } + + // Return the base name as fallback + return filepath.Base(path) + } + + return "unknown" +} + +// shouldPreloadFiles determines if we should preload files for a specific language server +// Some servers work better with preloaded files, others don't need it +func shouldPreloadFiles(serverName string) bool { + // TypeScript/JavaScript servers typically need some files preloaded + // to properly resolve imports and provide intellisense + switch serverName { + case "typescript", "typescript-language-server", "tsserver", "vtsls": + return true + case "java", "jdtls": + // Java servers often need to see source files to build the project model + return true + default: + // For most servers, we'll use lazy loading by default + return false + } +} + // Common patterns for directories and files to exclude // TODO: make configurable var ( @@ -647,9 +863,119 @@ func (w *WorkspaceWatcher) openMatchingFile(ctx context.Context, path string) { // Check if this path should be watched according to server registrations if watched, _ := w.isPathWatched(path); watched { - // Don't need to check if it's already open - the client.OpenFile handles that - if err := w.client.OpenFile(ctx, path); err != nil && cnf.DebugLSP { - logging.Error("Error opening file", "path", path, "error", err) + // Get server name for specialized handling + serverName := getServerNameFromContext(ctx) + + // Check if the file is a high-priority file that should be opened immediately + // This helps with project initialization for certain language servers + if isHighPriorityFile(path, serverName) { + if cnf.DebugLSP { + logging.Debug("Opening high-priority file", "path", path, "serverName", serverName) + } + if err := w.client.OpenFile(ctx, path); err != nil && cnf.DebugLSP { + logging.Error("Error opening high-priority file", "path", path, "error", err) + } + return + } + + // For non-high-priority files, we'll use different strategies based on server type + if shouldPreloadFiles(serverName) { + // For servers that benefit from preloading, open files but with limits + + // Check file size - for preloading we're more conservative + if info.Size() > (1 * 1024 * 1024) { // 1MB limit for preloaded files + if cnf.DebugLSP { + logging.Debug("Skipping large file for preloading", "path", path, "size", info.Size()) + } + return + } + + // Check file extension for common source files + ext := strings.ToLower(filepath.Ext(path)) + + // Only preload source files for the specific language + shouldOpen := false + + switch serverName { + case "typescript", "typescript-language-server", "tsserver", "vtsls": + shouldOpen = ext == ".ts" || ext == ".js" || ext == ".tsx" || ext == ".jsx" + case "gopls": + shouldOpen = ext == ".go" + case "rust-analyzer": + shouldOpen = ext == ".rs" + case "python", "pyright", "pylsp": + shouldOpen = ext == ".py" + case "clangd": + shouldOpen = ext == ".c" || ext == ".cpp" || ext == ".h" || ext == ".hpp" + case "java", "jdtls": + shouldOpen = ext == ".java" + default: + // For unknown servers, be conservative + shouldOpen = false + } + + if shouldOpen { + // Don't need to check if it's already open - the client.OpenFile handles that + if err := w.client.OpenFile(ctx, path); err != nil && cnf.DebugLSP { + logging.Error("Error opening file", "path", path, "error", err) + } + } } } } + +// isHighPriorityFile determines if a file should be opened immediately +// regardless of the preloading strategy +func isHighPriorityFile(path string, serverName string) bool { + fileName := filepath.Base(path) + ext := filepath.Ext(path) + + switch serverName { + case "typescript", "typescript-language-server", "tsserver", "vtsls": + // For TypeScript, we want to open configuration files immediately + return fileName == "tsconfig.json" || + fileName == "package.json" || + fileName == "jsconfig.json" || + // Also open main entry points + fileName == "index.ts" || + fileName == "index.js" || + fileName == "main.ts" || + fileName == "main.js" + case "gopls": + // For Go, we want to open go.mod files immediately + return fileName == "go.mod" || + fileName == "go.sum" || + // Also open main.go files + fileName == "main.go" + case "rust-analyzer": + // For Rust, we want to open Cargo.toml files immediately + return fileName == "Cargo.toml" || + fileName == "Cargo.lock" || + // Also open lib.rs and main.rs + fileName == "lib.rs" || + fileName == "main.rs" + case "python", "pyright", "pylsp": + // For Python, open key project files + return fileName == "pyproject.toml" || + fileName == "setup.py" || + fileName == "requirements.txt" || + fileName == "__init__.py" || + fileName == "__main__.py" + case "clangd": + // For C/C++, open key project files + return fileName == "CMakeLists.txt" || + fileName == "Makefile" || + fileName == "compile_commands.json" + case "java", "jdtls": + // For Java, open key project files + return fileName == "pom.xml" || + fileName == "build.gradle" || + ext == ".java" // Java servers often need to see source files + } + + // For unknown servers, prioritize common configuration files + return fileName == "package.json" || + fileName == "Makefile" || + fileName == "CMakeLists.txt" || + fileName == ".editorconfig" +} -- cgit v1.2.3