client.go 22 KB

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