heartbit_core/lsp/
language.rs1use std::path::Path;
2
3#[derive(Debug, Clone)]
5pub struct LanguageConfig {
6 pub lang_id: &'static str,
8 pub command: &'static str,
10 pub args: &'static [&'static str],
12}
13
14pub 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
43pub 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
59pub fn find_server_config(lang_id: &str) -> Option<&'static LanguageConfig> {
61 BUILTIN_SERVERS.iter().find(|c| c.lang_id == lang_id)
62}
63
64const FILE_MODIFYING_TOOLS: &[&str] = &["write", "edit", "patch"];
66
67pub 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}