client.go 22 KB

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