client.go 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472
  1. package lsp
  2. import (
  3. "context"
  4. "encoding/json"
  5. "fmt"
  6. "log/slog"
  7. "maps"
  8. "os"
  9. "path/filepath"
  10. "strings"
  11. "sync/atomic"
  12. "time"
  13. "github.com/charmbracelet/crush/internal/config"
  14. "github.com/charmbracelet/crush/internal/csync"
  15. "github.com/charmbracelet/crush/internal/fsext"
  16. "github.com/charmbracelet/crush/internal/home"
  17. powernap "github.com/charmbracelet/x/powernap/pkg/lsp"
  18. "github.com/charmbracelet/x/powernap/pkg/lsp/protocol"
  19. "github.com/charmbracelet/x/powernap/pkg/transport"
  20. )
  21. type Client struct {
  22. client *powernap.Client
  23. name string
  24. // File types this LSP server handles (e.g., .go, .rs, .py)
  25. fileTypes []string
  26. // Configuration for this LSP client
  27. config config.LSPConfig
  28. // Diagnostic change callback
  29. onDiagnosticsChanged func(name string, count int)
  30. // Diagnostic cache
  31. diagnostics *csync.VersionedMap[protocol.DocumentURI, []protocol.Diagnostic]
  32. // Files are currently opened by the LSP
  33. openFiles *csync.Map[string, *OpenFileInfo]
  34. // Server state
  35. serverState atomic.Value
  36. }
  37. // New creates a new LSP client using the powernap implementation.
  38. func New(ctx context.Context, name string, config config.LSPConfig, resolver config.VariableResolver) (*Client, error) {
  39. // Convert working directory to file URI
  40. workDir, err := os.Getwd()
  41. if err != nil {
  42. return nil, fmt.Errorf("failed to get working directory: %w", err)
  43. }
  44. rootURI := string(protocol.URIFromPath(workDir))
  45. command, err := resolver.ResolveValue(config.Command)
  46. if err != nil {
  47. return nil, fmt.Errorf("invalid lsp command: %w", err)
  48. }
  49. // Create powernap client config
  50. clientConfig := powernap.ClientConfig{
  51. Command: home.Long(command),
  52. Args: config.Args,
  53. RootURI: rootURI,
  54. Environment: func() map[string]string {
  55. env := make(map[string]string)
  56. maps.Copy(env, config.Env)
  57. return env
  58. }(),
  59. Settings: config.Options,
  60. InitOptions: config.InitOptions,
  61. WorkspaceFolders: []protocol.WorkspaceFolder{
  62. {
  63. URI: rootURI,
  64. Name: filepath.Base(workDir),
  65. },
  66. },
  67. }
  68. // Create the powernap client
  69. powernapClient, err := powernap.NewClient(clientConfig)
  70. if err != nil {
  71. return nil, fmt.Errorf("failed to create lsp client: %w", err)
  72. }
  73. client := &Client{
  74. client: powernapClient,
  75. name: name,
  76. fileTypes: config.FileTypes,
  77. diagnostics: csync.NewVersionedMap[protocol.DocumentURI, []protocol.Diagnostic](),
  78. openFiles: csync.NewMap[string, *OpenFileInfo](),
  79. config: config,
  80. }
  81. // Initialize server state
  82. client.serverState.Store(StateStarting)
  83. return client, nil
  84. }
  85. // Initialize initializes the LSP client and returns the server capabilities.
  86. func (c *Client) Initialize(ctx context.Context, workspaceDir string) (*protocol.InitializeResult, error) {
  87. if err := c.client.Initialize(ctx, false); err != nil {
  88. return nil, fmt.Errorf("failed to initialize the lsp client: %w", err)
  89. }
  90. // Convert powernap capabilities to protocol capabilities
  91. caps := c.client.GetCapabilities()
  92. protocolCaps := protocol.ServerCapabilities{
  93. TextDocumentSync: caps.TextDocumentSync,
  94. CompletionProvider: func() *protocol.CompletionOptions {
  95. if caps.CompletionProvider != nil {
  96. return &protocol.CompletionOptions{
  97. TriggerCharacters: caps.CompletionProvider.TriggerCharacters,
  98. AllCommitCharacters: caps.CompletionProvider.AllCommitCharacters,
  99. ResolveProvider: caps.CompletionProvider.ResolveProvider,
  100. }
  101. }
  102. return nil
  103. }(),
  104. }
  105. result := &protocol.InitializeResult{
  106. Capabilities: protocolCaps,
  107. }
  108. c.RegisterServerRequestHandler("workspace/applyEdit", HandleApplyEdit)
  109. c.RegisterServerRequestHandler("workspace/configuration", HandleWorkspaceConfiguration)
  110. c.RegisterServerRequestHandler("client/registerCapability", HandleRegisterCapability)
  111. c.RegisterNotificationHandler("window/showMessage", HandleServerMessage)
  112. c.RegisterNotificationHandler("textDocument/publishDiagnostics", func(_ context.Context, _ string, params json.RawMessage) {
  113. HandleDiagnostics(c, params)
  114. })
  115. return result, nil
  116. }
  117. // Close closes the LSP client.
  118. func (c *Client) Close(ctx context.Context) error {
  119. // Try to close all open files first
  120. ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
  121. defer cancel()
  122. c.CloseAllFiles(ctx)
  123. // Shutdown and exit the client
  124. if err := c.client.Shutdown(ctx); err != nil {
  125. slog.Warn("Failed to shutdown LSP client", "error", err)
  126. }
  127. return c.client.Exit()
  128. }
  129. // ServerState represents the state of an LSP server
  130. type ServerState int
  131. const (
  132. StateStarting ServerState = iota
  133. StateReady
  134. StateError
  135. StateDisabled
  136. )
  137. // GetServerState returns the current state of the LSP server
  138. func (c *Client) GetServerState() ServerState {
  139. if val := c.serverState.Load(); val != nil {
  140. return val.(ServerState)
  141. }
  142. return StateStarting
  143. }
  144. // SetServerState sets the current state of the LSP server
  145. func (c *Client) SetServerState(state ServerState) {
  146. c.serverState.Store(state)
  147. }
  148. // GetName returns the name of the LSP client
  149. func (c *Client) GetName() string {
  150. return c.name
  151. }
  152. // SetDiagnosticsCallback sets the callback function for diagnostic changes
  153. func (c *Client) SetDiagnosticsCallback(callback func(name string, count int)) {
  154. c.onDiagnosticsChanged = callback
  155. }
  156. // WaitForServerReady waits for the server to be ready
  157. func (c *Client) WaitForServerReady(ctx context.Context) error {
  158. cfg := config.Get()
  159. // Set initial state
  160. c.SetServerState(StateStarting)
  161. // Create a context with timeout
  162. ctx, cancel := context.WithTimeout(ctx, 30*time.Second)
  163. defer cancel()
  164. // Try to ping the server with a simple request
  165. ticker := time.NewTicker(500 * time.Millisecond)
  166. defer ticker.Stop()
  167. if cfg != nil && cfg.Options.DebugLSP {
  168. slog.Debug("Waiting for LSP server to be ready...")
  169. }
  170. c.openKeyConfigFiles(ctx)
  171. for {
  172. select {
  173. case <-ctx.Done():
  174. c.SetServerState(StateError)
  175. return fmt.Errorf("timeout waiting for LSP server to be ready")
  176. case <-ticker.C:
  177. // Check if client is running
  178. if !c.client.IsRunning() {
  179. if cfg != nil && cfg.Options.DebugLSP {
  180. slog.Debug("LSP server not ready yet", "server", c.name)
  181. }
  182. continue
  183. }
  184. // Server is ready
  185. c.SetServerState(StateReady)
  186. if cfg != nil && cfg.Options.DebugLSP {
  187. slog.Debug("LSP server is ready")
  188. }
  189. return nil
  190. }
  191. }
  192. }
  193. // OpenFileInfo contains information about an open file
  194. type OpenFileInfo struct {
  195. Version int32
  196. URI protocol.DocumentURI
  197. }
  198. // HandlesFile checks if this LSP client handles the given file based on its extension.
  199. func (c *Client) HandlesFile(path string) bool {
  200. // If no file types are specified, handle all files (backward compatibility)
  201. if len(c.fileTypes) == 0 {
  202. return true
  203. }
  204. name := strings.ToLower(filepath.Base(path))
  205. for _, filetype := range c.fileTypes {
  206. suffix := strings.ToLower(filetype)
  207. if !strings.HasPrefix(suffix, ".") {
  208. suffix = "." + suffix
  209. }
  210. if strings.HasSuffix(name, suffix) {
  211. slog.Debug("handles file", "name", c.name, "file", name, "filetype", filetype)
  212. return true
  213. }
  214. }
  215. slog.Debug("doesn't handle file", "name", c.name, "file", name)
  216. return false
  217. }
  218. // OpenFile opens a file in the LSP server.
  219. func (c *Client) OpenFile(ctx context.Context, filepath string) error {
  220. if !c.HandlesFile(filepath) {
  221. return nil
  222. }
  223. uri := string(protocol.URIFromPath(filepath))
  224. if _, exists := c.openFiles.Get(uri); exists {
  225. return nil // Already open
  226. }
  227. // Skip files that do not exist or cannot be read
  228. content, err := os.ReadFile(filepath)
  229. if err != nil {
  230. return fmt.Errorf("error reading file: %w", err)
  231. }
  232. // Notify the server about the opened document
  233. if err = c.client.NotifyDidOpenTextDocument(ctx, uri, string(DetectLanguageID(uri)), 1, string(content)); err != nil {
  234. return err
  235. }
  236. c.openFiles.Set(uri, &OpenFileInfo{
  237. Version: 1,
  238. URI: protocol.DocumentURI(uri),
  239. })
  240. return nil
  241. }
  242. // NotifyChange notifies the server about a file change.
  243. func (c *Client) NotifyChange(ctx context.Context, filepath string) error {
  244. uri := string(protocol.URIFromPath(filepath))
  245. content, err := os.ReadFile(filepath)
  246. if err != nil {
  247. return fmt.Errorf("error reading file: %w", err)
  248. }
  249. fileInfo, isOpen := c.openFiles.Get(uri)
  250. if !isOpen {
  251. return fmt.Errorf("cannot notify change for unopened file: %s", filepath)
  252. }
  253. // Increment version
  254. fileInfo.Version++
  255. // Create change event
  256. changes := []protocol.TextDocumentContentChangeEvent{
  257. {
  258. Value: protocol.TextDocumentContentChangeWholeDocument{
  259. Text: string(content),
  260. },
  261. },
  262. }
  263. return c.client.NotifyDidChangeTextDocument(ctx, uri, int(fileInfo.Version), changes)
  264. }
  265. // IsFileOpen checks if a file is currently open.
  266. func (c *Client) IsFileOpen(filepath string) bool {
  267. uri := string(protocol.URIFromPath(filepath))
  268. _, exists := c.openFiles.Get(uri)
  269. return exists
  270. }
  271. // CloseAllFiles closes all currently open files.
  272. func (c *Client) CloseAllFiles(ctx context.Context) {
  273. cfg := config.Get()
  274. debugLSP := cfg != nil && cfg.Options.DebugLSP
  275. for uri := range c.openFiles.Seq2() {
  276. if debugLSP {
  277. slog.Debug("Closing file", "file", uri)
  278. }
  279. if err := c.client.NotifyDidCloseTextDocument(ctx, uri); err != nil {
  280. slog.Warn("Error closing rile", "uri", uri, "error", err)
  281. continue
  282. }
  283. c.openFiles.Del(uri)
  284. }
  285. }
  286. // GetFileDiagnostics returns diagnostics for a specific file.
  287. func (c *Client) GetFileDiagnostics(uri protocol.DocumentURI) []protocol.Diagnostic {
  288. diags, _ := c.diagnostics.Get(uri)
  289. return diags
  290. }
  291. // GetDiagnostics returns all diagnostics for all files.
  292. func (c *Client) GetDiagnostics() map[protocol.DocumentURI][]protocol.Diagnostic {
  293. return maps.Collect(c.diagnostics.Seq2())
  294. }
  295. // OpenFileOnDemand opens a file only if it's not already open.
  296. func (c *Client) OpenFileOnDemand(ctx context.Context, filepath string) error {
  297. // Check if the file is already open
  298. if c.IsFileOpen(filepath) {
  299. return nil
  300. }
  301. // Open the file
  302. return c.OpenFile(ctx, filepath)
  303. }
  304. // GetDiagnosticsForFile ensures a file is open and returns its diagnostics.
  305. func (c *Client) GetDiagnosticsForFile(ctx context.Context, filepath string) ([]protocol.Diagnostic, error) {
  306. documentURI := protocol.URIFromPath(filepath)
  307. // Make sure the file is open
  308. if !c.IsFileOpen(filepath) {
  309. if err := c.OpenFile(ctx, filepath); err != nil {
  310. return nil, fmt.Errorf("failed to open file for diagnostics: %w", err)
  311. }
  312. // Give the LSP server a moment to process the file
  313. time.Sleep(100 * time.Millisecond)
  314. }
  315. // Get diagnostics
  316. diagnostics, _ := c.diagnostics.Get(documentURI)
  317. return diagnostics, nil
  318. }
  319. // ClearDiagnosticsForURI removes diagnostics for a specific URI from the cache.
  320. func (c *Client) ClearDiagnosticsForURI(uri protocol.DocumentURI) {
  321. c.diagnostics.Del(uri)
  322. }
  323. // RegisterNotificationHandler registers a notification handler.
  324. func (c *Client) RegisterNotificationHandler(method string, handler transport.NotificationHandler) {
  325. c.client.RegisterNotificationHandler(method, handler)
  326. }
  327. // RegisterServerRequestHandler handles server requests.
  328. func (c *Client) RegisterServerRequestHandler(method string, handler transport.Handler) {
  329. c.client.RegisterHandler(method, handler)
  330. }
  331. // DidChangeWatchedFiles sends a workspace/didChangeWatchedFiles notification to the server.
  332. func (c *Client) DidChangeWatchedFiles(ctx context.Context, params protocol.DidChangeWatchedFilesParams) error {
  333. return c.client.NotifyDidChangeWatchedFiles(ctx, params.Changes)
  334. }
  335. // openKeyConfigFiles opens important configuration files that help initialize the server.
  336. func (c *Client) openKeyConfigFiles(ctx context.Context) {
  337. wd, err := os.Getwd()
  338. if err != nil {
  339. return
  340. }
  341. // Try to open each file, ignoring errors if they don't exist
  342. for _, file := range c.config.RootMarkers {
  343. file = filepath.Join(wd, file)
  344. if _, err := os.Stat(file); err == nil {
  345. // File exists, try to open it
  346. if err := c.OpenFile(ctx, file); err != nil {
  347. slog.Debug("Failed to open key config file", "file", file, "error", err)
  348. } else {
  349. slog.Debug("Opened key config file for initialization", "file", file)
  350. }
  351. }
  352. }
  353. }
  354. // WaitForDiagnostics waits until diagnostics change or the timeout is reached.
  355. func (c *Client) WaitForDiagnostics(ctx context.Context, d time.Duration) {
  356. ticker := time.NewTicker(200 * time.Millisecond)
  357. defer ticker.Stop()
  358. timeout := time.After(d)
  359. pv := c.diagnostics.Version()
  360. for {
  361. select {
  362. case <-ctx.Done():
  363. return
  364. case <-timeout:
  365. return
  366. case <-ticker.C:
  367. if pv != c.diagnostics.Version() {
  368. return
  369. }
  370. }
  371. }
  372. }
  373. // FindReferences finds all references to the symbol at the given position.
  374. func (c *Client) FindReferences(ctx context.Context, filepath string, line, character int, includeDeclaration bool) ([]protocol.Location, error) {
  375. if err := c.OpenFileOnDemand(ctx, filepath); err != nil {
  376. return nil, err
  377. }
  378. // NOTE: line and character should be 0-based.
  379. // See: https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#position
  380. return c.client.FindReferences(ctx, filepath, line-1, character-1, includeDeclaration)
  381. }
  382. // HasRootMarkers checks if any of the specified root marker patterns exist in the given directory.
  383. // Uses glob patterns to match files, allowing for more flexible matching.
  384. func HasRootMarkers(dir string, rootMarkers []string) bool {
  385. if len(rootMarkers) == 0 {
  386. return true
  387. }
  388. for _, pattern := range rootMarkers {
  389. // Use fsext.GlobWithDoubleStar to find matches
  390. matches, _, err := fsext.GlobWithDoubleStar(pattern, dir, 1)
  391. if err == nil && len(matches) > 0 {
  392. return true
  393. }
  394. }
  395. return false
  396. }