| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797 |
- package lsp
- import (
- "bufio"
- "context"
- "encoding/json"
- "fmt"
- "io"
- "os"
- "os/exec"
- "path/filepath"
- "strings"
- "sync"
- "sync/atomic"
- "time"
- "log/slog"
- "github.com/sst/opencode/internal/config"
- "github.com/sst/opencode/internal/logging"
- "github.com/sst/opencode/internal/lsp/protocol"
- "github.com/sst/opencode/internal/status"
- )
- type Client struct {
- Cmd *exec.Cmd
- stdin io.WriteCloser
- stdout *bufio.Reader
- stderr io.ReadCloser
- // Request ID counter
- nextID atomic.Int32
- // Response handlers
- handlers map[int32]chan *Message
- handlersMu sync.RWMutex
- // Server request handlers
- serverRequestHandlers map[string]ServerRequestHandler
- serverHandlersMu sync.RWMutex
- // Notification handlers
- notificationHandlers map[string]NotificationHandler
- notificationMu sync.RWMutex
- // Diagnostic cache
- diagnostics map[protocol.DocumentUri][]protocol.Diagnostic
- diagnosticsMu sync.RWMutex
- // 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) {
- cmd := exec.CommandContext(ctx, command, args...)
- // Copy env
- cmd.Env = os.Environ()
- stdin, err := cmd.StdinPipe()
- if err != nil {
- return nil, fmt.Errorf("failed to create stdin pipe: %w", err)
- }
- stdout, err := cmd.StdoutPipe()
- if err != nil {
- return nil, fmt.Errorf("failed to create stdout pipe: %w", err)
- }
- stderr, err := cmd.StderrPipe()
- if err != nil {
- return nil, fmt.Errorf("failed to create stderr pipe: %w", err)
- }
- client := &Client{
- Cmd: cmd,
- stdin: stdin,
- stdout: bufio.NewReader(stdout),
- stderr: stderr,
- handlers: make(map[int32]chan *Message),
- notificationHandlers: make(map[string]NotificationHandler),
- serverRequestHandlers: make(map[string]ServerRequestHandler),
- diagnostics: make(map[protocol.DocumentUri][]protocol.Diagnostic),
- 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)
- }
- // Handle stderr in a separate goroutine
- go func() {
- scanner := bufio.NewScanner(stderr)
- for scanner.Scan() {
- slog.Info("LSP Server", "message", scanner.Text())
- }
- if err := scanner.Err(); err != nil {
- slog.Error("Error reading LSP stderr", "error", err)
- }
- }()
- // Start message handling loop
- go func() {
- defer logging.RecoverPanic("LSP-message-handler", func() {
- status.Error("LSP message handler crashed, LSP functionality may be impaired")
- })
- client.handleMessages()
- }()
- return client, nil
- }
- func (c *Client) RegisterNotificationHandler(method string, handler NotificationHandler) {
- c.notificationMu.Lock()
- defer c.notificationMu.Unlock()
- c.notificationHandlers[method] = handler
- }
- func (c *Client) RegisterServerRequestHandler(method string, handler ServerRequestHandler) {
- c.serverHandlersMu.Lock()
- defer c.serverHandlersMu.Unlock()
- c.serverRequestHandlers[method] = handler
- }
- func (c *Client) InitializeLSPClient(ctx context.Context, workspaceDir string) (*protocol.InitializeResult, error) {
- initParams := &protocol.InitializeParams{
- WorkspaceFoldersInitializeParams: protocol.WorkspaceFoldersInitializeParams{
- WorkspaceFolders: []protocol.WorkspaceFolder{
- {
- URI: protocol.URI("file://" + workspaceDir),
- Name: workspaceDir,
- },
- },
- },
- XInitializeParams: protocol.XInitializeParams{
- ProcessID: int32(os.Getpid()),
- ClientInfo: &protocol.ClientInfo{
- Name: "mcp-language-server",
- Version: "0.1.0",
- },
- RootPath: workspaceDir,
- RootURI: protocol.DocumentUri("file://" + workspaceDir),
- Capabilities: protocol.ClientCapabilities{
- Workspace: protocol.WorkspaceClientCapabilities{
- Configuration: true,
- DidChangeConfiguration: protocol.DidChangeConfigurationClientCapabilities{
- DynamicRegistration: true,
- },
- DidChangeWatchedFiles: protocol.DidChangeWatchedFilesClientCapabilities{
- DynamicRegistration: true,
- RelativePatternSupport: true,
- },
- },
- TextDocument: protocol.TextDocumentClientCapabilities{
- Synchronization: &protocol.TextDocumentSyncClientCapabilities{
- DynamicRegistration: true,
- DidSave: true,
- },
- Completion: protocol.CompletionClientCapabilities{
- CompletionItem: protocol.ClientCompletionItemOptions{},
- },
- CodeLens: &protocol.CodeLensClientCapabilities{
- DynamicRegistration: true,
- },
- DocumentSymbol: protocol.DocumentSymbolClientCapabilities{},
- CodeAction: protocol.CodeActionClientCapabilities{
- CodeActionLiteralSupport: protocol.ClientCodeActionLiteralOptions{
- CodeActionKind: protocol.ClientCodeActionKindOptions{
- ValueSet: []protocol.CodeActionKind{},
- },
- },
- },
- PublishDiagnostics: protocol.PublishDiagnosticsClientCapabilities{
- VersionSupport: true,
- },
- SemanticTokens: protocol.SemanticTokensClientCapabilities{
- Requests: protocol.ClientSemanticTokensRequestOptions{
- Range: &protocol.Or_ClientSemanticTokensRequestOptions_range{},
- Full: &protocol.Or_ClientSemanticTokensRequestOptions_full{},
- },
- TokenTypes: []string{},
- TokenModifiers: []string{},
- Formats: []protocol.TokenFormat{},
- },
- },
- Window: protocol.WindowClientCapabilities{},
- },
- InitializationOptions: map[string]any{
- "codelenses": map[string]bool{
- "generate": true,
- "regenerate_cgo": true,
- "test": true,
- "tidy": true,
- "upgrade_dependency": true,
- "vendor": true,
- "vulncheck": false,
- },
- },
- },
- }
- var result protocol.InitializeResult
- if err := c.Call(ctx, "initialize", initParams, &result); err != nil {
- return nil, fmt.Errorf("initialize failed: %w", err)
- }
- if err := c.Notify(ctx, "initialized", struct{}{}); err != nil {
- return nil, fmt.Errorf("initialized notification failed: %w", err)
- }
- // Register handlers
- c.RegisterServerRequestHandler("workspace/applyEdit", HandleApplyEdit)
- c.RegisterServerRequestHandler("workspace/configuration", HandleWorkspaceConfiguration)
- c.RegisterServerRequestHandler("client/registerCapability", HandleRegisterCapability)
- c.RegisterNotificationHandler("window/showMessage", HandleServerMessage)
- c.RegisterNotificationHandler("textDocument/publishDiagnostics",
- func(params json.RawMessage) { HandleDiagnostics(c, params) })
- // Notify the LSP server
- err := c.Initialized(ctx, protocol.InitializedParams{})
- if err != nil {
- return nil, fmt.Errorf("initialization failed: %w", err)
- }
- return &result, nil
- }
- func (c *Client) Close() error {
- // Try to close all open files first
- ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
- defer cancel()
- // Attempt to close files but continue shutdown regardless
- c.CloseAllFiles(ctx)
- // Close stdin to signal the server
- if err := c.stdin.Close(); err != nil {
- return fmt.Errorf("failed to close stdin: %w", err)
- }
- // Use a channel to handle the Wait with timeout
- done := make(chan error, 1)
- go func() {
- done <- c.Cmd.Wait()
- }()
- // Wait for process to exit with timeout
- select {
- case err := <-done:
- return err
- case <-time.After(2 * time.Second):
- // If we timeout, try to kill the process
- if err := c.Cmd.Process.Kill(); err != nil {
- return fmt.Errorf("failed to kill process: %w", err)
- }
- return fmt.Errorf("process killed after timeout")
- }
- }
- type ServerState int
- const (
- StateStarting ServerState = iota
- StateReady
- 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 {
- 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 {
- slog.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 {
- slog.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 {
- slog.Debug("LSP server is ready")
- }
- return nil
- } else {
- slog.Debug("LSP server not ready yet", "error", err, "serverType", serverType)
- }
- if cnf.DebugLSP {
- slog.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 {
- slog.Debug("Failed to open key config file", "file", file, "error", err)
- } else {
- slog.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 {
- slog.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 {
- slog.Debug("Opened TypeScript file for initialization", "file", path)
- }
- }
- }
- return nil
- })
- if err != nil && cnf.DebugLSP {
- slog.Debug("Error walking directory for TypeScript files", "error", err)
- }
- if cnf.DebugLSP {
- slog.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 {
- Version int32
- URI protocol.DocumentUri
- }
- func (c *Client) OpenFile(ctx context.Context, filepath string) error {
- uri := fmt.Sprintf("file://%s", filepath)
- c.openFilesMu.Lock()
- if _, exists := c.openFiles[uri]; exists {
- c.openFilesMu.Unlock()
- return nil // Already open
- }
- c.openFilesMu.Unlock()
- // Skip files that do not exist or cannot be read
- content, err := os.ReadFile(filepath)
- if err != nil {
- return fmt.Errorf("error reading file: %w", err)
- }
- params := protocol.DidOpenTextDocumentParams{
- TextDocument: protocol.TextDocumentItem{
- URI: protocol.DocumentUri(uri),
- LanguageID: DetectLanguageID(uri),
- Version: 1,
- Text: string(content),
- },
- }
- if err := c.Notify(ctx, "textDocument/didOpen", params); err != nil {
- return err
- }
- c.openFilesMu.Lock()
- c.openFiles[uri] = &OpenFileInfo{
- Version: 1,
- URI: protocol.DocumentUri(uri),
- }
- c.openFilesMu.Unlock()
- return nil
- }
- func (c *Client) NotifyChange(ctx context.Context, filepath string) error {
- uri := fmt.Sprintf("file://%s", filepath)
- // Verify file exists before attempting to read it
- if _, err := os.Stat(filepath); err != nil {
- if os.IsNotExist(err) {
- // File was deleted - close it in the LSP client instead of notifying change
- return c.CloseFile(ctx, filepath)
- }
- return fmt.Errorf("error checking file: %w", err)
- }
- content, err := os.ReadFile(filepath)
- if err != nil {
- return fmt.Errorf("error reading file: %w", err)
- }
- c.openFilesMu.Lock()
- fileInfo, isOpen := c.openFiles[uri]
- if !isOpen {
- c.openFilesMu.Unlock()
- return fmt.Errorf("cannot notify change for unopened file: %s", filepath)
- }
- // Increment version
- fileInfo.Version++
- version := fileInfo.Version
- c.openFilesMu.Unlock()
- params := protocol.DidChangeTextDocumentParams{
- TextDocument: protocol.VersionedTextDocumentIdentifier{
- TextDocumentIdentifier: protocol.TextDocumentIdentifier{
- URI: protocol.DocumentUri(uri),
- },
- Version: version,
- },
- ContentChanges: []protocol.TextDocumentContentChangeEvent{
- {
- Value: protocol.TextDocumentContentChangeWholeDocument{
- Text: string(content),
- },
- },
- },
- }
- return c.Notify(ctx, "textDocument/didChange", params)
- }
- func (c *Client) CloseFile(ctx context.Context, filepath string) error {
- cnf := config.Get()
- uri := fmt.Sprintf("file://%s", filepath)
- c.openFilesMu.Lock()
- if _, exists := c.openFiles[uri]; !exists {
- c.openFilesMu.Unlock()
- return nil // Already closed
- }
- c.openFilesMu.Unlock()
- params := protocol.DidCloseTextDocumentParams{
- TextDocument: protocol.TextDocumentIdentifier{
- URI: protocol.DocumentUri(uri),
- },
- }
- if cnf.DebugLSP {
- slog.Debug("Closing file", "file", filepath)
- }
- if err := c.Notify(ctx, "textDocument/didClose", params); err != nil {
- return err
- }
- c.openFilesMu.Lock()
- delete(c.openFiles, uri)
- c.openFilesMu.Unlock()
- return nil
- }
- func (c *Client) IsFileOpen(filepath string) bool {
- uri := fmt.Sprintf("file://%s", filepath)
- c.openFilesMu.RLock()
- defer c.openFilesMu.RUnlock()
- _, exists := c.openFiles[uri]
- return exists
- }
- // CloseAllFiles closes all currently open files
- func (c *Client) CloseAllFiles(ctx context.Context) {
- cnf := config.Get()
- c.openFilesMu.Lock()
- filesToClose := make([]string, 0, len(c.openFiles))
- // First collect all URIs that need to be closed
- for uri := range c.openFiles {
- // Convert URI back to file path by trimming "file://" prefix
- filePath := strings.TrimPrefix(uri, "file://")
- filesToClose = append(filesToClose, filePath)
- }
- c.openFilesMu.Unlock()
- // Then close them all
- for _, filePath := range filesToClose {
- err := c.CloseFile(ctx, filePath)
- if err != nil && cnf.DebugLSP {
- slog.Warn("Error closing file", "file", filePath, "error", err)
- }
- }
- if cnf.DebugLSP {
- slog.Debug("Closed all files", "files", filesToClose)
- }
- }
- func (c *Client) GetFileDiagnostics(uri protocol.DocumentUri) []protocol.Diagnostic {
- c.diagnosticsMu.RLock()
- defer c.diagnosticsMu.RUnlock()
- 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
- }
- // ClearDiagnosticsForURI removes diagnostics for a specific URI from the cache
- func (c *Client) ClearDiagnosticsForURI(uri protocol.DocumentUri) {
- c.diagnosticsMu.Lock()
- defer c.diagnosticsMu.Unlock()
- delete(c.diagnostics, uri)
- }
|