summaryrefslogtreecommitdiffhomepage
path: root/internal/lsp
diff options
context:
space:
mode:
Diffstat (limited to 'internal/lsp')
-rw-r--r--internal/lsp/client.go364
-rw-r--r--internal/lsp/watcher/watcher.go428
2 files changed, 728 insertions, 64 deletions
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"
+}