client.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429
  1. package lsp
  2. import (
  3. "bufio"
  4. "context"
  5. "encoding/json"
  6. "fmt"
  7. "io"
  8. "log"
  9. "os"
  10. "os/exec"
  11. "strings"
  12. "sync"
  13. "sync/atomic"
  14. "time"
  15. "github.com/kujtimiihoxha/termai/internal/lsp/protocol"
  16. )
  17. type Client struct {
  18. Cmd *exec.Cmd
  19. stdin io.WriteCloser
  20. stdout *bufio.Reader
  21. stderr io.ReadCloser
  22. // Request ID counter
  23. nextID atomic.Int32
  24. // Response handlers
  25. handlers map[int32]chan *Message
  26. handlersMu sync.RWMutex
  27. // Server request handlers
  28. serverRequestHandlers map[string]ServerRequestHandler
  29. serverHandlersMu sync.RWMutex
  30. // Notification handlers
  31. notificationHandlers map[string]NotificationHandler
  32. notificationMu sync.RWMutex
  33. // Diagnostic cache
  34. diagnostics map[protocol.DocumentUri][]protocol.Diagnostic
  35. diagnosticsMu sync.RWMutex
  36. // Files are currently opened by the LSP
  37. openFiles map[string]*OpenFileInfo
  38. openFilesMu sync.RWMutex
  39. }
  40. func NewClient(command string, args ...string) (*Client, error) {
  41. cmd := exec.Command(command, args...)
  42. // Copy env
  43. cmd.Env = os.Environ()
  44. stdin, err := cmd.StdinPipe()
  45. if err != nil {
  46. return nil, fmt.Errorf("failed to create stdin pipe: %w", err)
  47. }
  48. stdout, err := cmd.StdoutPipe()
  49. if err != nil {
  50. return nil, fmt.Errorf("failed to create stdout pipe: %w", err)
  51. }
  52. stderr, err := cmd.StderrPipe()
  53. if err != nil {
  54. return nil, fmt.Errorf("failed to create stderr pipe: %w", err)
  55. }
  56. client := &Client{
  57. Cmd: cmd,
  58. stdin: stdin,
  59. stdout: bufio.NewReader(stdout),
  60. stderr: stderr,
  61. handlers: make(map[int32]chan *Message),
  62. notificationHandlers: make(map[string]NotificationHandler),
  63. serverRequestHandlers: make(map[string]ServerRequestHandler),
  64. diagnostics: make(map[protocol.DocumentUri][]protocol.Diagnostic),
  65. openFiles: make(map[string]*OpenFileInfo),
  66. }
  67. // Start the LSP server process
  68. if err := cmd.Start(); err != nil {
  69. return nil, fmt.Errorf("failed to start LSP server: %w", err)
  70. }
  71. // Handle stderr in a separate goroutine
  72. go func() {
  73. scanner := bufio.NewScanner(stderr)
  74. for scanner.Scan() {
  75. fmt.Fprintf(os.Stderr, "LSP Server: %s\n", scanner.Text())
  76. }
  77. if err := scanner.Err(); err != nil {
  78. fmt.Fprintf(os.Stderr, "Error reading stderr: %v\n", err)
  79. }
  80. }()
  81. // Start message handling loop
  82. go client.handleMessages()
  83. return client, nil
  84. }
  85. func (c *Client) RegisterNotificationHandler(method string, handler NotificationHandler) {
  86. c.notificationMu.Lock()
  87. defer c.notificationMu.Unlock()
  88. c.notificationHandlers[method] = handler
  89. }
  90. func (c *Client) RegisterServerRequestHandler(method string, handler ServerRequestHandler) {
  91. c.serverHandlersMu.Lock()
  92. defer c.serverHandlersMu.Unlock()
  93. c.serverRequestHandlers[method] = handler
  94. }
  95. func (c *Client) InitializeLSPClient(ctx context.Context, workspaceDir string) (*protocol.InitializeResult, error) {
  96. initParams := &protocol.InitializeParams{
  97. WorkspaceFoldersInitializeParams: protocol.WorkspaceFoldersInitializeParams{
  98. WorkspaceFolders: []protocol.WorkspaceFolder{
  99. {
  100. URI: protocol.URI("file://" + workspaceDir),
  101. Name: workspaceDir,
  102. },
  103. },
  104. },
  105. XInitializeParams: protocol.XInitializeParams{
  106. ProcessID: int32(os.Getpid()),
  107. ClientInfo: &protocol.ClientInfo{
  108. Name: "mcp-language-server",
  109. Version: "0.1.0",
  110. },
  111. RootPath: workspaceDir,
  112. RootURI: protocol.DocumentUri("file://" + workspaceDir),
  113. Capabilities: protocol.ClientCapabilities{
  114. Workspace: protocol.WorkspaceClientCapabilities{
  115. Configuration: true,
  116. DidChangeConfiguration: protocol.DidChangeConfigurationClientCapabilities{
  117. DynamicRegistration: true,
  118. },
  119. DidChangeWatchedFiles: protocol.DidChangeWatchedFilesClientCapabilities{
  120. DynamicRegistration: true,
  121. RelativePatternSupport: true,
  122. },
  123. },
  124. TextDocument: protocol.TextDocumentClientCapabilities{
  125. Synchronization: &protocol.TextDocumentSyncClientCapabilities{
  126. DynamicRegistration: true,
  127. DidSave: true,
  128. },
  129. Completion: protocol.CompletionClientCapabilities{
  130. CompletionItem: protocol.ClientCompletionItemOptions{},
  131. },
  132. CodeLens: &protocol.CodeLensClientCapabilities{
  133. DynamicRegistration: true,
  134. },
  135. DocumentSymbol: protocol.DocumentSymbolClientCapabilities{},
  136. CodeAction: protocol.CodeActionClientCapabilities{
  137. CodeActionLiteralSupport: protocol.ClientCodeActionLiteralOptions{
  138. CodeActionKind: protocol.ClientCodeActionKindOptions{
  139. ValueSet: []protocol.CodeActionKind{},
  140. },
  141. },
  142. },
  143. PublishDiagnostics: protocol.PublishDiagnosticsClientCapabilities{
  144. VersionSupport: true,
  145. },
  146. SemanticTokens: protocol.SemanticTokensClientCapabilities{
  147. Requests: protocol.ClientSemanticTokensRequestOptions{
  148. Range: &protocol.Or_ClientSemanticTokensRequestOptions_range{},
  149. Full: &protocol.Or_ClientSemanticTokensRequestOptions_full{},
  150. },
  151. TokenTypes: []string{},
  152. TokenModifiers: []string{},
  153. Formats: []protocol.TokenFormat{},
  154. },
  155. },
  156. Window: protocol.WindowClientCapabilities{},
  157. },
  158. InitializationOptions: map[string]any{
  159. "codelenses": map[string]bool{
  160. "generate": true,
  161. "regenerate_cgo": true,
  162. "test": true,
  163. "tidy": true,
  164. "upgrade_dependency": true,
  165. "vendor": true,
  166. "vulncheck": false,
  167. },
  168. },
  169. },
  170. }
  171. var result protocol.InitializeResult
  172. if err := c.Call(ctx, "initialize", initParams, &result); err != nil {
  173. return nil, fmt.Errorf("initialize failed: %w", err)
  174. }
  175. if err := c.Notify(ctx, "initialized", struct{}{}); err != nil {
  176. return nil, fmt.Errorf("initialized notification failed: %w", err)
  177. }
  178. // Register handlers
  179. c.RegisterServerRequestHandler("workspace/applyEdit", HandleApplyEdit)
  180. c.RegisterServerRequestHandler("workspace/configuration", HandleWorkspaceConfiguration)
  181. c.RegisterServerRequestHandler("client/registerCapability", HandleRegisterCapability)
  182. c.RegisterNotificationHandler("window/showMessage", HandleServerMessage)
  183. c.RegisterNotificationHandler("textDocument/publishDiagnostics",
  184. func(params json.RawMessage) { HandleDiagnostics(c, params) })
  185. // Notify the LSP server
  186. err := c.Initialized(ctx, protocol.InitializedParams{})
  187. if err != nil {
  188. return nil, fmt.Errorf("initialization failed: %w", err)
  189. }
  190. // LSP sepecific Initialization
  191. path := strings.ToLower(c.Cmd.Path)
  192. switch {
  193. case strings.Contains(path, "typescript-language-server"):
  194. // err := initializeTypescriptLanguageServer(ctx, c, workspaceDir)
  195. // if err != nil {
  196. // return nil, err
  197. // }
  198. }
  199. return &result, nil
  200. }
  201. func (c *Client) Close() error {
  202. // Try to close all open files first
  203. ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
  204. defer cancel()
  205. // Attempt to close files but continue shutdown regardless
  206. c.CloseAllFiles(ctx)
  207. // Close stdin to signal the server
  208. if err := c.stdin.Close(); err != nil {
  209. return fmt.Errorf("failed to close stdin: %w", err)
  210. }
  211. // Use a channel to handle the Wait with timeout
  212. done := make(chan error, 1)
  213. go func() {
  214. done <- c.Cmd.Wait()
  215. }()
  216. // Wait for process to exit with timeout
  217. select {
  218. case err := <-done:
  219. return err
  220. case <-time.After(2 * time.Second):
  221. // If we timeout, try to kill the process
  222. if err := c.Cmd.Process.Kill(); err != nil {
  223. return fmt.Errorf("failed to kill process: %w", err)
  224. }
  225. return fmt.Errorf("process killed after timeout")
  226. }
  227. }
  228. type ServerState int
  229. const (
  230. StateStarting ServerState = iota
  231. StateReady
  232. StateError
  233. )
  234. func (c *Client) WaitForServerReady(ctx context.Context) error {
  235. // TODO: wait for specific messages or poll workspace/symbol
  236. time.Sleep(time.Second * 1)
  237. return nil
  238. }
  239. type OpenFileInfo struct {
  240. Version int32
  241. URI protocol.DocumentUri
  242. }
  243. func (c *Client) OpenFile(ctx context.Context, filepath string) error {
  244. uri := fmt.Sprintf("file://%s", filepath)
  245. c.openFilesMu.Lock()
  246. if _, exists := c.openFiles[uri]; exists {
  247. c.openFilesMu.Unlock()
  248. return nil // Already open
  249. }
  250. c.openFilesMu.Unlock()
  251. // Skip files that do not exist or cannot be read
  252. content, err := os.ReadFile(filepath)
  253. if err != nil {
  254. return fmt.Errorf("error reading file: %w", err)
  255. }
  256. params := protocol.DidOpenTextDocumentParams{
  257. TextDocument: protocol.TextDocumentItem{
  258. URI: protocol.DocumentUri(uri),
  259. LanguageID: DetectLanguageID(uri),
  260. Version: 1,
  261. Text: string(content),
  262. },
  263. }
  264. if err := c.Notify(ctx, "textDocument/didOpen", params); err != nil {
  265. return err
  266. }
  267. c.openFilesMu.Lock()
  268. c.openFiles[uri] = &OpenFileInfo{
  269. Version: 1,
  270. URI: protocol.DocumentUri(uri),
  271. }
  272. c.openFilesMu.Unlock()
  273. return nil
  274. }
  275. func (c *Client) NotifyChange(ctx context.Context, filepath string) error {
  276. uri := fmt.Sprintf("file://%s", filepath)
  277. content, err := os.ReadFile(filepath)
  278. if err != nil {
  279. return fmt.Errorf("error reading file: %w", err)
  280. }
  281. c.openFilesMu.Lock()
  282. fileInfo, isOpen := c.openFiles[uri]
  283. if !isOpen {
  284. c.openFilesMu.Unlock()
  285. return fmt.Errorf("cannot notify change for unopened file: %s", filepath)
  286. }
  287. // Increment version
  288. fileInfo.Version++
  289. version := fileInfo.Version
  290. c.openFilesMu.Unlock()
  291. params := protocol.DidChangeTextDocumentParams{
  292. TextDocument: protocol.VersionedTextDocumentIdentifier{
  293. TextDocumentIdentifier: protocol.TextDocumentIdentifier{
  294. URI: protocol.DocumentUri(uri),
  295. },
  296. Version: version,
  297. },
  298. ContentChanges: []protocol.TextDocumentContentChangeEvent{
  299. {
  300. Value: protocol.TextDocumentContentChangeWholeDocument{
  301. Text: string(content),
  302. },
  303. },
  304. },
  305. }
  306. return c.Notify(ctx, "textDocument/didChange", params)
  307. }
  308. func (c *Client) CloseFile(ctx context.Context, filepath string) error {
  309. uri := fmt.Sprintf("file://%s", filepath)
  310. c.openFilesMu.Lock()
  311. if _, exists := c.openFiles[uri]; !exists {
  312. c.openFilesMu.Unlock()
  313. return nil // Already closed
  314. }
  315. c.openFilesMu.Unlock()
  316. params := protocol.DidCloseTextDocumentParams{
  317. TextDocument: protocol.TextDocumentIdentifier{
  318. URI: protocol.DocumentUri(uri),
  319. },
  320. }
  321. log.Println("Closing", params.TextDocument.URI.Dir())
  322. if err := c.Notify(ctx, "textDocument/didClose", params); err != nil {
  323. return err
  324. }
  325. c.openFilesMu.Lock()
  326. delete(c.openFiles, uri)
  327. c.openFilesMu.Unlock()
  328. return nil
  329. }
  330. func (c *Client) IsFileOpen(filepath string) bool {
  331. uri := fmt.Sprintf("file://%s", filepath)
  332. c.openFilesMu.RLock()
  333. defer c.openFilesMu.RUnlock()
  334. _, exists := c.openFiles[uri]
  335. return exists
  336. }
  337. // CloseAllFiles closes all currently open files
  338. func (c *Client) CloseAllFiles(ctx context.Context) {
  339. c.openFilesMu.Lock()
  340. filesToClose := make([]string, 0, len(c.openFiles))
  341. // First collect all URIs that need to be closed
  342. for uri := range c.openFiles {
  343. // Convert URI back to file path by trimming "file://" prefix
  344. filePath := strings.TrimPrefix(uri, "file://")
  345. filesToClose = append(filesToClose, filePath)
  346. }
  347. c.openFilesMu.Unlock()
  348. // Then close them all
  349. for _, filePath := range filesToClose {
  350. err := c.CloseFile(ctx, filePath)
  351. if err != nil && debug {
  352. log.Printf("Error closing file %s: %v", filePath, err)
  353. }
  354. }
  355. if debug {
  356. log.Printf("Closed %d files", len(filesToClose))
  357. }
  358. }
  359. func (c *Client) GetFileDiagnostics(uri protocol.DocumentUri) []protocol.Diagnostic {
  360. c.diagnosticsMu.RLock()
  361. defer c.diagnosticsMu.RUnlock()
  362. return c.diagnostics[uri]
  363. }
  364. func (c *Client) GetDiagnostics() map[protocol.DocumentUri][]protocol.Diagnostic {
  365. return c.diagnostics
  366. }