Skip to main content

ane/data/lsp/
types.rs

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    /// Returns true if a transition from `self` to `new` is permitted by the
46    /// engine's state machine. Any unlisted pair is invalid.
47    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        // Markdown has no LSP languageId
355        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}