Skip to main content

lean_ctx/lsp/
router.rs

1use lsp_types::Uri;
2use std::collections::HashMap;
3use std::path::Path;
4use std::sync::Mutex;
5
6use super::client::{file_path_to_uri, LspClient};
7use super::config::{
8    check_server_available, default_servers, language_for_extension, LspServerConfig,
9};
10
11static CLIENTS: std::sync::LazyLock<Mutex<HashMap<String, LspClient>>> =
12    std::sync::LazyLock::new(|| Mutex::new(HashMap::new()));
13
14fn expand_tilde(path: &str) -> String {
15    if let Some(rest) = path.strip_prefix("~/") {
16        if let Some(home) = dirs::home_dir() {
17            return format!("{}/{rest}", home.display());
18        }
19    }
20    path.to_string()
21}
22
23fn resolve_config_for_language(language: &str) -> LspServerConfig {
24    let cfg = crate::core::config::Config::load();
25    if let Some(custom_path) = cfg.lsp.get(language) {
26        let expanded = expand_tilde(custom_path);
27        return LspServerConfig {
28            command: expanded,
29            args: if language == "typescript" || language == "javascript" {
30                vec!["--stdio".into()]
31            } else if language == "go" {
32                vec!["serve".into()]
33            } else {
34                vec![]
35            },
36        };
37    }
38    let servers = default_servers();
39    servers.get(language).cloned().unwrap_or(LspServerConfig {
40        command: format!("{language}-language-server"),
41        args: vec![],
42    })
43}
44
45pub fn with_client<F, R>(file_path: &str, project_root: &str, f: F) -> Result<R, String>
46where
47    F: FnOnce(&mut LspClient, &str) -> Result<R, String>,
48{
49    let ext = Path::new(file_path)
50        .extension()
51        .and_then(|e| e.to_str())
52        .unwrap_or("");
53
54    let language = language_for_extension(ext).ok_or_else(|| {
55        format!(
56            "No LSP server configured for extension '.{ext}'. Supported: rs, ts, tsx, js, py, go"
57        )
58    })?;
59
60    let mut clients = CLIENTS.lock().map_err(|e| e.to_string())?;
61
62    if !clients.contains_key(language) {
63        let config = resolve_config_for_language(language);
64
65        if super::config::find_binary_in_path(&config.command).is_none()
66            && !Path::new(&config.command).is_file()
67        {
68            check_server_available(language)?;
69        }
70
71        let root_uri = file_path_to_uri(project_root)?;
72        let client = LspClient::start(&config, &root_uri)?;
73        clients.insert(language.to_string(), client);
74    }
75
76    let client = clients
77        .get_mut(language)
78        .ok_or_else(|| format!("LSP client for '{language}' not available"))?;
79
80    f(client, language)
81}
82
83pub fn open_file(file_path: &str, project_root: &str) -> Result<Uri, String> {
84    let ext = Path::new(file_path)
85        .extension()
86        .and_then(|e| e.to_str())
87        .unwrap_or("");
88    language_for_extension(ext).ok_or_else(|| {
89        format!(
90            "No LSP server configured for extension '.{ext}'. Supported: rs, ts, tsx, js, py, go"
91        )
92    })?;
93
94    let content = std::fs::read_to_string(file_path)
95        .map_err(|e| format!("Cannot read '{file_path}': {e}"))?;
96
97    let uri = file_path_to_uri(file_path)?;
98
99    with_client(file_path, project_root, |client, language| {
100        client.did_open(&uri, language, &content)?;
101        Ok(uri.clone())
102    })
103}
104
105pub fn shutdown_all() {
106    if let Ok(mut clients) = CLIENTS.lock() {
107        for (_, client) in clients.drain() {
108            drop(client);
109        }
110    }
111}