client.go 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811
  1. package lsp
  2. import (
  3. "bufio"
  4. "context"
  5. "encoding/json"
  6. "fmt"
  7. "io"
  8. "log/slog"
  9. "os"
  10. "os/exec"
  11. "path/filepath"
  12. "strings"
  13. "sync"
  14. "sync/atomic"
  15. "time"
  16. "github.com/charmbracelet/crush/internal/config"
  17. "github.com/charmbracelet/crush/internal/log"
  18. "github.com/charmbracelet/crush/internal/lsp/protocol"
  19. )
  20. type Client struct {
  21. Cmd *exec.Cmd
  22. stdin io.WriteCloser
  23. stdout *bufio.Reader
  24. stderr io.ReadCloser
  25. // Client name for identification
  26. name string
  27. // Diagnostic change callback
  28. onDiagnosticsChanged func(name string, count int)
  29. // Request ID counter
  30. nextID atomic.Int32
  31. // Response handlers
  32. handlers map[int32]chan *Message
  33. handlersMu sync.RWMutex
  34. // Server request handlers
  35. serverRequestHandlers map[string]ServerRequestHandler
  36. serverHandlersMu sync.RWMutex
  37. // Notification handlers
  38. notificationHandlers map[string]NotificationHandler
  39. notificationMu sync.RWMutex
  40. // Diagnostic cache
  41. diagnostics map[protocol.DocumentURI][]protocol.Diagnostic
  42. diagnosticsMu sync.RWMutex
  43. // Files are currently opened by the LSP
  44. openFiles map[string]*OpenFileInfo
  45. openFilesMu sync.RWMutex
  46. // Server state
  47. serverState atomic.Value
  48. }
  49. func NewClient(ctx context.Context, name, command string, args ...string) (*Client, error) {
  50. cmd := exec.CommandContext(ctx, command, args...)
  51. // Copy env
  52. cmd.Env = os.Environ()
  53. stdin, err := cmd.StdinPipe()
  54. if err != nil {
  55. return nil, fmt.Errorf("failed to create stdin pipe: %w", err)
  56. }
  57. stdout, err := cmd.StdoutPipe()
  58. if err != nil {
  59. return nil, fmt.Errorf("failed to create stdout pipe: %w", err)
  60. }
  61. stderr, err := cmd.StderrPipe()
  62. if err != nil {
  63. return nil, fmt.Errorf("failed to create stderr pipe: %w", err)
  64. }
  65. client := &Client{
  66. Cmd: cmd,
  67. name: name,
  68. stdin: stdin,
  69. stdout: bufio.NewReader(stdout),
  70. stderr: stderr,
  71. handlers: make(map[int32]chan *Message),
  72. notificationHandlers: make(map[string]NotificationHandler),
  73. serverRequestHandlers: make(map[string]ServerRequestHandler),
  74. diagnostics: make(map[protocol.DocumentURI][]protocol.Diagnostic),
  75. openFiles: make(map[string]*OpenFileInfo),
  76. }
  77. // Initialize server state
  78. client.serverState.Store(StateStarting)
  79. // Start the LSP server process
  80. if err := cmd.Start(); err != nil {
  81. return nil, fmt.Errorf("failed to start LSP server: %w", err)
  82. }
  83. // Handle stderr in a separate goroutine
  84. go func() {
  85. scanner := bufio.NewScanner(stderr)
  86. for scanner.Scan() {
  87. slog.Error("LSP Server", "err", scanner.Text())
  88. }
  89. if err := scanner.Err(); err != nil {
  90. slog.Error("Error reading", "err", err)
  91. }
  92. }()
  93. // Start message handling loop
  94. go func() {
  95. defer log.RecoverPanic("LSP-message-handler", func() {
  96. slog.Error("LSP message handler crashed, LSP functionality may be impaired")
  97. })
  98. client.handleMessages()
  99. }()
  100. return client, nil
  101. }
  102. func (c *Client) RegisterNotificationHandler(method string, handler NotificationHandler) {
  103. c.notificationMu.Lock()
  104. defer c.notificationMu.Unlock()
  105. c.notificationHandlers[method] = handler
  106. }
  107. func (c *Client) RegisterServerRequestHandler(method string, handler ServerRequestHandler) {
  108. c.serverHandlersMu.Lock()
  109. defer c.serverHandlersMu.Unlock()
  110. c.serverRequestHandlers[method] = handler
  111. }
  112. func (c *Client) InitializeLSPClient(ctx context.Context, workspaceDir string) (*protocol.InitializeResult, error) {
  113. initParams := &protocol.InitializeParams{
  114. WorkspaceFoldersInitializeParams: protocol.WorkspaceFoldersInitializeParams{
  115. WorkspaceFolders: []protocol.WorkspaceFolder{
  116. {
  117. URI: protocol.URI(protocol.URIFromPath(workspaceDir)),
  118. Name: workspaceDir,
  119. },
  120. },
  121. },
  122. XInitializeParams: protocol.XInitializeParams{
  123. ProcessID: int32(os.Getpid()),
  124. ClientInfo: &protocol.ClientInfo{
  125. Name: "mcp-language-server",
  126. Version: "0.1.0",
  127. },
  128. RootPath: workspaceDir,
  129. RootURI: protocol.URIFromPath(workspaceDir),
  130. Capabilities: protocol.ClientCapabilities{
  131. Workspace: protocol.WorkspaceClientCapabilities{
  132. Configuration: true,
  133. DidChangeConfiguration: protocol.DidChangeConfigurationClientCapabilities{
  134. DynamicRegistration: true,
  135. },
  136. DidChangeWatchedFiles: protocol.DidChangeWatchedFilesClientCapabilities{
  137. DynamicRegistration: true,
  138. RelativePatternSupport: true,
  139. },
  140. },
  141. TextDocument: protocol.TextDocumentClientCapabilities{
  142. Synchronization: &protocol.TextDocumentSyncClientCapabilities{
  143. DynamicRegistration: true,
  144. DidSave: true,
  145. },
  146. Completion: protocol.CompletionClientCapabilities{
  147. CompletionItem: protocol.ClientCompletionItemOptions{},
  148. },
  149. CodeLens: &protocol.CodeLensClientCapabilities{
  150. DynamicRegistration: true,
  151. },
  152. DocumentSymbol: protocol.DocumentSymbolClientCapabilities{},
  153. CodeAction: protocol.CodeActionClientCapabilities{
  154. CodeActionLiteralSupport: protocol.ClientCodeActionLiteralOptions{
  155. CodeActionKind: protocol.ClientCodeActionKindOptions{
  156. ValueSet: []protocol.CodeActionKind{},
  157. },
  158. },
  159. },
  160. PublishDiagnostics: protocol.PublishDiagnosticsClientCapabilities{
  161. VersionSupport: true,
  162. },
  163. SemanticTokens: protocol.SemanticTokensClientCapabilities{
  164. Requests: protocol.ClientSemanticTokensRequestOptions{
  165. Range: &protocol.Or_ClientSemanticTokensRequestOptions_range{},
  166. Full: &protocol.Or_ClientSemanticTokensRequestOptions_full{},
  167. },
  168. TokenTypes: []string{},
  169. TokenModifiers: []string{},
  170. Formats: []protocol.TokenFormat{},
  171. },
  172. },
  173. Window: protocol.WindowClientCapabilities{},
  174. },
  175. InitializationOptions: map[string]any{
  176. "codelenses": map[string]bool{
  177. "generate": true,
  178. "regenerate_cgo": true,
  179. "test": true,
  180. "tidy": true,
  181. "upgrade_dependency": true,
  182. "vendor": true,
  183. "vulncheck": false,
  184. },
  185. },
  186. },
  187. }
  188. var result protocol.InitializeResult
  189. if err := c.Call(ctx, "initialize", initParams, &result); err != nil {
  190. return nil, fmt.Errorf("initialize failed: %w", err)
  191. }
  192. if err := c.Notify(ctx, "initialized", struct{}{}); err != nil {
  193. return nil, fmt.Errorf("initialized notification failed: %w", err)
  194. }
  195. // Register handlers
  196. c.RegisterServerRequestHandler("workspace/applyEdit", HandleApplyEdit)
  197. c.RegisterServerRequestHandler("workspace/configuration", HandleWorkspaceConfiguration)
  198. c.RegisterServerRequestHandler("client/registerCapability", HandleRegisterCapability)
  199. c.RegisterNotificationHandler("window/showMessage", HandleServerMessage)
  200. c.RegisterNotificationHandler("textDocument/publishDiagnostics",
  201. func(params json.RawMessage) { HandleDiagnostics(c, params) })
  202. // Notify the LSP server
  203. err := c.Initialized(ctx, protocol.InitializedParams{})
  204. if err != nil {
  205. return nil, fmt.Errorf("initialization failed: %w", err)
  206. }
  207. return &result, nil
  208. }
  209. func (c *Client) Close() error {
  210. // Try to close all open files first
  211. ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
  212. defer cancel()
  213. // Attempt to close files but continue shutdown regardless
  214. c.CloseAllFiles(ctx)
  215. // Close stdin to signal the server
  216. if err := c.stdin.Close(); err != nil {
  217. return fmt.Errorf("failed to close stdin: %w", err)
  218. }
  219. // Use a channel to handle the Wait with timeout
  220. done := make(chan error, 1)
  221. go func() {
  222. done <- c.Cmd.Wait()
  223. }()
  224. // Wait for process to exit with timeout
  225. select {
  226. case err := <-done:
  227. return err
  228. case <-time.After(2 * time.Second):
  229. // If we timeout, try to kill the process
  230. if err := c.Cmd.Process.Kill(); err != nil {
  231. return fmt.Errorf("failed to kill process: %w", err)
  232. }
  233. return fmt.Errorf("process killed after timeout")
  234. }
  235. }
  236. type ServerState int
  237. const (
  238. StateStarting ServerState = iota
  239. StateReady
  240. StateError
  241. )
  242. // GetServerState returns the current state of the LSP server
  243. func (c *Client) GetServerState() ServerState {
  244. if val := c.serverState.Load(); val != nil {
  245. return val.(ServerState)
  246. }
  247. return StateStarting
  248. }
  249. // SetServerState sets the current state of the LSP server
  250. func (c *Client) SetServerState(state ServerState) {
  251. c.serverState.Store(state)
  252. }
  253. // GetName returns the name of the LSP client
  254. func (c *Client) GetName() string {
  255. return c.name
  256. }
  257. // SetDiagnosticsCallback sets the callback function for diagnostic changes
  258. func (c *Client) SetDiagnosticsCallback(callback func(name string, count int)) {
  259. c.onDiagnosticsChanged = callback
  260. }
  261. // WaitForServerReady waits for the server to be ready by polling the server
  262. // with a simple request until it responds successfully or times out
  263. func (c *Client) WaitForServerReady(ctx context.Context) error {
  264. cfg := config.Get()
  265. // Set initial state
  266. c.SetServerState(StateStarting)
  267. // Create a context with timeout
  268. ctx, cancel := context.WithTimeout(ctx, 30*time.Second)
  269. defer cancel()
  270. // Try to ping the server with a simple request
  271. ticker := time.NewTicker(500 * time.Millisecond)
  272. defer ticker.Stop()
  273. if cfg.Options.DebugLSP {
  274. slog.Debug("Waiting for LSP server to be ready...")
  275. }
  276. // Determine server type for specialized initialization
  277. serverType := c.detectServerType()
  278. // For TypeScript-like servers, we need to open some key files first
  279. if serverType == ServerTypeTypeScript {
  280. if cfg.Options.DebugLSP {
  281. slog.Debug("TypeScript-like server detected, opening key configuration files")
  282. }
  283. c.openKeyConfigFiles(ctx)
  284. }
  285. for {
  286. select {
  287. case <-ctx.Done():
  288. c.SetServerState(StateError)
  289. return fmt.Errorf("timeout waiting for LSP server to be ready")
  290. case <-ticker.C:
  291. // Try a ping method appropriate for this server type
  292. err := c.pingServerByType(ctx, serverType)
  293. if err == nil {
  294. // Server responded successfully
  295. c.SetServerState(StateReady)
  296. if cfg.Options.DebugLSP {
  297. slog.Debug("LSP server is ready")
  298. }
  299. return nil
  300. } else {
  301. slog.Debug("LSP server not ready yet", "error", err, "serverType", serverType)
  302. }
  303. if cfg.Options.DebugLSP {
  304. slog.Debug("LSP server not ready yet", "error", err, "serverType", serverType)
  305. }
  306. }
  307. }
  308. }
  309. // ServerType represents the type of LSP server
  310. type ServerType int
  311. const (
  312. ServerTypeUnknown ServerType = iota
  313. ServerTypeGo
  314. ServerTypeTypeScript
  315. ServerTypeRust
  316. ServerTypePython
  317. ServerTypeGeneric
  318. )
  319. // detectServerType tries to determine what type of LSP server we're dealing with
  320. func (c *Client) detectServerType() ServerType {
  321. if c.Cmd == nil {
  322. return ServerTypeUnknown
  323. }
  324. cmdPath := strings.ToLower(c.Cmd.Path)
  325. switch {
  326. case strings.Contains(cmdPath, "gopls"):
  327. return ServerTypeGo
  328. case strings.Contains(cmdPath, "typescript") || strings.Contains(cmdPath, "vtsls") || strings.Contains(cmdPath, "tsserver"):
  329. return ServerTypeTypeScript
  330. case strings.Contains(cmdPath, "rust-analyzer"):
  331. return ServerTypeRust
  332. case strings.Contains(cmdPath, "pyright") || strings.Contains(cmdPath, "pylsp") || strings.Contains(cmdPath, "python"):
  333. return ServerTypePython
  334. default:
  335. return ServerTypeGeneric
  336. }
  337. }
  338. // openKeyConfigFiles opens important configuration files that help initialize the server
  339. func (c *Client) openKeyConfigFiles(ctx context.Context) {
  340. workDir := config.Get().WorkingDir()
  341. serverType := c.detectServerType()
  342. var filesToOpen []string
  343. switch serverType {
  344. case ServerTypeTypeScript:
  345. // TypeScript servers need these config files to properly initialize
  346. filesToOpen = []string{
  347. filepath.Join(workDir, "tsconfig.json"),
  348. filepath.Join(workDir, "package.json"),
  349. filepath.Join(workDir, "jsconfig.json"),
  350. }
  351. // Also find and open a few TypeScript files to help the server initialize
  352. c.openTypeScriptFiles(ctx, workDir)
  353. case ServerTypeGo:
  354. filesToOpen = []string{
  355. filepath.Join(workDir, "go.mod"),
  356. filepath.Join(workDir, "go.sum"),
  357. }
  358. case ServerTypeRust:
  359. filesToOpen = []string{
  360. filepath.Join(workDir, "Cargo.toml"),
  361. filepath.Join(workDir, "Cargo.lock"),
  362. }
  363. }
  364. // Try to open each file, ignoring errors if they don't exist
  365. for _, file := range filesToOpen {
  366. if _, err := os.Stat(file); err == nil {
  367. // File exists, try to open it
  368. if err := c.OpenFile(ctx, file); err != nil {
  369. slog.Debug("Failed to open key config file", "file", file, "error", err)
  370. } else {
  371. slog.Debug("Opened key config file for initialization", "file", file)
  372. }
  373. }
  374. }
  375. }
  376. // pingServerByType sends a ping request appropriate for the server type
  377. func (c *Client) pingServerByType(ctx context.Context, serverType ServerType) error {
  378. switch serverType {
  379. case ServerTypeTypeScript:
  380. // For TypeScript, try a document symbol request on an open file
  381. return c.pingTypeScriptServer(ctx)
  382. case ServerTypeGo:
  383. // For Go, workspace/symbol works well
  384. return c.pingWithWorkspaceSymbol(ctx)
  385. case ServerTypeRust:
  386. // For Rust, workspace/symbol works well
  387. return c.pingWithWorkspaceSymbol(ctx)
  388. default:
  389. // Default ping method
  390. return c.pingWithWorkspaceSymbol(ctx)
  391. }
  392. }
  393. // pingTypeScriptServer tries to ping a TypeScript server with appropriate methods
  394. func (c *Client) pingTypeScriptServer(ctx context.Context) error {
  395. // First try workspace/symbol which works for many servers
  396. if err := c.pingWithWorkspaceSymbol(ctx); err == nil {
  397. return nil
  398. }
  399. // If that fails, try to find an open file and request document symbols
  400. c.openFilesMu.RLock()
  401. defer c.openFilesMu.RUnlock()
  402. // If we have any open files, try to get document symbols for one
  403. for uri := range c.openFiles {
  404. filePath, err := protocol.DocumentURI(uri).Path()
  405. if err != nil {
  406. slog.Error("Failed to convert URI to path for TypeScript symbol collection", "uri", uri, "error", err)
  407. continue
  408. }
  409. if strings.HasSuffix(filePath, ".ts") || strings.HasSuffix(filePath, ".js") ||
  410. strings.HasSuffix(filePath, ".tsx") || strings.HasSuffix(filePath, ".jsx") {
  411. var symbols []protocol.DocumentSymbol
  412. err := c.Call(ctx, "textDocument/documentSymbol", protocol.DocumentSymbolParams{
  413. TextDocument: protocol.TextDocumentIdentifier{
  414. URI: protocol.DocumentURI(uri),
  415. },
  416. }, &symbols)
  417. if err == nil {
  418. return nil
  419. }
  420. }
  421. }
  422. // If we have no open TypeScript files, try to find and open one
  423. workDir := config.Get().WorkingDir()
  424. err := filepath.WalkDir(workDir, func(path string, d os.DirEntry, err error) error {
  425. if err != nil {
  426. return err
  427. }
  428. // Skip directories and non-TypeScript files
  429. if d.IsDir() {
  430. return nil
  431. }
  432. ext := filepath.Ext(path)
  433. if ext == ".ts" || ext == ".js" || ext == ".tsx" || ext == ".jsx" {
  434. // Found a TypeScript file, try to open it
  435. if err := c.OpenFile(ctx, path); err == nil {
  436. // Successfully opened, stop walking
  437. return filepath.SkipAll
  438. }
  439. }
  440. return nil
  441. })
  442. if err != nil {
  443. slog.Debug("Error walking directory for TypeScript files", "error", err)
  444. }
  445. // Final fallback - just try a generic capability
  446. return c.pingWithServerCapabilities(ctx)
  447. }
  448. // openTypeScriptFiles finds and opens TypeScript files to help initialize the server
  449. func (c *Client) openTypeScriptFiles(ctx context.Context, workDir string) {
  450. cfg := config.Get()
  451. filesOpened := 0
  452. maxFilesToOpen := 5 // Limit to a reasonable number of files
  453. // Find and open TypeScript files
  454. err := filepath.WalkDir(workDir, func(path string, d os.DirEntry, err error) error {
  455. if err != nil {
  456. return err
  457. }
  458. // Skip directories and non-TypeScript files
  459. if d.IsDir() {
  460. // Skip common directories to avoid wasting time
  461. if shouldSkipDir(path) {
  462. return filepath.SkipDir
  463. }
  464. return nil
  465. }
  466. // Check if we've opened enough files
  467. if filesOpened >= maxFilesToOpen {
  468. return filepath.SkipAll
  469. }
  470. // Check file extension
  471. ext := filepath.Ext(path)
  472. if ext == ".ts" || ext == ".tsx" || ext == ".js" || ext == ".jsx" {
  473. // Try to open the file
  474. if err := c.OpenFile(ctx, path); err == nil {
  475. filesOpened++
  476. if cfg.Options.DebugLSP {
  477. slog.Debug("Opened TypeScript file for initialization", "file", path)
  478. }
  479. }
  480. }
  481. return nil
  482. })
  483. if err != nil && cfg.Options.DebugLSP {
  484. slog.Debug("Error walking directory for TypeScript files", "error", err)
  485. }
  486. if cfg.Options.DebugLSP {
  487. slog.Debug("Opened TypeScript files for initialization", "count", filesOpened)
  488. }
  489. }
  490. // shouldSkipDir returns true if the directory should be skipped during file search
  491. func shouldSkipDir(path string) bool {
  492. dirName := filepath.Base(path)
  493. // Skip hidden directories
  494. if strings.HasPrefix(dirName, ".") {
  495. return true
  496. }
  497. // Skip common directories that won't contain relevant source files
  498. skipDirs := map[string]bool{
  499. "node_modules": true,
  500. "dist": true,
  501. "build": true,
  502. "coverage": true,
  503. "vendor": true,
  504. "target": true,
  505. }
  506. return skipDirs[dirName]
  507. }
  508. // pingWithWorkspaceSymbol tries a workspace/symbol request
  509. func (c *Client) pingWithWorkspaceSymbol(ctx context.Context) error {
  510. var result []protocol.SymbolInformation
  511. return c.Call(ctx, "workspace/symbol", protocol.WorkspaceSymbolParams{
  512. Query: "",
  513. }, &result)
  514. }
  515. // pingWithServerCapabilities tries to get server capabilities
  516. func (c *Client) pingWithServerCapabilities(ctx context.Context) error {
  517. // This is a very lightweight request that should work for most servers
  518. return c.Notify(ctx, "$/cancelRequest", struct{ ID int }{ID: -1})
  519. }
  520. type OpenFileInfo struct {
  521. Version int32
  522. URI protocol.DocumentURI
  523. }
  524. func (c *Client) OpenFile(ctx context.Context, filepath string) error {
  525. uri := string(protocol.URIFromPath(filepath))
  526. c.openFilesMu.Lock()
  527. if _, exists := c.openFiles[uri]; exists {
  528. c.openFilesMu.Unlock()
  529. return nil // Already open
  530. }
  531. c.openFilesMu.Unlock()
  532. // Skip files that do not exist or cannot be read
  533. content, err := os.ReadFile(filepath)
  534. if err != nil {
  535. return fmt.Errorf("error reading file: %w", err)
  536. }
  537. params := protocol.DidOpenTextDocumentParams{
  538. TextDocument: protocol.TextDocumentItem{
  539. URI: protocol.DocumentURI(uri),
  540. LanguageID: DetectLanguageID(uri),
  541. Version: 1,
  542. Text: string(content),
  543. },
  544. }
  545. if err := c.Notify(ctx, "textDocument/didOpen", params); err != nil {
  546. return err
  547. }
  548. c.openFilesMu.Lock()
  549. c.openFiles[uri] = &OpenFileInfo{
  550. Version: 1,
  551. URI: protocol.DocumentURI(uri),
  552. }
  553. c.openFilesMu.Unlock()
  554. return nil
  555. }
  556. func (c *Client) NotifyChange(ctx context.Context, filepath string) error {
  557. uri := string(protocol.URIFromPath(filepath))
  558. content, err := os.ReadFile(filepath)
  559. if err != nil {
  560. return fmt.Errorf("error reading file: %w", err)
  561. }
  562. c.openFilesMu.Lock()
  563. fileInfo, isOpen := c.openFiles[uri]
  564. if !isOpen {
  565. c.openFilesMu.Unlock()
  566. return fmt.Errorf("cannot notify change for unopened file: %s", filepath)
  567. }
  568. // Increment version
  569. fileInfo.Version++
  570. version := fileInfo.Version
  571. c.openFilesMu.Unlock()
  572. params := protocol.DidChangeTextDocumentParams{
  573. TextDocument: protocol.VersionedTextDocumentIdentifier{
  574. TextDocumentIdentifier: protocol.TextDocumentIdentifier{
  575. URI: protocol.DocumentURI(uri),
  576. },
  577. Version: version,
  578. },
  579. ContentChanges: []protocol.TextDocumentContentChangeEvent{
  580. {
  581. Value: protocol.TextDocumentContentChangeWholeDocument{
  582. Text: string(content),
  583. },
  584. },
  585. },
  586. }
  587. return c.Notify(ctx, "textDocument/didChange", params)
  588. }
  589. func (c *Client) CloseFile(ctx context.Context, filepath string) error {
  590. cfg := config.Get()
  591. uri := string(protocol.URIFromPath(filepath))
  592. c.openFilesMu.Lock()
  593. if _, exists := c.openFiles[uri]; !exists {
  594. c.openFilesMu.Unlock()
  595. return nil // Already closed
  596. }
  597. c.openFilesMu.Unlock()
  598. params := protocol.DidCloseTextDocumentParams{
  599. TextDocument: protocol.TextDocumentIdentifier{
  600. URI: protocol.DocumentURI(uri),
  601. },
  602. }
  603. if cfg.Options.DebugLSP {
  604. slog.Debug("Closing file", "file", filepath)
  605. }
  606. if err := c.Notify(ctx, "textDocument/didClose", params); err != nil {
  607. return err
  608. }
  609. c.openFilesMu.Lock()
  610. delete(c.openFiles, uri)
  611. c.openFilesMu.Unlock()
  612. return nil
  613. }
  614. func (c *Client) IsFileOpen(filepath string) bool {
  615. uri := string(protocol.URIFromPath(filepath))
  616. c.openFilesMu.RLock()
  617. defer c.openFilesMu.RUnlock()
  618. _, exists := c.openFiles[uri]
  619. return exists
  620. }
  621. // CloseAllFiles closes all currently open files
  622. func (c *Client) CloseAllFiles(ctx context.Context) {
  623. cfg := config.Get()
  624. c.openFilesMu.Lock()
  625. filesToClose := make([]string, 0, len(c.openFiles))
  626. // First collect all URIs that need to be closed
  627. for uri := range c.openFiles {
  628. // Convert URI back to file path using proper URI handling
  629. filePath, err := protocol.DocumentURI(uri).Path()
  630. if err != nil {
  631. slog.Error("Failed to convert URI to path for file closing", "uri", uri, "error", err)
  632. continue
  633. }
  634. filesToClose = append(filesToClose, filePath)
  635. }
  636. c.openFilesMu.Unlock()
  637. // Then close them all
  638. for _, filePath := range filesToClose {
  639. err := c.CloseFile(ctx, filePath)
  640. if err != nil && cfg.Options.DebugLSP {
  641. slog.Warn("Error closing file", "file", filePath, "error", err)
  642. }
  643. }
  644. if cfg.Options.DebugLSP {
  645. slog.Debug("Closed all files", "files", filesToClose)
  646. }
  647. }
  648. func (c *Client) GetFileDiagnostics(uri protocol.DocumentURI) []protocol.Diagnostic {
  649. c.diagnosticsMu.RLock()
  650. defer c.diagnosticsMu.RUnlock()
  651. return c.diagnostics[uri]
  652. }
  653. // GetDiagnostics returns all diagnostics for all files
  654. func (c *Client) GetDiagnostics() map[protocol.DocumentURI][]protocol.Diagnostic {
  655. return c.diagnostics
  656. }
  657. // OpenFileOnDemand opens a file only if it's not already open
  658. // This is used for lazy-loading files when they're actually needed
  659. func (c *Client) OpenFileOnDemand(ctx context.Context, filepath string) error {
  660. // Check if the file is already open
  661. if c.IsFileOpen(filepath) {
  662. return nil
  663. }
  664. // Open the file
  665. return c.OpenFile(ctx, filepath)
  666. }
  667. // GetDiagnosticsForFile ensures a file is open and returns its diagnostics
  668. // This is useful for on-demand diagnostics when using lazy loading
  669. func (c *Client) GetDiagnosticsForFile(ctx context.Context, filepath string) ([]protocol.Diagnostic, error) {
  670. documentURI := protocol.URIFromPath(filepath)
  671. // Make sure the file is open
  672. if !c.IsFileOpen(filepath) {
  673. if err := c.OpenFile(ctx, filepath); err != nil {
  674. return nil, fmt.Errorf("failed to open file for diagnostics: %w", err)
  675. }
  676. // Give the LSP server a moment to process the file
  677. time.Sleep(100 * time.Millisecond)
  678. }
  679. // Get diagnostics
  680. c.diagnosticsMu.RLock()
  681. diagnostics := c.diagnostics[documentURI]
  682. c.diagnosticsMu.RUnlock()
  683. return diagnostics, nil
  684. }
  685. // ClearDiagnosticsForURI removes diagnostics for a specific URI from the cache
  686. func (c *Client) ClearDiagnosticsForURI(uri protocol.DocumentURI) {
  687. c.diagnosticsMu.Lock()
  688. defer c.diagnosticsMu.Unlock()
  689. delete(c.diagnostics, uri)
  690. }