返回 CodeWhale
registry.rs
根目录 / crates / tui / src / lsp / registry.rs
1 //! Language detection + the fixed dictionary mapping a language to the LSP
2 //! server binary that handles it.
3 //!
4 //! Built-in dictionary covers common languages. Users can override defaults
5 //! via `[lsp.servers]` and register custom language servers for additional
6 //! file extensions via `[lsp.custom]` in their config (handled by
7 //! [`super::LspConfig`], not this file).
8
9 use std::path::Path;
10
11 /// A language we know how to ask an LSP server about. Detected from the file
12 /// extension by [`detect_language`]. `Other` is a sentinel used when we do
13 /// not have an LSP for the file — the LSP manager treats it as "skip".
14 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
15 pub enum Language {
16 Rust,
17 Go,
18 Python,
19 TypeScript,
20 JavaScript,
21 Java,
22 Php,
23 Vue,
24 C,
25 Cpp,
26 Other,
27 }
28
29 impl Language {
30 /// Stable lowercase string used as the key in `[lsp.servers]` overrides
31 /// and in log lines.
32 #[must_use]
33 pub fn as_key(self) -> &'static str {
34 match self {
35 Language::Rust => "rust",
36 Language::Go => "go",
37 Language::Python => "python",
38 Language::TypeScript => "typescript",
39 Language::JavaScript => "javascript",
40 Language::Java => "java",
41 Language::Php => "php",
42 Language::Vue => "vue",
43 Language::C => "c",
44 Language::Cpp => "cpp",
45 Language::Other => "other",
46 }
47 }
48
49 /// LSP `languageId` value used in `textDocument/didOpen`. We follow the
50 /// LSP-spec values: `rust`, `go`, `python`, `typescript`, `javascript`,
51 /// `java`, `vue`, `c`, `cpp`.
52 #[must_use]
53 pub fn language_id(self) -> &'static str {
54 match self {
55 Language::Rust => "rust",
56 Language::Go => "go",
57 Language::Python => "python",
58 Language::TypeScript => "typescript",
59 Language::JavaScript => "javascript",
60 Language::Java => "java",
61 Language::Php => "php",
62 Language::Vue => "vue",
63 Language::C => "c",
64 Language::Cpp => "cpp",
65 Language::Other => "plaintext",
66 }
67 }
68 }
69
70 /// Detect the language of `path` from its extension. Falls back to
71 /// `Language::Other` when the extension is unknown (or the file has none),
72 /// which signals "skip" to the manager.
73 #[must_use]
74 pub fn detect_language(path: &Path) -> Language {
75 let ext = match path.extension().and_then(|e| e.to_str()) {
76 Some(ext) => ext.to_ascii_lowercase(),
77 None => return Language::Other,
78 };
79 match ext.as_str() {
80 "rs" => Language::Rust,
81 "go" => Language::Go,
82 "py" | "pyi" => Language::Python,
83 "ts" | "tsx" => Language::TypeScript,
84 "js" | "jsx" | "mjs" | "cjs" => Language::JavaScript,
85 "java" => Language::Java,
86 "php" => Language::Php,
87 "vue" => Language::Vue,
88 "c" | "h" => Language::C,
89 "cpp" | "cc" | "cxx" | "hpp" | "hxx" | "hh" => Language::Cpp,
90 _ => Language::Other,
91 }
92 }
93
94 /// Fixed default for "what executable + args do we run for `lang`?".
95 /// Returns `None` when no LSP server is wired for that language. The TUI
96 /// config layer can override this dictionary at runtime.
97 #[must_use]
98 pub fn server_for(lang: Language) -> Option<(&'static str, &'static [&'static str])> {
99 match lang {
100 Language::Rust => Some(("rust-analyzer", &[])),
101 Language::Go => Some(("gopls", &["serve"])),
102 Language::Python => Some(("pyright-langserver", &["--stdio"])),
103 Language::TypeScript | Language::JavaScript => {
104 Some(("typescript-language-server", &["--stdio"]))
105 }
106 Language::Java => Some(("jdtls", &[])),
107 Language::Php => Some(("intelephense", &["--stdio"])),
108 Language::Vue => Some(("vue-language-server", &["--stdio"])),
109 Language::C | Language::Cpp => Some(("clangd", &[])),
110 Language::Other => None,
111 }
112 }
113
114 #[cfg(test)]
115 mod tests {
116 use super::*;
117 use std::path::PathBuf;
118
119 #[test]
120 fn detects_rust_extension() {
121 assert_eq!(detect_language(&PathBuf::from("foo.rs")), Language::Rust);
122 assert_eq!(detect_language(&PathBuf::from("FOO.RS")), Language::Rust);
123 }
124
125 #[test]
126 fn detects_unknown_as_other() {
127 assert_eq!(
128 detect_language(&PathBuf::from("notes.txt")),
129 Language::Other
130 );
131 assert_eq!(detect_language(&PathBuf::from("README")), Language::Other);
132 }
133
134 #[test]
135 fn detects_typescript_variants() {
136 assert_eq!(
137 detect_language(&PathBuf::from("foo.ts")),
138 Language::TypeScript
139 );
140 assert_eq!(
141 detect_language(&PathBuf::from("foo.tsx")),
142 Language::TypeScript
143 );
144 assert_eq!(
145 detect_language(&PathBuf::from("foo.js")),
146 Language::JavaScript
147 );
148 }
149
150 #[test]
151 fn detects_java_extension() {
152 assert_eq!(detect_language(&PathBuf::from("App.java")), Language::Java);
153 assert_eq!(detect_language(&PathBuf::from("APP.JAVA")), Language::Java);
154 }
155
156 #[test]
157 fn detects_php_extension() {
158 assert_eq!(detect_language(&PathBuf::from("index.php")), Language::Php);
159 assert_eq!(detect_language(&PathBuf::from("INDEX.PHP")), Language::Php);
160 assert_eq!(detect_language(&PathBuf::from("router.php")), Language::Php);
161 }
162
163 #[test]
164 fn detects_vue_extension() {
165 assert_eq!(
166 detect_language(&PathBuf::from("Component.vue")),
167 Language::Vue
168 );
169 assert_eq!(
170 detect_language(&PathBuf::from("COMPONENT.VUE")),
171 Language::Vue
172 );
173 }
174
175 #[test]
176 fn language_ids_for_php_and_vue_match_lsp_values() {
177 assert_eq!(Language::Php.as_key(), "php");
178 assert_eq!(Language::Php.language_id(), "php");
179 assert_eq!(Language::Vue.as_key(), "vue");
180 assert_eq!(Language::Vue.language_id(), "vue");
181 }
182
183 #[test]
184 fn server_for_php_is_intelephense() {
185 let (cmd, args) = server_for(Language::Php).expect("php has a server");
186 assert_eq!(cmd, "intelephense");
187 assert_eq!(args, &["--stdio"]);
188 }
189
190 #[test]
191 fn language_ids_for_java_and_vue_match_lsp_values() {
192 assert_eq!(Language::Java.as_key(), "java");
193 assert_eq!(Language::Java.language_id(), "java");
194 assert_eq!(Language::Vue.as_key(), "vue");
195 assert_eq!(Language::Vue.language_id(), "vue");
196 }
197
198 #[test]
199 fn server_for_rust_is_rust_analyzer() {
200 let (cmd, args) = server_for(Language::Rust).expect("rust has a server");
201 assert_eq!(cmd, "rust-analyzer");
202 assert!(args.is_empty());
203 }
204
205 #[test]
206 fn server_for_java_is_jdtls() {
207 let (cmd, args) = server_for(Language::Java).expect("java has a server");
208 assert_eq!(cmd, "jdtls");
209 assert!(args.is_empty());
210 }
211
212 #[test]
213 fn server_for_vue_is_vue_language_server() {
214 let (cmd, args) = server_for(Language::Vue).expect("vue has a server");
215 assert_eq!(cmd, "vue-language-server");
216 assert_eq!(args, &["--stdio"]);
217 }
218
219 #[test]
220 fn server_for_other_is_none() {
221 assert!(server_for(Language::Other).is_none());
222 }
223 }
224
224 lines RUST