| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438 |
- //! LSP (Language Server Protocol) client registry for tool dispatch.
- //!
- //! Provides a stateful registry of LSP server connections, supporting
- //! the LSP tool actions: diagnostics, hover, definition, references,
- //! completion, symbols, and formatting.
- use std::collections::HashMap;
- use std::sync::{Arc, Mutex};
- use serde::{Deserialize, Serialize};
- /// Supported LSP actions.
- #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
- #[serde(rename_all = "snake_case")]
- pub enum LspAction {
- Diagnostics,
- Hover,
- Definition,
- References,
- Completion,
- Symbols,
- Format,
- }
- impl LspAction {
- pub fn from_str(s: &str) -> Option<Self> {
- match s {
- "diagnostics" => Some(Self::Diagnostics),
- "hover" => Some(Self::Hover),
- "definition" | "goto_definition" => Some(Self::Definition),
- "references" | "find_references" => Some(Self::References),
- "completion" | "completions" => Some(Self::Completion),
- "symbols" | "document_symbols" => Some(Self::Symbols),
- "format" | "formatting" => Some(Self::Format),
- _ => None,
- }
- }
- }
- /// A diagnostic entry from an LSP server.
- #[derive(Debug, Clone, Serialize, Deserialize)]
- pub struct LspDiagnostic {
- pub path: String,
- pub line: u32,
- pub character: u32,
- pub severity: String,
- pub message: String,
- pub source: Option<String>,
- }
- /// A location result (definition, references).
- #[derive(Debug, Clone, Serialize, Deserialize)]
- pub struct LspLocation {
- pub path: String,
- pub line: u32,
- pub character: u32,
- pub end_line: Option<u32>,
- pub end_character: Option<u32>,
- pub preview: Option<String>,
- }
- /// A hover result.
- #[derive(Debug, Clone, Serialize, Deserialize)]
- pub struct LspHoverResult {
- pub content: String,
- pub language: Option<String>,
- }
- /// A completion item.
- #[derive(Debug, Clone, Serialize, Deserialize)]
- pub struct LspCompletionItem {
- pub label: String,
- pub kind: Option<String>,
- pub detail: Option<String>,
- pub insert_text: Option<String>,
- }
- /// A document symbol.
- #[derive(Debug, Clone, Serialize, Deserialize)]
- pub struct LspSymbol {
- pub name: String,
- pub kind: String,
- pub path: String,
- pub line: u32,
- pub character: u32,
- }
- /// Connection status.
- #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
- #[serde(rename_all = "snake_case")]
- pub enum LspServerStatus {
- Connected,
- Disconnected,
- Starting,
- Error,
- }
- impl std::fmt::Display for LspServerStatus {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- match self {
- Self::Connected => write!(f, "connected"),
- Self::Disconnected => write!(f, "disconnected"),
- Self::Starting => write!(f, "starting"),
- Self::Error => write!(f, "error"),
- }
- }
- }
- /// Tracked state of an LSP server.
- #[derive(Debug, Clone, Serialize, Deserialize)]
- pub struct LspServerState {
- pub language: String,
- pub status: LspServerStatus,
- pub root_path: Option<String>,
- pub capabilities: Vec<String>,
- pub diagnostics: Vec<LspDiagnostic>,
- }
- /// Thread-safe LSP server registry.
- #[derive(Debug, Clone, Default)]
- pub struct LspRegistry {
- inner: Arc<Mutex<RegistryInner>>,
- }
- #[derive(Debug, Default)]
- struct RegistryInner {
- servers: HashMap<String, LspServerState>,
- }
- impl LspRegistry {
- #[must_use]
- pub fn new() -> Self {
- Self::default()
- }
- /// Register an LSP server for a language.
- pub fn register(
- &self,
- language: &str,
- status: LspServerStatus,
- root_path: Option<&str>,
- capabilities: Vec<String>,
- ) {
- let mut inner = self.inner.lock().expect("lsp registry lock poisoned");
- inner.servers.insert(
- language.to_owned(),
- LspServerState {
- language: language.to_owned(),
- status,
- root_path: root_path.map(str::to_owned),
- capabilities,
- diagnostics: Vec::new(),
- },
- );
- }
- /// Get server state by language.
- pub fn get(&self, language: &str) -> Option<LspServerState> {
- let inner = self.inner.lock().expect("lsp registry lock poisoned");
- inner.servers.get(language).cloned()
- }
- /// Find the appropriate server for a file path based on extension.
- pub fn find_server_for_path(&self, path: &str) -> Option<LspServerState> {
- let ext = std::path::Path::new(path)
- .extension()
- .and_then(|e| e.to_str())
- .unwrap_or("");
- let language = match ext {
- "rs" => "rust",
- "ts" | "tsx" => "typescript",
- "js" | "jsx" => "javascript",
- "py" => "python",
- "go" => "go",
- "java" => "java",
- "c" | "h" => "c",
- "cpp" | "hpp" | "cc" => "cpp",
- "rb" => "ruby",
- "lua" => "lua",
- _ => return None,
- };
- self.get(language)
- }
- /// List all registered servers.
- pub fn list_servers(&self) -> Vec<LspServerState> {
- let inner = self.inner.lock().expect("lsp registry lock poisoned");
- inner.servers.values().cloned().collect()
- }
- /// Add diagnostics to a server.
- pub fn add_diagnostics(
- &self,
- language: &str,
- diagnostics: Vec<LspDiagnostic>,
- ) -> Result<(), String> {
- let mut inner = self.inner.lock().expect("lsp registry lock poisoned");
- let server = inner
- .servers
- .get_mut(language)
- .ok_or_else(|| format!("LSP server not found for language: {language}"))?;
- server.diagnostics.extend(diagnostics);
- Ok(())
- }
- /// Get diagnostics for a specific file path.
- pub fn get_diagnostics(&self, path: &str) -> Vec<LspDiagnostic> {
- let inner = self.inner.lock().expect("lsp registry lock poisoned");
- inner
- .servers
- .values()
- .flat_map(|s| &s.diagnostics)
- .filter(|d| d.path == path)
- .cloned()
- .collect()
- }
- /// Clear diagnostics for a language server.
- pub fn clear_diagnostics(&self, language: &str) -> Result<(), String> {
- let mut inner = self.inner.lock().expect("lsp registry lock poisoned");
- let server = inner
- .servers
- .get_mut(language)
- .ok_or_else(|| format!("LSP server not found for language: {language}"))?;
- server.diagnostics.clear();
- Ok(())
- }
- /// Disconnect a server.
- pub fn disconnect(&self, language: &str) -> Option<LspServerState> {
- let mut inner = self.inner.lock().expect("lsp registry lock poisoned");
- inner.servers.remove(language)
- }
- #[must_use]
- pub fn len(&self) -> usize {
- let inner = self.inner.lock().expect("lsp registry lock poisoned");
- inner.servers.len()
- }
- #[must_use]
- pub fn is_empty(&self) -> bool {
- self.len() == 0
- }
- /// Dispatch an LSP action and return a structured result.
- pub fn dispatch(
- &self,
- action: &str,
- path: Option<&str>,
- line: Option<u32>,
- character: Option<u32>,
- _query: Option<&str>,
- ) -> Result<serde_json::Value, String> {
- let lsp_action =
- LspAction::from_str(action).ok_or_else(|| format!("unknown LSP action: {action}"))?;
- // For diagnostics, we can check existing cached diagnostics
- if lsp_action == LspAction::Diagnostics {
- if let Some(path) = path {
- let diags = self.get_diagnostics(path);
- return Ok(serde_json::json!({
- "action": "diagnostics",
- "path": path,
- "diagnostics": diags,
- "count": diags.len()
- }));
- }
- // All diagnostics across all servers
- let inner = self.inner.lock().expect("lsp registry lock poisoned");
- let all_diags: Vec<_> = inner
- .servers
- .values()
- .flat_map(|s| &s.diagnostics)
- .collect();
- return Ok(serde_json::json!({
- "action": "diagnostics",
- "diagnostics": all_diags,
- "count": all_diags.len()
- }));
- }
- // For other actions, we need a connected server for the given file
- let path = path.ok_or("path is required for this LSP action")?;
- let server = self
- .find_server_for_path(path)
- .ok_or_else(|| format!("no LSP server available for path: {path}"))?;
- if server.status != LspServerStatus::Connected {
- return Err(format!(
- "LSP server for '{}' is not connected (status: {})",
- server.language, server.status
- ));
- }
- // Return structured placeholder — actual LSP JSON-RPC calls would
- // go through the real LSP process here.
- Ok(serde_json::json!({
- "action": action,
- "path": path,
- "line": line,
- "character": character,
- "language": server.language,
- "status": "dispatched",
- "message": format!("LSP {} dispatched to {} server", action, server.language)
- }))
- }
- }
- #[cfg(test)]
- mod tests {
- use super::*;
- #[test]
- fn registers_and_retrieves_server() {
- let registry = LspRegistry::new();
- registry.register(
- "rust",
- LspServerStatus::Connected,
- Some("/workspace"),
- vec!["hover".into(), "completion".into()],
- );
- let server = registry.get("rust").expect("should exist");
- assert_eq!(server.language, "rust");
- assert_eq!(server.status, LspServerStatus::Connected);
- assert_eq!(server.capabilities.len(), 2);
- }
- #[test]
- fn finds_server_by_file_extension() {
- let registry = LspRegistry::new();
- registry.register("rust", LspServerStatus::Connected, None, vec![]);
- registry.register("typescript", LspServerStatus::Connected, None, vec![]);
- let rs_server = registry.find_server_for_path("src/main.rs").unwrap();
- assert_eq!(rs_server.language, "rust");
- let ts_server = registry.find_server_for_path("src/index.ts").unwrap();
- assert_eq!(ts_server.language, "typescript");
- assert!(registry.find_server_for_path("data.csv").is_none());
- }
- #[test]
- fn manages_diagnostics() {
- let registry = LspRegistry::new();
- registry.register("rust", LspServerStatus::Connected, None, vec![]);
- registry
- .add_diagnostics(
- "rust",
- vec![LspDiagnostic {
- path: "src/main.rs".into(),
- line: 10,
- character: 5,
- severity: "error".into(),
- message: "mismatched types".into(),
- source: Some("rust-analyzer".into()),
- }],
- )
- .unwrap();
- let diags = registry.get_diagnostics("src/main.rs");
- assert_eq!(diags.len(), 1);
- assert_eq!(diags[0].message, "mismatched types");
- registry.clear_diagnostics("rust").unwrap();
- assert!(registry.get_diagnostics("src/main.rs").is_empty());
- }
- #[test]
- fn dispatches_diagnostics_action() {
- let registry = LspRegistry::new();
- registry.register("rust", LspServerStatus::Connected, None, vec![]);
- registry
- .add_diagnostics(
- "rust",
- vec![LspDiagnostic {
- path: "src/lib.rs".into(),
- line: 1,
- character: 0,
- severity: "warning".into(),
- message: "unused import".into(),
- source: None,
- }],
- )
- .unwrap();
- let result = registry
- .dispatch("diagnostics", Some("src/lib.rs"), None, None, None)
- .unwrap();
- assert_eq!(result["count"], 1);
- }
- #[test]
- fn dispatches_hover_action() {
- let registry = LspRegistry::new();
- registry.register("rust", LspServerStatus::Connected, None, vec![]);
- let result = registry
- .dispatch("hover", Some("src/main.rs"), Some(10), Some(5), None)
- .unwrap();
- assert_eq!(result["action"], "hover");
- assert_eq!(result["language"], "rust");
- }
- #[test]
- fn rejects_action_on_disconnected_server() {
- let registry = LspRegistry::new();
- registry.register("rust", LspServerStatus::Disconnected, None, vec![]);
- assert!(registry
- .dispatch("hover", Some("src/main.rs"), Some(1), Some(0), None)
- .is_err());
- }
- #[test]
- fn rejects_unknown_action() {
- let registry = LspRegistry::new();
- assert!(registry
- .dispatch("unknown_action", Some("file.rs"), None, None, None)
- .is_err());
- }
- #[test]
- fn disconnects_server() {
- let registry = LspRegistry::new();
- registry.register("rust", LspServerStatus::Connected, None, vec![]);
- assert_eq!(registry.len(), 1);
- let removed = registry.disconnect("rust");
- assert!(removed.is_some());
- assert!(registry.is_empty());
- }
- }
|