lsp improvements
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user