1use std::collections::HashMap;
2use std::path::PathBuf;
3
4#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5pub enum ServerState {
6 Undetected,
7 Missing,
8 Available,
9 Installing,
10 Starting,
11 Running,
12 Stopped,
13 Failed,
14}
15
16impl ServerState {
17 pub fn display(&self) -> &'static str {
18 match self {
19 Self::Undetected => "LSP: checking...",
20 Self::Missing => "LSP: not installed",
21 Self::Available => "LSP: available",
22 Self::Installing => "LSP: installing...",
23 Self::Starting => "LSP: starting...",
24 Self::Running => "LSP: ready",
25 Self::Stopped => "LSP: stopped",
26 Self::Failed => "LSP: failed",
27 }
28 }
29
30 pub fn is_available(&self) -> bool {
31 matches!(self, Self::Running)
32 }
33
34 pub fn is_pending(&self) -> bool {
35 matches!(
36 self,
37 Self::Undetected | Self::Installing | Self::Starting | Self::Available
38 )
39 }
40
41 pub fn is_terminal(&self) -> bool {
42 matches!(self, Self::Running | Self::Failed | Self::Stopped)
43 }
44
45 pub fn can_transition_to(self, new: Self) -> bool {
48 use ServerState::*;
49 if self == new {
50 return false;
51 }
52 matches!(
53 (self, new),
54 (
55 Undetected,
56 Missing | Available | Installing | Failed | Stopped
57 ) | (Missing, Installing | Available | Failed | Stopped)
58 | (Installing, Available | Failed | Stopped)
59 | (Available, Starting | Stopped | Failed)
60 | (Starting, Running | Failed | Stopped)
61 | (Running, Stopped | Failed)
62 | (Failed, Available | Installing | Starting | Stopped)
63 | (Stopped, Available | Starting)
64 )
65 }
66}
67
68#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
69pub enum Language {
70 Rust,
71 Go,
72 TypeScript,
73 Python,
74 Markdown,
75}
76
77pub struct LanguageCapabilities {
78 pub has_tree_sitter: bool,
79 pub has_lsp: bool,
80}
81
82impl Language {
83 pub fn capabilities(self) -> LanguageCapabilities {
84 match self {
85 Language::Rust => LanguageCapabilities {
86 has_tree_sitter: true,
87 has_lsp: true,
88 },
89 Language::Go => LanguageCapabilities {
90 has_tree_sitter: true,
91 has_lsp: true,
92 },
93 Language::TypeScript => LanguageCapabilities {
94 has_tree_sitter: true,
95 has_lsp: true,
96 },
97 Language::Python => LanguageCapabilities {
98 has_tree_sitter: true,
99 has_lsp: true,
100 },
101 Language::Markdown => LanguageCapabilities {
102 has_tree_sitter: true,
103 has_lsp: false,
104 },
105 }
106 }
107
108 pub fn from_extension(ext: &str) -> Option<Self> {
109 match ext {
110 "rs" => Some(Self::Rust),
111 "go" => Some(Self::Go),
112 "ts" | "tsx" | "js" | "jsx" => Some(Self::TypeScript),
113 "py" => Some(Self::Python),
114 "md" | "markdown" => Some(Self::Markdown),
115 _ => None,
116 }
117 }
118
119 pub fn from_path(path: &std::path::Path) -> Option<Self> {
120 path.extension()
121 .and_then(|ext| ext.to_str())
122 .and_then(Self::from_extension)
123 }
124
125 pub fn language_id_for_path(path: &std::path::Path) -> Option<&'static str> {
126 path.extension()
127 .and_then(|e| e.to_str())
128 .and_then(|ext| match ext {
129 "rs" => Some("rust"),
130 "go" => Some("go"),
131 "ts" => Some("typescript"),
132 "tsx" => Some("typescriptreact"),
133 "js" => Some("javascript"),
134 "jsx" => Some("javascriptreact"),
135 "py" => Some("python"),
136 _ => None,
137 })
138 }
139
140 pub fn name(&self) -> &'static str {
141 match self {
142 Self::Rust => "rust",
143 Self::Go => "go",
144 Self::TypeScript => "typescript",
145 Self::Python => "python",
146 Self::Markdown => "markdown",
147 }
148 }
149
150 pub fn short_name(&self) -> &'static str {
151 match self {
152 Self::Rust => "rs",
153 Self::Go => "go",
154 Self::TypeScript => "ts",
155 Self::Python => "py",
156 Self::Markdown => "md",
157 }
158 }
159}
160
161#[derive(Debug, Clone)]
162pub struct DocumentSymbol {
163 pub name: String,
164 pub kind: SymbolKind,
165 pub range: SymbolRange,
166 pub selection_range: Option<SymbolRange>,
167 pub children: Vec<DocumentSymbol>,
168}
169
170#[derive(Debug, Clone, Copy, PartialEq, Eq)]
171pub struct SymbolRange {
172 pub start_line: usize,
173 pub start_col: usize,
174 pub end_line: usize,
175 pub end_col: usize,
176}
177
178#[derive(Debug, Clone, PartialEq, Eq)]
179pub enum SymbolKind {
180 Function,
181 Variable,
182 Struct,
183 Enum,
184 Impl,
185 Const,
186 Field,
187 Method,
188 Module,
189 Other(String),
190}
191
192#[derive(Debug, Clone)]
193pub struct CompletionItem {
194 pub label: String,
195 pub detail: Option<String>,
196 pub kind: Option<String>,
197}
198
199#[derive(Debug, Clone)]
200pub struct HoverInfo {
201 pub contents: String,
202}
203
204#[derive(Debug, Clone)]
205pub struct Location {
206 pub file_path: PathBuf,
207 pub range: SymbolRange,
208}
209
210#[derive(Debug, Clone)]
211pub enum LspEvent {
212 StateChanged {
213 language: Language,
214 old: ServerState,
215 new: ServerState,
216 },
217 DiagnosticsReceived {
218 file_path: PathBuf,
219 diagnostics: Vec<Diagnostic>,
220 },
221 ServerMessage {
222 language: Language,
223 message: String,
224 },
225 Error {
226 language: Language,
227 error: String,
228 },
229}
230
231#[derive(Debug, Clone)]
232pub struct Diagnostic {
233 pub range: SymbolRange,
234 pub severity: DiagnosticSeverity,
235 pub message: String,
236}
237
238#[derive(Debug, Clone, Copy, PartialEq, Eq)]
239pub enum DiagnosticSeverity {
240 Error,
241 Warning,
242 Information,
243 Hint,
244}
245
246#[derive(Debug, Clone)]
247pub struct SemanticToken {
248 pub line: usize,
249 pub start_col: usize,
250 pub length: usize,
251 pub token_type: String,
252}
253
254#[derive(Debug, Clone)]
255pub struct SelectionRange {
256 pub range: SymbolRange,
257 pub parent: Option<Box<SelectionRange>>,
258}
259
260#[derive(Debug, Clone, PartialEq, Eq)]
261pub enum InstallLine {
262 Stdout(String),
263 Stderr(String),
264 Failed(String),
265}
266
267#[derive(Debug, Clone, Default)]
268pub struct LspSharedState {
269 pub status: HashMap<Language, ServerState>,
270 pub install_line: Option<InstallLine>,
271}
272
273pub struct LspServerInfo {
274 pub language: Language,
275 pub server_name: &'static str,
276 pub binary_name: &'static str,
277 pub install_command: &'static str,
278 pub check_command: &'static str,
279 pub default_args: &'static [&'static str],
280 pub init_options_json: &'static str,
281}
282
283#[cfg(test)]
284mod tests {
285 use super::*;
286
287 #[test]
288 fn language_capabilities_all_variants() {
289 let rust = Language::Rust.capabilities();
290 assert!(rust.has_tree_sitter);
291 assert!(rust.has_lsp);
292
293 let go = Language::Go.capabilities();
294 assert!(go.has_tree_sitter);
295 assert!(go.has_lsp);
296
297 let ts = Language::TypeScript.capabilities();
298 assert!(ts.has_tree_sitter);
299 assert!(ts.has_lsp);
300
301 let py = Language::Python.capabilities();
302 assert!(py.has_tree_sitter);
303 assert!(py.has_lsp);
304
305 let md = Language::Markdown.capabilities();
306 assert!(md.has_tree_sitter);
307 assert!(!md.has_lsp, "Markdown has no LSP server");
308 }
309
310 #[test]
311 fn language_from_extension_work_item_cases() {
312 assert_eq!(Language::from_extension("md"), Some(Language::Markdown));
313 assert_eq!(
314 Language::from_extension("markdown"),
315 Some(Language::Markdown)
316 );
317 assert_eq!(Language::from_extension("tsx"), Some(Language::TypeScript));
318 assert_eq!(Language::from_extension("py"), Some(Language::Python));
319 assert_eq!(Language::from_extension("go"), Some(Language::Go));
320 assert_eq!(Language::from_extension("rs"), Some(Language::Rust));
321 }
322
323 #[test]
324 fn language_id_for_path_all_cases() {
325 use std::path::Path;
326 assert_eq!(
327 Language::language_id_for_path(Path::new("foo.tsx")),
328 Some("typescriptreact")
329 );
330 assert_eq!(
331 Language::language_id_for_path(Path::new("foo.jsx")),
332 Some("javascriptreact")
333 );
334 assert_eq!(
335 Language::language_id_for_path(Path::new("foo.ts")),
336 Some("typescript")
337 );
338 assert_eq!(
339 Language::language_id_for_path(Path::new("foo.js")),
340 Some("javascript")
341 );
342 assert_eq!(
343 Language::language_id_for_path(Path::new("foo.rs")),
344 Some("rust")
345 );
346 assert_eq!(
347 Language::language_id_for_path(Path::new("foo.go")),
348 Some("go")
349 );
350 assert_eq!(
351 Language::language_id_for_path(Path::new("foo.py")),
352 Some("python")
353 );
354 assert_eq!(Language::language_id_for_path(Path::new("foo.md")), None);
356 }
357
358 #[test]
359 fn language_detection() {
360 assert_eq!(Language::from_extension("rs"), Some(Language::Rust));
361 assert_eq!(Language::from_extension("go"), Some(Language::Go));
362 assert_eq!(Language::from_extension("ts"), Some(Language::TypeScript));
363 assert_eq!(Language::from_extension("tsx"), Some(Language::TypeScript));
364 assert_eq!(Language::from_extension("js"), Some(Language::TypeScript));
365 assert_eq!(Language::from_extension("jsx"), Some(Language::TypeScript));
366 assert_eq!(Language::from_extension("py"), Some(Language::Python));
367 assert_eq!(Language::from_extension("md"), Some(Language::Markdown));
368 assert_eq!(
369 Language::from_extension("markdown"),
370 Some(Language::Markdown)
371 );
372 assert_eq!(Language::from_extension(""), None);
373 assert_eq!(Language::from_extension("txt"), None);
374 }
375
376 #[test]
377 fn language_name() {
378 assert_eq!(Language::Rust.name(), "rust");
379 assert_eq!(Language::Go.name(), "go");
380 assert_eq!(Language::TypeScript.name(), "typescript");
381 assert_eq!(Language::Python.name(), "python");
382 assert_eq!(Language::Markdown.name(), "markdown");
383 }
384
385 #[test]
386 fn is_available_only_for_running() {
387 assert!(ServerState::Running.is_available());
388 assert!(!ServerState::Undetected.is_available());
389 assert!(!ServerState::Missing.is_available());
390 assert!(!ServerState::Available.is_available());
391 assert!(!ServerState::Installing.is_available());
392 assert!(!ServerState::Starting.is_available());
393 assert!(!ServerState::Stopped.is_available());
394 assert!(!ServerState::Failed.is_available());
395 }
396
397 #[test]
398 fn is_pending_covers_transient_states() {
399 assert!(ServerState::Undetected.is_pending());
400 assert!(ServerState::Installing.is_pending());
401 assert!(ServerState::Starting.is_pending());
402 assert!(ServerState::Available.is_pending());
403 assert!(!ServerState::Running.is_pending());
404 assert!(!ServerState::Missing.is_pending());
405 assert!(!ServerState::Stopped.is_pending());
406 assert!(!ServerState::Failed.is_pending());
407 }
408
409 #[test]
410 fn is_terminal_covers_final_states() {
411 assert!(ServerState::Running.is_terminal());
412 assert!(ServerState::Failed.is_terminal());
413 assert!(ServerState::Stopped.is_terminal());
414 assert!(!ServerState::Undetected.is_terminal());
415 assert!(!ServerState::Missing.is_terminal());
416 assert!(!ServerState::Available.is_terminal());
417 assert!(!ServerState::Installing.is_terminal());
418 assert!(!ServerState::Starting.is_terminal());
419 }
420
421 #[test]
422 fn terminal_states_are_not_pending() {
423 for state in [
424 ServerState::Running,
425 ServerState::Failed,
426 ServerState::Stopped,
427 ] {
428 assert!(!state.is_pending(), "{:?} should not be pending", state);
429 }
430 }
431
432 #[test]
433 fn pending_states_are_not_terminal() {
434 for state in [
435 ServerState::Undetected,
436 ServerState::Installing,
437 ServerState::Starting,
438 ServerState::Available,
439 ] {
440 assert!(!state.is_terminal(), "{:?} should not be terminal", state);
441 }
442 }
443
444 #[test]
445 fn all_states_have_display() {
446 assert_eq!(ServerState::Undetected.display(), "LSP: checking...");
447 assert_eq!(ServerState::Missing.display(), "LSP: not installed");
448 assert_eq!(ServerState::Available.display(), "LSP: available");
449 assert_eq!(ServerState::Installing.display(), "LSP: installing...");
450 assert_eq!(ServerState::Starting.display(), "LSP: starting...");
451 assert_eq!(ServerState::Running.display(), "LSP: ready");
452 assert_eq!(ServerState::Stopped.display(), "LSP: stopped");
453 assert_eq!(ServerState::Failed.display(), "LSP: failed");
454 }
455
456 #[test]
457 fn state_equality() {
458 assert_eq!(ServerState::Running, ServerState::Running);
459 assert_ne!(ServerState::Running, ServerState::Failed);
460 assert_ne!(ServerState::Stopped, ServerState::Failed);
461 }
462
463 #[test]
464 fn symbol_kind_other_preserves_content() {
465 let kind = SymbolKind::Other("custom_42".to_string());
466 assert_eq!(kind, SymbolKind::Other("custom_42".to_string()));
467 assert_ne!(kind, SymbolKind::Other("custom_99".to_string()));
468 assert_ne!(kind, SymbolKind::Function);
469 }
470
471 #[test]
472 fn symbol_range_equality() {
473 let r1 = SymbolRange {
474 start_line: 0,
475 start_col: 0,
476 end_line: 5,
477 end_col: 10,
478 };
479 let r2 = SymbolRange {
480 start_line: 0,
481 start_col: 0,
482 end_line: 5,
483 end_col: 10,
484 };
485 assert_eq!(r1, r2);
486 }
487
488 #[test]
489 fn can_transition_to_allows_normal_startup_path() {
490 assert!(ServerState::Undetected.can_transition_to(ServerState::Available));
491 assert!(ServerState::Available.can_transition_to(ServerState::Starting));
492 assert!(ServerState::Starting.can_transition_to(ServerState::Running));
493 assert!(ServerState::Running.can_transition_to(ServerState::Stopped));
494 }
495
496 #[test]
497 fn can_transition_to_allows_install_path() {
498 assert!(ServerState::Undetected.can_transition_to(ServerState::Missing));
499 assert!(ServerState::Missing.can_transition_to(ServerState::Installing));
500 assert!(ServerState::Installing.can_transition_to(ServerState::Available));
501 assert!(ServerState::Installing.can_transition_to(ServerState::Failed));
502 }
503
504 #[test]
505 fn can_transition_to_allows_failure_paths() {
506 assert!(ServerState::Starting.can_transition_to(ServerState::Failed));
507 assert!(ServerState::Running.can_transition_to(ServerState::Failed));
508 assert!(ServerState::Available.can_transition_to(ServerState::Failed));
509 }
510
511 #[test]
512 fn can_transition_to_allows_retry_from_failed() {
513 assert!(ServerState::Failed.can_transition_to(ServerState::Available));
514 assert!(ServerState::Failed.can_transition_to(ServerState::Installing));
515 assert!(ServerState::Failed.can_transition_to(ServerState::Starting));
516 }
517
518 #[test]
519 fn can_transition_to_rejects_invalid_jumps() {
520 assert!(!ServerState::Undetected.can_transition_to(ServerState::Running));
521 assert!(!ServerState::Undetected.can_transition_to(ServerState::Starting));
522 assert!(!ServerState::Missing.can_transition_to(ServerState::Running));
523 assert!(!ServerState::Available.can_transition_to(ServerState::Running));
524 assert!(!ServerState::Installing.can_transition_to(ServerState::Running));
525 assert!(!ServerState::Installing.can_transition_to(ServerState::Starting));
526 assert!(!ServerState::Running.can_transition_to(ServerState::Undetected));
527 assert!(!ServerState::Running.can_transition_to(ServerState::Available));
528 assert!(!ServerState::Running.can_transition_to(ServerState::Starting));
529 assert!(!ServerState::Stopped.can_transition_to(ServerState::Running));
530 assert!(!ServerState::Stopped.can_transition_to(ServerState::Failed));
531 }
532
533 #[test]
534 fn can_transition_to_rejects_self_transitions() {
535 for state in [
536 ServerState::Undetected,
537 ServerState::Missing,
538 ServerState::Available,
539 ServerState::Installing,
540 ServerState::Starting,
541 ServerState::Running,
542 ServerState::Stopped,
543 ServerState::Failed,
544 ] {
545 assert!(
546 !state.can_transition_to(state),
547 "{:?} → {:?} should not be allowed",
548 state,
549 state
550 );
551 }
552 }
553
554 #[test]
555 fn lsp_event_state_changed_fields() {
556 let event = LspEvent::StateChanged {
557 language: Language::Rust,
558 old: ServerState::Starting,
559 new: ServerState::Running,
560 };
561 match event {
562 LspEvent::StateChanged { language, old, new } => {
563 assert_eq!(language, Language::Rust);
564 assert_eq!(old, ServerState::Starting);
565 assert_eq!(new, ServerState::Running);
566 }
567 _ => panic!("wrong variant"),
568 }
569 }
570}