Skip to main content

heartbit_core/lsp/
language.rs

1use std::path::Path;
2
3/// Configuration for a language server.
4#[derive(Debug, Clone)]
5pub struct LanguageConfig {
6    /// Language identifier (e.g., "rust", "python").
7    pub lang_id: &'static str,
8    /// Server command (e.g., "rust-analyzer").
9    pub command: &'static str,
10    /// Command arguments.
11    pub args: &'static [&'static str],
12}
13
14/// Built-in language server configurations.
15pub const BUILTIN_SERVERS: &[LanguageConfig] = &[
16    LanguageConfig {
17        lang_id: "rust",
18        command: "rust-analyzer",
19        args: &[],
20    },
21    LanguageConfig {
22        lang_id: "typescript",
23        command: "npx",
24        args: &["typescript-language-server", "--stdio"],
25    },
26    LanguageConfig {
27        lang_id: "python",
28        command: "pyright-langserver",
29        args: &["--stdio"],
30    },
31    LanguageConfig {
32        lang_id: "go",
33        command: "gopls",
34        args: &["serve"],
35    },
36    LanguageConfig {
37        lang_id: "c",
38        command: "clangd",
39        args: &[],
40    },
41];
42
43/// Detect the language ID from a file extension.
44///
45/// Returns `None` for unsupported extensions.
46pub fn detect_language(path: &Path) -> Option<&'static str> {
47    let ext = path.extension()?.to_str()?;
48    match ext {
49        "rs" => Some("rust"),
50        "ts" | "tsx" => Some("typescript"),
51        "js" | "jsx" | "mjs" | "cjs" => Some("typescript"),
52        "py" | "pyi" => Some("python"),
53        "go" => Some("go"),
54        "c" | "h" | "cpp" | "cxx" | "cc" | "hpp" | "hxx" => Some("c"),
55        _ => None,
56    }
57}
58
59/// Find the built-in server config for a language ID.
60pub fn find_server_config(lang_id: &str) -> Option<&'static LanguageConfig> {
61    BUILTIN_SERVERS.iter().find(|c| c.lang_id == lang_id)
62}
63
64/// File-modifying tool names that should trigger LSP diagnostics.
65const FILE_MODIFYING_TOOLS: &[&str] = &["write", "edit", "patch"];
66
67/// Check if a tool name is a file-modifying tool.
68pub fn is_file_modifying_tool(tool_name: &str) -> bool {
69    FILE_MODIFYING_TOOLS.contains(&tool_name)
70}
71
72#[cfg(test)]
73mod tests {
74    use super::*;
75    use std::path::PathBuf;
76
77    #[test]
78    fn detect_rust() {
79        assert_eq!(detect_language(&PathBuf::from("src/main.rs")), Some("rust"));
80    }
81
82    #[test]
83    fn detect_typescript() {
84        assert_eq!(
85            detect_language(&PathBuf::from("app/index.ts")),
86            Some("typescript")
87        );
88        assert_eq!(
89            detect_language(&PathBuf::from("app/App.tsx")),
90            Some("typescript")
91        );
92    }
93
94    #[test]
95    fn detect_javascript_maps_to_typescript() {
96        assert_eq!(
97            detect_language(&PathBuf::from("lib/utils.js")),
98            Some("typescript")
99        );
100        assert_eq!(
101            detect_language(&PathBuf::from("lib/utils.jsx")),
102            Some("typescript")
103        );
104        assert_eq!(
105            detect_language(&PathBuf::from("lib/utils.mjs")),
106            Some("typescript")
107        );
108        assert_eq!(
109            detect_language(&PathBuf::from("lib/utils.cjs")),
110            Some("typescript")
111        );
112    }
113
114    #[test]
115    fn detect_python() {
116        assert_eq!(detect_language(&PathBuf::from("script.py")), Some("python"));
117        assert_eq!(detect_language(&PathBuf::from("types.pyi")), Some("python"));
118    }
119
120    #[test]
121    fn detect_go() {
122        assert_eq!(detect_language(&PathBuf::from("main.go")), Some("go"));
123    }
124
125    #[test]
126    fn detect_c_family() {
127        assert_eq!(detect_language(&PathBuf::from("main.c")), Some("c"));
128        assert_eq!(detect_language(&PathBuf::from("main.cpp")), Some("c"));
129        assert_eq!(detect_language(&PathBuf::from("main.h")), Some("c"));
130        assert_eq!(detect_language(&PathBuf::from("main.hpp")), Some("c"));
131        assert_eq!(detect_language(&PathBuf::from("main.cc")), Some("c"));
132        assert_eq!(detect_language(&PathBuf::from("main.cxx")), Some("c"));
133        assert_eq!(detect_language(&PathBuf::from("main.hxx")), Some("c"));
134    }
135
136    #[test]
137    fn detect_unsupported_returns_none() {
138        assert_eq!(detect_language(&PathBuf::from("README.md")), None);
139        assert_eq!(detect_language(&PathBuf::from("Cargo.toml")), None);
140        assert_eq!(detect_language(&PathBuf::from("data.json")), None);
141    }
142
143    #[test]
144    fn detect_no_extension_returns_none() {
145        assert_eq!(detect_language(&PathBuf::from("Makefile")), None);
146    }
147
148    #[test]
149    fn find_server_config_rust() {
150        let config = find_server_config("rust").unwrap();
151        assert_eq!(config.command, "rust-analyzer");
152    }
153
154    #[test]
155    fn find_server_config_typescript() {
156        let config = find_server_config("typescript").unwrap();
157        assert_eq!(config.command, "npx");
158    }
159
160    #[test]
161    fn find_server_config_unknown() {
162        assert!(find_server_config("brainfuck").is_none());
163    }
164
165    #[test]
166    fn file_modifying_tool_detection() {
167        assert!(is_file_modifying_tool("write"));
168        assert!(is_file_modifying_tool("edit"));
169        assert!(is_file_modifying_tool("patch"));
170        assert!(!is_file_modifying_tool("read"));
171        assert!(!is_file_modifying_tool("bash"));
172        assert!(!is_file_modifying_tool("search"));
173    }
174
175    #[test]
176    fn builtin_servers_has_five_entries() {
177        assert_eq!(BUILTIN_SERVERS.len(), 5);
178    }
179}