lsp_client.rs 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438
  1. //! LSP (Language Server Protocol) client registry for tool dispatch.
  2. //!
  3. //! Provides a stateful registry of LSP server connections, supporting
  4. //! the LSP tool actions: diagnostics, hover, definition, references,
  5. //! completion, symbols, and formatting.
  6. use std::collections::HashMap;
  7. use std::sync::{Arc, Mutex};
  8. use serde::{Deserialize, Serialize};
  9. /// Supported LSP actions.
  10. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
  11. #[serde(rename_all = "snake_case")]
  12. pub enum LspAction {
  13. Diagnostics,
  14. Hover,
  15. Definition,
  16. References,
  17. Completion,
  18. Symbols,
  19. Format,
  20. }
  21. impl LspAction {
  22. pub fn from_str(s: &str) -> Option<Self> {
  23. match s {
  24. "diagnostics" => Some(Self::Diagnostics),
  25. "hover" => Some(Self::Hover),
  26. "definition" | "goto_definition" => Some(Self::Definition),
  27. "references" | "find_references" => Some(Self::References),
  28. "completion" | "completions" => Some(Self::Completion),
  29. "symbols" | "document_symbols" => Some(Self::Symbols),
  30. "format" | "formatting" => Some(Self::Format),
  31. _ => None,
  32. }
  33. }
  34. }
  35. /// A diagnostic entry from an LSP server.
  36. #[derive(Debug, Clone, Serialize, Deserialize)]
  37. pub struct LspDiagnostic {
  38. pub path: String,
  39. pub line: u32,
  40. pub character: u32,
  41. pub severity: String,
  42. pub message: String,
  43. pub source: Option<String>,
  44. }
  45. /// A location result (definition, references).
  46. #[derive(Debug, Clone, Serialize, Deserialize)]
  47. pub struct LspLocation {
  48. pub path: String,
  49. pub line: u32,
  50. pub character: u32,
  51. pub end_line: Option<u32>,
  52. pub end_character: Option<u32>,
  53. pub preview: Option<String>,
  54. }
  55. /// A hover result.
  56. #[derive(Debug, Clone, Serialize, Deserialize)]
  57. pub struct LspHoverResult {
  58. pub content: String,
  59. pub language: Option<String>,
  60. }
  61. /// A completion item.
  62. #[derive(Debug, Clone, Serialize, Deserialize)]
  63. pub struct LspCompletionItem {
  64. pub label: String,
  65. pub kind: Option<String>,
  66. pub detail: Option<String>,
  67. pub insert_text: Option<String>,
  68. }
  69. /// A document symbol.
  70. #[derive(Debug, Clone, Serialize, Deserialize)]
  71. pub struct LspSymbol {
  72. pub name: String,
  73. pub kind: String,
  74. pub path: String,
  75. pub line: u32,
  76. pub character: u32,
  77. }
  78. /// Connection status.
  79. #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
  80. #[serde(rename_all = "snake_case")]
  81. pub enum LspServerStatus {
  82. Connected,
  83. Disconnected,
  84. Starting,
  85. Error,
  86. }
  87. impl std::fmt::Display for LspServerStatus {
  88. fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
  89. match self {
  90. Self::Connected => write!(f, "connected"),
  91. Self::Disconnected => write!(f, "disconnected"),
  92. Self::Starting => write!(f, "starting"),
  93. Self::Error => write!(f, "error"),
  94. }
  95. }
  96. }
  97. /// Tracked state of an LSP server.
  98. #[derive(Debug, Clone, Serialize, Deserialize)]
  99. pub struct LspServerState {
  100. pub language: String,
  101. pub status: LspServerStatus,
  102. pub root_path: Option<String>,
  103. pub capabilities: Vec<String>,
  104. pub diagnostics: Vec<LspDiagnostic>,
  105. }
  106. /// Thread-safe LSP server registry.
  107. #[derive(Debug, Clone, Default)]
  108. pub struct LspRegistry {
  109. inner: Arc<Mutex<RegistryInner>>,
  110. }
  111. #[derive(Debug, Default)]
  112. struct RegistryInner {
  113. servers: HashMap<String, LspServerState>,
  114. }
  115. impl LspRegistry {
  116. #[must_use]
  117. pub fn new() -> Self {
  118. Self::default()
  119. }
  120. /// Register an LSP server for a language.
  121. pub fn register(
  122. &self,
  123. language: &str,
  124. status: LspServerStatus,
  125. root_path: Option<&str>,
  126. capabilities: Vec<String>,
  127. ) {
  128. let mut inner = self.inner.lock().expect("lsp registry lock poisoned");
  129. inner.servers.insert(
  130. language.to_owned(),
  131. LspServerState {
  132. language: language.to_owned(),
  133. status,
  134. root_path: root_path.map(str::to_owned),
  135. capabilities,
  136. diagnostics: Vec::new(),
  137. },
  138. );
  139. }
  140. /// Get server state by language.
  141. pub fn get(&self, language: &str) -> Option<LspServerState> {
  142. let inner = self.inner.lock().expect("lsp registry lock poisoned");
  143. inner.servers.get(language).cloned()
  144. }
  145. /// Find the appropriate server for a file path based on extension.
  146. pub fn find_server_for_path(&self, path: &str) -> Option<LspServerState> {
  147. let ext = std::path::Path::new(path)
  148. .extension()
  149. .and_then(|e| e.to_str())
  150. .unwrap_or("");
  151. let language = match ext {
  152. "rs" => "rust",
  153. "ts" | "tsx" => "typescript",
  154. "js" | "jsx" => "javascript",
  155. "py" => "python",
  156. "go" => "go",
  157. "java" => "java",
  158. "c" | "h" => "c",
  159. "cpp" | "hpp" | "cc" => "cpp",
  160. "rb" => "ruby",
  161. "lua" => "lua",
  162. _ => return None,
  163. };
  164. self.get(language)
  165. }
  166. /// List all registered servers.
  167. pub fn list_servers(&self) -> Vec<LspServerState> {
  168. let inner = self.inner.lock().expect("lsp registry lock poisoned");
  169. inner.servers.values().cloned().collect()
  170. }
  171. /// Add diagnostics to a server.
  172. pub fn add_diagnostics(
  173. &self,
  174. language: &str,
  175. diagnostics: Vec<LspDiagnostic>,
  176. ) -> Result<(), String> {
  177. let mut inner = self.inner.lock().expect("lsp registry lock poisoned");
  178. let server = inner
  179. .servers
  180. .get_mut(language)
  181. .ok_or_else(|| format!("LSP server not found for language: {language}"))?;
  182. server.diagnostics.extend(diagnostics);
  183. Ok(())
  184. }
  185. /// Get diagnostics for a specific file path.
  186. pub fn get_diagnostics(&self, path: &str) -> Vec<LspDiagnostic> {
  187. let inner = self.inner.lock().expect("lsp registry lock poisoned");
  188. inner
  189. .servers
  190. .values()
  191. .flat_map(|s| &s.diagnostics)
  192. .filter(|d| d.path == path)
  193. .cloned()
  194. .collect()
  195. }
  196. /// Clear diagnostics for a language server.
  197. pub fn clear_diagnostics(&self, language: &str) -> Result<(), String> {
  198. let mut inner = self.inner.lock().expect("lsp registry lock poisoned");
  199. let server = inner
  200. .servers
  201. .get_mut(language)
  202. .ok_or_else(|| format!("LSP server not found for language: {language}"))?;
  203. server.diagnostics.clear();
  204. Ok(())
  205. }
  206. /// Disconnect a server.
  207. pub fn disconnect(&self, language: &str) -> Option<LspServerState> {
  208. let mut inner = self.inner.lock().expect("lsp registry lock poisoned");
  209. inner.servers.remove(language)
  210. }
  211. #[must_use]
  212. pub fn len(&self) -> usize {
  213. let inner = self.inner.lock().expect("lsp registry lock poisoned");
  214. inner.servers.len()
  215. }
  216. #[must_use]
  217. pub fn is_empty(&self) -> bool {
  218. self.len() == 0
  219. }
  220. /// Dispatch an LSP action and return a structured result.
  221. pub fn dispatch(
  222. &self,
  223. action: &str,
  224. path: Option<&str>,
  225. line: Option<u32>,
  226. character: Option<u32>,
  227. _query: Option<&str>,
  228. ) -> Result<serde_json::Value, String> {
  229. let lsp_action =
  230. LspAction::from_str(action).ok_or_else(|| format!("unknown LSP action: {action}"))?;
  231. // For diagnostics, we can check existing cached diagnostics
  232. if lsp_action == LspAction::Diagnostics {
  233. if let Some(path) = path {
  234. let diags = self.get_diagnostics(path);
  235. return Ok(serde_json::json!({
  236. "action": "diagnostics",
  237. "path": path,
  238. "diagnostics": diags,
  239. "count": diags.len()
  240. }));
  241. }
  242. // All diagnostics across all servers
  243. let inner = self.inner.lock().expect("lsp registry lock poisoned");
  244. let all_diags: Vec<_> = inner
  245. .servers
  246. .values()
  247. .flat_map(|s| &s.diagnostics)
  248. .collect();
  249. return Ok(serde_json::json!({
  250. "action": "diagnostics",
  251. "diagnostics": all_diags,
  252. "count": all_diags.len()
  253. }));
  254. }
  255. // For other actions, we need a connected server for the given file
  256. let path = path.ok_or("path is required for this LSP action")?;
  257. let server = self
  258. .find_server_for_path(path)
  259. .ok_or_else(|| format!("no LSP server available for path: {path}"))?;
  260. if server.status != LspServerStatus::Connected {
  261. return Err(format!(
  262. "LSP server for '{}' is not connected (status: {})",
  263. server.language, server.status
  264. ));
  265. }
  266. // Return structured placeholder — actual LSP JSON-RPC calls would
  267. // go through the real LSP process here.
  268. Ok(serde_json::json!({
  269. "action": action,
  270. "path": path,
  271. "line": line,
  272. "character": character,
  273. "language": server.language,
  274. "status": "dispatched",
  275. "message": format!("LSP {} dispatched to {} server", action, server.language)
  276. }))
  277. }
  278. }
  279. #[cfg(test)]
  280. mod tests {
  281. use super::*;
  282. #[test]
  283. fn registers_and_retrieves_server() {
  284. let registry = LspRegistry::new();
  285. registry.register(
  286. "rust",
  287. LspServerStatus::Connected,
  288. Some("/workspace"),
  289. vec!["hover".into(), "completion".into()],
  290. );
  291. let server = registry.get("rust").expect("should exist");
  292. assert_eq!(server.language, "rust");
  293. assert_eq!(server.status, LspServerStatus::Connected);
  294. assert_eq!(server.capabilities.len(), 2);
  295. }
  296. #[test]
  297. fn finds_server_by_file_extension() {
  298. let registry = LspRegistry::new();
  299. registry.register("rust", LspServerStatus::Connected, None, vec![]);
  300. registry.register("typescript", LspServerStatus::Connected, None, vec![]);
  301. let rs_server = registry.find_server_for_path("src/main.rs").unwrap();
  302. assert_eq!(rs_server.language, "rust");
  303. let ts_server = registry.find_server_for_path("src/index.ts").unwrap();
  304. assert_eq!(ts_server.language, "typescript");
  305. assert!(registry.find_server_for_path("data.csv").is_none());
  306. }
  307. #[test]
  308. fn manages_diagnostics() {
  309. let registry = LspRegistry::new();
  310. registry.register("rust", LspServerStatus::Connected, None, vec![]);
  311. registry
  312. .add_diagnostics(
  313. "rust",
  314. vec![LspDiagnostic {
  315. path: "src/main.rs".into(),
  316. line: 10,
  317. character: 5,
  318. severity: "error".into(),
  319. message: "mismatched types".into(),
  320. source: Some("rust-analyzer".into()),
  321. }],
  322. )
  323. .unwrap();
  324. let diags = registry.get_diagnostics("src/main.rs");
  325. assert_eq!(diags.len(), 1);
  326. assert_eq!(diags[0].message, "mismatched types");
  327. registry.clear_diagnostics("rust").unwrap();
  328. assert!(registry.get_diagnostics("src/main.rs").is_empty());
  329. }
  330. #[test]
  331. fn dispatches_diagnostics_action() {
  332. let registry = LspRegistry::new();
  333. registry.register("rust", LspServerStatus::Connected, None, vec![]);
  334. registry
  335. .add_diagnostics(
  336. "rust",
  337. vec![LspDiagnostic {
  338. path: "src/lib.rs".into(),
  339. line: 1,
  340. character: 0,
  341. severity: "warning".into(),
  342. message: "unused import".into(),
  343. source: None,
  344. }],
  345. )
  346. .unwrap();
  347. let result = registry
  348. .dispatch("diagnostics", Some("src/lib.rs"), None, None, None)
  349. .unwrap();
  350. assert_eq!(result["count"], 1);
  351. }
  352. #[test]
  353. fn dispatches_hover_action() {
  354. let registry = LspRegistry::new();
  355. registry.register("rust", LspServerStatus::Connected, None, vec![]);
  356. let result = registry
  357. .dispatch("hover", Some("src/main.rs"), Some(10), Some(5), None)
  358. .unwrap();
  359. assert_eq!(result["action"], "hover");
  360. assert_eq!(result["language"], "rust");
  361. }
  362. #[test]
  363. fn rejects_action_on_disconnected_server() {
  364. let registry = LspRegistry::new();
  365. registry.register("rust", LspServerStatus::Disconnected, None, vec![]);
  366. assert!(registry
  367. .dispatch("hover", Some("src/main.rs"), Some(1), Some(0), None)
  368. .is_err());
  369. }
  370. #[test]
  371. fn rejects_unknown_action() {
  372. let registry = LspRegistry::new();
  373. assert!(registry
  374. .dispatch("unknown_action", Some("file.rs"), None, None, None)
  375. .is_err());
  376. }
  377. #[test]
  378. fn disconnects_server() {
  379. let registry = LspRegistry::new();
  380. registry.register("rust", LspServerStatus::Connected, None, vec![]);
  381. assert_eq!(registry.len(), 1);
  382. let removed = registry.disconnect("rust");
  383. assert!(removed.is_some());
  384. assert!(registry.is_empty());
  385. }
  386. }