Skip to main content

iced_code_editor/canvas_editor/lsp_process/
config.rs

1//! LSP (Language Server Protocol) configuration module.
2//!
3//! This module handles language server detection, configuration, and command resolution
4//! for various programming languages. It maps file extensions to language servers and
5//! provides functionality to resolve the correct server command based on environment
6//! variables and system availability.
7
8use std::path::{Path, PathBuf};
9use std::process::Command;
10
11/// Represents a language supported by an LSP server.
12///
13/// Contains the language identifier and the associated server key.
14///
15/// # Examples
16///
17/// ```no_run
18/// use iced_code_editor::lsp_language_for_extension;
19///
20/// if let Some(lang) = lsp_language_for_extension("rs") {
21///     assert_eq!(lang.language_id, "rust");
22///     assert_eq!(lang.server_key, "rust-analyzer");
23/// }
24/// ```
25#[derive(Clone, Copy)]
26pub struct LspLanguage {
27    /// Language identifier (e.g., "rust", "python", "typescript")
28    pub language_id: &'static str,
29    /// Key identifying the LSP server (e.g., "rust-analyzer", "pyright")
30    pub server_key: &'static str,
31}
32
33/// Internal mapping between file extensions and language/server configurations.
34#[derive(Clone, Copy)]
35struct LspLanguageMapping {
36    /// File extensions associated with this language (e.g., ["rs"], ["ts", "tsx"])
37    extensions: &'static [&'static str],
38    /// Language identifier for LSP protocol
39    language_id: &'static str,
40    /// Key to look up the server configuration
41    server_key: &'static str,
42}
43
44/// Configuration for an LSP server.
45///
46/// Defines how to locate and run the language server.
47///
48/// # Examples
49///
50/// ```no_run
51/// use iced_code_editor::lsp_server_config;
52///
53/// if let Some(config) = lsp_server_config("rust-analyzer") {
54///     println!("key: {}", config.key);
55/// }
56/// ```
57#[derive(Clone, Copy)]
58pub struct LspServerConfig {
59    /// Unique identifier for this server configuration
60    pub key: &'static str,
61    /// Environment variables to check for custom server paths (checked in order)
62    pub env_vars: &'static [&'static str],
63    /// Default command and arguments to run the server
64    pub default_command: &'static [&'static str],
65}
66
67/// Resolved command to execute an LSP server.
68///
69/// # Examples
70///
71/// ```no_run
72/// use iced_code_editor::{lsp_server_config, resolve_lsp_command};
73///
74/// if let Some(config) = lsp_server_config("gopls") {
75///     if let Ok(cmd) = resolve_lsp_command(config) {
76///         println!("program: {}", cmd.program);
77///     }
78/// }
79/// ```
80pub struct LspCommand {
81    /// Program path or name
82    pub program: String,
83    /// Command-line arguments
84    pub args: Vec<String>,
85}
86
87/// Supported language mappings: file extensions -> language ID -> server key
88const LSP_LANGUAGE_MAPPINGS: &[LspLanguageMapping] = &[
89    LspLanguageMapping {
90        extensions: &["rs"],
91        language_id: "rust",
92        server_key: "rust-analyzer",
93    },
94    LspLanguageMapping {
95        extensions: &["py"],
96        language_id: "python",
97        server_key: "pyright",
98    },
99    LspLanguageMapping {
100        extensions: &["js", "jsx"],
101        language_id: "javascript",
102        server_key: "typescript-language-server",
103    },
104    LspLanguageMapping {
105        extensions: &["ts", "tsx"],
106        language_id: "typescript",
107        server_key: "typescript-language-server",
108    },
109    LspLanguageMapping {
110        extensions: &["lua"],
111        language_id: "lua",
112        server_key: "lua-language-server",
113    },
114    LspLanguageMapping {
115        extensions: &["go"],
116        language_id: "go",
117        server_key: "gopls",
118    },
119];
120
121/// Server configurations for each supported LSP server.
122/// Defines environment variables and default commands for each server.
123const LSP_SERVER_CONFIGS: &[LspServerConfig] = &[
124    LspServerConfig {
125        key: "rust-analyzer",
126        env_vars: &["RUST_ANALYZER", "RUST_ANALYZER_PATH"],
127        default_command: &["rust-analyzer"],
128    },
129    LspServerConfig {
130        key: "pyright",
131        env_vars: &["PYRIGHT_LANGSERVER", "PYRIGHT_LANGSERVER_PATH"],
132        default_command: &["pyright-langserver", "--stdio"],
133    },
134    LspServerConfig {
135        key: "typescript-language-server",
136        env_vars: &[
137            "TYPESCRIPT_LANGUAGE_SERVER",
138            "TYPESCRIPT_LANGUAGE_SERVER_PATH",
139        ],
140        default_command: &["typescript-language-server", "--stdio"],
141    },
142    LspServerConfig {
143        key: "lua-language-server",
144        env_vars: &["LUA_LANGUAGE_SERVER", "LUA_LANGUAGE_SERVER_PATH"],
145        default_command: &["lua-language-server"],
146    },
147    LspServerConfig {
148        key: "gopls",
149        env_vars: &["GOPLS", "GOPLS_PATH"],
150        default_command: &["gopls"],
151    },
152];
153
154/// Looks up the LSP language configuration for a file extension.
155///
156/// Returns `None` if the extension is not supported.
157///
158/// # Examples
159///
160/// ```
161/// use iced_code_editor::lsp_language_for_extension;
162///
163/// let lang = lsp_language_for_extension("rs");
164/// assert!(lang.is_some());
165///
166/// let unknown = lsp_language_for_extension("xyz");
167/// assert!(unknown.is_none());
168/// ```
169pub fn lsp_language_for_extension(extension: &str) -> Option<LspLanguage> {
170    let extension = extension.to_lowercase();
171    LSP_LANGUAGE_MAPPINGS
172        .iter()
173        .find(|mapping| {
174            mapping
175                .extensions
176                .iter()
177                .any(|ext| ext.eq_ignore_ascii_case(extension.as_str()))
178        })
179        .map(|mapping| LspLanguage {
180            language_id: mapping.language_id,
181            server_key: mapping.server_key,
182        })
183}
184
185/// Looks up the LSP language configuration for a file path.
186///
187/// Extracts the extension and delegates to [`lsp_language_for_extension`].
188///
189/// # Examples
190///
191/// ```
192/// use std::path::Path;
193/// use iced_code_editor::lsp_language_for_path;
194///
195/// let lang = lsp_language_for_path(Path::new("main.rs"));
196/// assert!(lang.is_some());
197///
198/// let none = lsp_language_for_path(Path::new("noext"));
199/// assert!(none.is_none());
200/// ```
201pub fn lsp_language_for_path(path: &Path) -> Option<LspLanguage> {
202    let extension = path.extension()?.to_str()?;
203    lsp_language_for_extension(extension)
204}
205
206/// Retrieves the server configuration for a given server key.
207///
208/// # Examples
209///
210/// ```
211/// use iced_code_editor::lsp_server_config;
212///
213/// let cfg = lsp_server_config("rust-analyzer");
214/// assert!(cfg.is_some());
215///
216/// let missing = lsp_server_config("unknown-server");
217/// assert!(missing.is_none());
218/// ```
219pub fn lsp_server_config(key: &str) -> Option<&'static LspServerConfig> {
220    LSP_SERVER_CONFIGS.iter().find(|config| config.key == key)
221}
222
223/// Resolves the command to execute an LSP server.
224///
225/// Checks environment variables first, then falls back to the default command.
226/// Special handling for rust-analyzer to support rustup-installed versions.
227///
228/// # Errors
229///
230/// Returns an error string if the program cannot be located (e.g. rust-analyzer
231/// or gopls are not installed and not found via their fallback discovery logic).
232///
233/// # Examples
234///
235/// ```no_run
236/// use iced_code_editor::{lsp_server_config, resolve_lsp_command};
237///
238/// if let Some(config) = lsp_server_config("lua-language-server") {
239///     match resolve_lsp_command(config) {
240///         Ok(cmd) => println!("Run: {}", cmd.program),
241///         Err(e) => eprintln!("Not found: {e}"),
242///     }
243/// }
244/// ```
245pub fn resolve_lsp_command(
246    config: &LspServerConfig,
247) -> Result<LspCommand, String> {
248    let program = if config.key == "rust-analyzer" {
249        resolve_rust_analyzer_command()?
250    } else if config.key == "gopls" {
251        resolve_gopls_command()?
252    } else {
253        resolve_program_from_envs(config.env_vars)
254            .unwrap_or_else(|| config.default_command[0].to_string())
255    };
256    let args = config
257        .default_command
258        .iter()
259        .skip(1)
260        .map(|arg| arg.to_string())
261        .collect();
262    Ok(LspCommand { program, args })
263}
264
265/// Resolves a program path from a list of environment variables.
266/// Returns the first non-empty value found, or None if all are unset/empty.
267fn resolve_program_from_envs(env_vars: &[&str]) -> Option<String> {
268    for var in env_vars {
269        if let Ok(path) = std::env::var(var)
270            && !path.trim().is_empty()
271        {
272            return Some(path);
273        }
274    }
275    None
276}
277
278/// Resolves the rust-analyzer command with special handling.
279/// Checks in order:
280/// 1. RUST_ANALYZER environment variable
281/// 2. RUST_ANALYZER_PATH environment variable
282/// 3. Direct rust-analyzer command
283/// 4. rustup which rust-analyzer
284fn resolve_rust_analyzer_command() -> Result<String, String> {
285    if let Ok(path) = std::env::var("RUST_ANALYZER")
286        && !path.trim().is_empty()
287    {
288        return Ok(path);
289    }
290    if let Ok(path) = std::env::var("RUST_ANALYZER_PATH")
291        && !path.trim().is_empty()
292    {
293        return Ok(path);
294    }
295    if Command::new("rust-analyzer").arg("--version").output().is_ok() {
296        return Ok("rust-analyzer".to_string());
297    }
298    if let Ok(output) =
299        Command::new("rustup").args(["which", "rust-analyzer"]).output()
300        && output.status.success()
301    {
302        let path = String::from_utf8_lossy(&output.stdout).trim().to_string();
303        if !path.is_empty() {
304            return Ok(path);
305        }
306    }
307    Err(
308        "rust-analyzer not found. Please run rustup component add rust-analyzer or brew install rust-analyzer"
309            .to_string(),
310    )
311}
312
313fn resolve_gopls_command() -> Result<String, String> {
314    if let Some(path) = resolve_program_from_envs(&["GOPLS", "GOPLS_PATH"]) {
315        return Ok(path);
316    }
317    if Command::new("gopls").arg("version").output().is_ok() {
318        return Ok("gopls".to_string());
319    }
320    if let Ok(output) = Command::new("go").args(["env", "GOBIN"]).output()
321        && output.status.success()
322    {
323        let path = String::from_utf8_lossy(&output.stdout).trim().to_string();
324        if !path.is_empty() {
325            let candidate = PathBuf::from(path).join("gopls");
326            if candidate.exists() {
327                return Ok(candidate.to_string_lossy().to_string());
328            }
329        }
330    }
331    if let Ok(output) = Command::new("go").args(["env", "GOPATH"]).output()
332        && output.status.success()
333    {
334        let paths = String::from_utf8_lossy(&output.stdout);
335        for path in paths.trim().split(':') {
336            let path = path.trim();
337            if path.is_empty() {
338                continue;
339            }
340            let candidate = PathBuf::from(path).join("bin").join("gopls");
341            if candidate.exists() {
342                return Ok(candidate.to_string_lossy().to_string());
343            }
344        }
345    }
346    Err(
347        "gopls not found. Please set GOPLS/GOPLS_PATH or add GOPATH/bin to PATH"
348            .to_string(),
349    )
350}
351
352/// Ensures rust-analyzer configuration directory exists on macOS.
353///
354/// Creates the configuration directory and an empty config file if they don't exist.
355/// This prevents rust-analyzer from failing on first run on macOS.
356///
357/// # Examples
358///
359/// ```no_run
360/// use iced_code_editor::ensure_rust_analyzer_config;
361///
362/// // Safe to call on any platform; no-op on non-macOS.
363/// ensure_rust_analyzer_config();
364/// ```
365#[cfg(target_os = "macos")]
366pub fn ensure_rust_analyzer_config() {
367    let Some(home) = std::env::var_os("HOME") else { return };
368    let mut path = std::path::PathBuf::from(home);
369    path.push("Library");
370    path.push("Application Support");
371    path.push("rust-analyzer");
372    let _ = std::fs::create_dir_all(&path);
373    path.push("rust-analyzer.toml");
374    if !path.exists() {
375        let _ = std::fs::write(path, "");
376    }
377}
378
379/// No-op on non-macOS platforms.
380///
381/// # Examples
382///
383/// ```no_run
384/// use iced_code_editor::ensure_rust_analyzer_config;
385///
386/// // Safe to call on any platform; no-op on non-macOS.
387/// ensure_rust_analyzer_config();
388/// ```
389#[cfg(not(target_os = "macos"))]
390pub fn ensure_rust_analyzer_config() {}