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    Json,
76    Yaml,
77    Toml,
78    Dockerfile,
79    Xml,
80}
81
82pub struct LanguageCapabilities {
83    pub has_tree_sitter: bool,
84    pub has_lsp: bool,
85}
86
87impl Language {
88    pub fn capabilities(self) -> LanguageCapabilities {
89        match self {
90            Language::Rust => LanguageCapabilities {
91                has_tree_sitter: true,
92                has_lsp: true,
93            },
94            Language::Go => LanguageCapabilities {
95                has_tree_sitter: true,
96                has_lsp: true,
97            },
98            Language::TypeScript => LanguageCapabilities {
99                has_tree_sitter: true,
100                has_lsp: true,
101            },
102            Language::Python => LanguageCapabilities {
103                has_tree_sitter: true,
104                has_lsp: true,
105            },
106            Language::Markdown => LanguageCapabilities {
107                has_tree_sitter: true,
108                has_lsp: false,
109            },
110            Language::Json => LanguageCapabilities {
111                has_tree_sitter: true,
112                has_lsp: false,
113            },
114            Language::Yaml => LanguageCapabilities {
115                has_tree_sitter: true,
116                has_lsp: false,
117            },
118            Language::Toml => LanguageCapabilities {
119                has_tree_sitter: true,
120                has_lsp: false,
121            },
122            Language::Dockerfile => LanguageCapabilities {
123                has_tree_sitter: true,
124                has_lsp: false,
125            },
126            Language::Xml => LanguageCapabilities {
127                has_tree_sitter: true,
128                has_lsp: false,
129            },
130        }
131    }
132
133    pub fn from_extension(ext: &str) -> Option<Self> {
134        match ext {
135            "rs" => Some(Self::Rust),
136            "go" => Some(Self::Go),
137            "ts" | "tsx" | "js" | "jsx" => Some(Self::TypeScript),
138            "py" => Some(Self::Python),
139            "md" | "markdown" => Some(Self::Markdown),
140            "json" | "jsonc" => Some(Self::Json),
141            "yaml" | "yml" => Some(Self::Yaml),
142            "toml" => Some(Self::Toml),
143            "dockerfile" => Some(Self::Dockerfile),
144            "xml" | "xsd" | "xsl" | "xslt" | "svg" | "rss" => Some(Self::Xml),
145            _ => None,
146        }
147    }
148
149    pub fn from_path(path: &std::path::Path) -> Option<Self> {
150        if let Some(ext) = path.extension().and_then(|e| e.to_str())
151            && let Some(lang) = Self::from_extension(ext)
152        {
153            return Some(lang);
154        }
155        let file_name = path.file_name().and_then(|n| n.to_str()).unwrap_or("");
156        let lower = file_name.to_ascii_lowercase();
157        if lower == "docker-compose.yml" || lower == "docker-compose.yaml" {
158            return Some(Self::Yaml);
159        }
160        // Match `Dockerfile`, `dockerfile`, and any `Dockerfile{suffix}` variant
161        // (e.g. `Dockerfile.dev`, `Dockerfile-prod`) case-insensitively.
162        if lower.starts_with("dockerfile") {
163            return Some(Self::Dockerfile);
164        }
165        None
166    }
167
168    pub fn language_id_for_path(path: &std::path::Path) -> Option<&'static str> {
169        path.extension()
170            .and_then(|e| e.to_str())
171            .and_then(|ext| match ext {
172                "rs" => Some("rust"),
173                "go" => Some("go"),
174                "ts" => Some("typescript"),
175                "tsx" => Some("typescriptreact"),
176                "js" => Some("javascript"),
177                "jsx" => Some("javascriptreact"),
178                "py" => Some("python"),
179                _ => None,
180            })
181    }
182
183    pub fn name(&self) -> &'static str {
184        match self {
185            Self::Rust => "rust",
186            Self::Go => "go",
187            Self::TypeScript => "typescript",
188            Self::Python => "python",
189            Self::Markdown => "markdown",
190            Self::Json => "json",
191            Self::Yaml => "yaml",
192            Self::Toml => "toml",
193            Self::Dockerfile => "dockerfile",
194            Self::Xml => "xml",
195        }
196    }
197
198    pub fn short_name(&self) -> &'static str {
199        match self {
200            Self::Rust => "rs",
201            Self::Go => "go",
202            Self::TypeScript => "ts",
203            Self::Python => "py",
204            Self::Markdown => "md",
205            Self::Json => "json",
206            Self::Yaml => "yaml",
207            Self::Toml => "toml",
208            Self::Dockerfile => "docker",
209            Self::Xml => "xml",
210        }
211    }
212}
213
214#[derive(Debug, Clone)]
215pub struct DocumentSymbol {
216    pub name: String,
217    pub kind: SymbolKind,
218    pub range: SymbolRange,
219    pub selection_range: Option<SymbolRange>,
220    pub children: Vec<DocumentSymbol>,
221}
222
223#[derive(Debug, Clone, Copy, PartialEq, Eq)]
224pub struct SymbolRange {
225    pub start_line: usize,
226    pub start_col: usize,
227    pub end_line: usize,
228    pub end_col: usize,
229}
230
231#[derive(Debug, Clone, PartialEq, Eq)]
232pub enum SymbolKind {
233    Function,
234    Variable,
235    Struct,
236    Enum,
237    Impl,
238    Const,
239    Field,
240    Method,
241    Module,
242    Other(String),
243}
244
245#[derive(Debug, Clone)]
246pub struct CompletionItem {
247    pub label: String,
248    pub detail: Option<String>,
249    pub kind: Option<String>,
250}
251
252#[derive(Debug, Clone)]
253pub struct HoverInfo {
254    pub contents: String,
255}
256
257#[derive(Debug, Clone)]
258pub struct Location {
259    pub file_path: PathBuf,
260    pub range: SymbolRange,
261}
262
263#[derive(Debug, Clone)]
264pub enum LspEvent {
265    StateChanged {
266        language: Language,
267        old: ServerState,
268        new: ServerState,
269    },
270    DiagnosticsReceived {
271        file_path: PathBuf,
272        diagnostics: Vec<Diagnostic>,
273    },
274    ServerMessage {
275        language: Language,
276        message: String,
277    },
278    Error {
279        language: Language,
280        error: String,
281    },
282}
283
284#[derive(Debug, Clone)]
285pub struct Diagnostic {
286    pub range: SymbolRange,
287    pub severity: DiagnosticSeverity,
288    pub message: String,
289}
290
291#[derive(Debug, Clone, Copy, PartialEq, Eq)]
292pub enum DiagnosticSeverity {
293    Error,
294    Warning,
295    Information,
296    Hint,
297}
298
299#[derive(Debug, Clone)]
300pub struct SemanticToken {
301    pub line: usize,
302    pub start_col: usize,
303    pub length: usize,
304    pub token_type: String,
305}
306
307#[derive(Debug, Clone)]
308pub struct SelectionRange {
309    pub range: SymbolRange,
310    pub parent: Option<Box<SelectionRange>>,
311}
312
313#[derive(Debug, Clone, PartialEq, Eq)]
314pub enum InstallLine {
315    Stdout(String),
316    Stderr(String),
317    Failed(String),
318}
319
320#[derive(Debug, Clone, Default)]
321pub struct LspSharedState {
322    pub status: HashMap<Language, ServerState>,
323    pub install_line: Option<InstallLine>,
324}
325
326pub struct LspServerInfo {
327    pub language: Language,
328    pub server_name: &'static str,
329    pub binary_name: &'static str,
330    pub install_command: &'static str,
331    pub check_command: &'static str,
332    pub default_args: &'static [&'static str],
333    pub init_options_json: &'static str,
334}
335
336#[cfg(test)]
337mod tests {
338    use super::*;
339
340    #[test]
341    fn language_capabilities_all_variants() {
342        let rust = Language::Rust.capabilities();
343        assert!(rust.has_tree_sitter);
344        assert!(rust.has_lsp);
345
346        let go = Language::Go.capabilities();
347        assert!(go.has_tree_sitter);
348        assert!(go.has_lsp);
349
350        let ts = Language::TypeScript.capabilities();
351        assert!(ts.has_tree_sitter);
352        assert!(ts.has_lsp);
353
354        let py = Language::Python.capabilities();
355        assert!(py.has_tree_sitter);
356        assert!(py.has_lsp);
357
358        let md = Language::Markdown.capabilities();
359        assert!(md.has_tree_sitter);
360        assert!(!md.has_lsp, "Markdown has no LSP server");
361    }
362
363    #[test]
364    fn language_from_extension_work_item_cases() {
365        assert_eq!(Language::from_extension("md"), Some(Language::Markdown));
366        assert_eq!(
367            Language::from_extension("markdown"),
368            Some(Language::Markdown)
369        );
370        assert_eq!(Language::from_extension("tsx"), Some(Language::TypeScript));
371        assert_eq!(Language::from_extension("py"), Some(Language::Python));
372        assert_eq!(Language::from_extension("go"), Some(Language::Go));
373        assert_eq!(Language::from_extension("rs"), Some(Language::Rust));
374    }
375
376    #[test]
377    fn language_id_for_path_all_cases() {
378        use std::path::Path;
379        assert_eq!(
380            Language::language_id_for_path(Path::new("foo.tsx")),
381            Some("typescriptreact")
382        );
383        assert_eq!(
384            Language::language_id_for_path(Path::new("foo.jsx")),
385            Some("javascriptreact")
386        );
387        assert_eq!(
388            Language::language_id_for_path(Path::new("foo.ts")),
389            Some("typescript")
390        );
391        assert_eq!(
392            Language::language_id_for_path(Path::new("foo.js")),
393            Some("javascript")
394        );
395        assert_eq!(
396            Language::language_id_for_path(Path::new("foo.rs")),
397            Some("rust")
398        );
399        assert_eq!(
400            Language::language_id_for_path(Path::new("foo.go")),
401            Some("go")
402        );
403        assert_eq!(
404            Language::language_id_for_path(Path::new("foo.py")),
405            Some("python")
406        );
407        // Markdown has no LSP languageId
408        assert_eq!(Language::language_id_for_path(Path::new("foo.md")), None);
409    }
410
411    #[test]
412    fn language_detection() {
413        assert_eq!(Language::from_extension("rs"), Some(Language::Rust));
414        assert_eq!(Language::from_extension("go"), Some(Language::Go));
415        assert_eq!(Language::from_extension("ts"), Some(Language::TypeScript));
416        assert_eq!(Language::from_extension("tsx"), Some(Language::TypeScript));
417        assert_eq!(Language::from_extension("js"), Some(Language::TypeScript));
418        assert_eq!(Language::from_extension("jsx"), Some(Language::TypeScript));
419        assert_eq!(Language::from_extension("py"), Some(Language::Python));
420        assert_eq!(Language::from_extension("md"), Some(Language::Markdown));
421        assert_eq!(
422            Language::from_extension("markdown"),
423            Some(Language::Markdown)
424        );
425        assert_eq!(Language::from_extension(""), None);
426        assert_eq!(Language::from_extension("txt"), None);
427    }
428
429    #[test]
430    fn language_name() {
431        assert_eq!(Language::Rust.name(), "rust");
432        assert_eq!(Language::Go.name(), "go");
433        assert_eq!(Language::TypeScript.name(), "typescript");
434        assert_eq!(Language::Python.name(), "python");
435        assert_eq!(Language::Markdown.name(), "markdown");
436    }
437
438    #[test]
439    fn is_available_only_for_running() {
440        assert!(ServerState::Running.is_available());
441        assert!(!ServerState::Undetected.is_available());
442        assert!(!ServerState::Missing.is_available());
443        assert!(!ServerState::Available.is_available());
444        assert!(!ServerState::Installing.is_available());
445        assert!(!ServerState::Starting.is_available());
446        assert!(!ServerState::Stopped.is_available());
447        assert!(!ServerState::Failed.is_available());
448    }
449
450    #[test]
451    fn is_pending_covers_transient_states() {
452        assert!(ServerState::Undetected.is_pending());
453        assert!(ServerState::Installing.is_pending());
454        assert!(ServerState::Starting.is_pending());
455        assert!(ServerState::Available.is_pending());
456        assert!(!ServerState::Running.is_pending());
457        assert!(!ServerState::Missing.is_pending());
458        assert!(!ServerState::Stopped.is_pending());
459        assert!(!ServerState::Failed.is_pending());
460    }
461
462    #[test]
463    fn is_terminal_covers_final_states() {
464        assert!(ServerState::Running.is_terminal());
465        assert!(ServerState::Failed.is_terminal());
466        assert!(ServerState::Stopped.is_terminal());
467        assert!(!ServerState::Undetected.is_terminal());
468        assert!(!ServerState::Missing.is_terminal());
469        assert!(!ServerState::Available.is_terminal());
470        assert!(!ServerState::Installing.is_terminal());
471        assert!(!ServerState::Starting.is_terminal());
472    }
473
474    #[test]
475    fn terminal_states_are_not_pending() {
476        for state in [
477            ServerState::Running,
478            ServerState::Failed,
479            ServerState::Stopped,
480        ] {
481            assert!(!state.is_pending(), "{:?} should not be pending", state);
482        }
483    }
484
485    #[test]
486    fn pending_states_are_not_terminal() {
487        for state in [
488            ServerState::Undetected,
489            ServerState::Installing,
490            ServerState::Starting,
491            ServerState::Available,
492        ] {
493            assert!(!state.is_terminal(), "{:?} should not be terminal", state);
494        }
495    }
496
497    #[test]
498    fn all_states_have_display() {
499        assert_eq!(ServerState::Undetected.display(), "LSP: checking...");
500        assert_eq!(ServerState::Missing.display(), "LSP: not installed");
501        assert_eq!(ServerState::Available.display(), "LSP: available");
502        assert_eq!(ServerState::Installing.display(), "LSP: installing...");
503        assert_eq!(ServerState::Starting.display(), "LSP: starting...");
504        assert_eq!(ServerState::Running.display(), "LSP: ready");
505        assert_eq!(ServerState::Stopped.display(), "LSP: stopped");
506        assert_eq!(ServerState::Failed.display(), "LSP: failed");
507    }
508
509    #[test]
510    fn state_equality() {
511        assert_eq!(ServerState::Running, ServerState::Running);
512        assert_ne!(ServerState::Running, ServerState::Failed);
513        assert_ne!(ServerState::Stopped, ServerState::Failed);
514    }
515
516    #[test]
517    fn symbol_kind_other_preserves_content() {
518        let kind = SymbolKind::Other("custom_42".to_string());
519        assert_eq!(kind, SymbolKind::Other("custom_42".to_string()));
520        assert_ne!(kind, SymbolKind::Other("custom_99".to_string()));
521        assert_ne!(kind, SymbolKind::Function);
522    }
523
524    #[test]
525    fn symbol_range_equality() {
526        let r1 = SymbolRange {
527            start_line: 0,
528            start_col: 0,
529            end_line: 5,
530            end_col: 10,
531        };
532        let r2 = SymbolRange {
533            start_line: 0,
534            start_col: 0,
535            end_line: 5,
536            end_col: 10,
537        };
538        assert_eq!(r1, r2);
539    }
540
541    #[test]
542    fn can_transition_to_allows_normal_startup_path() {
543        assert!(ServerState::Undetected.can_transition_to(ServerState::Available));
544        assert!(ServerState::Available.can_transition_to(ServerState::Starting));
545        assert!(ServerState::Starting.can_transition_to(ServerState::Running));
546        assert!(ServerState::Running.can_transition_to(ServerState::Stopped));
547    }
548
549    #[test]
550    fn can_transition_to_allows_install_path() {
551        assert!(ServerState::Undetected.can_transition_to(ServerState::Missing));
552        assert!(ServerState::Missing.can_transition_to(ServerState::Installing));
553        assert!(ServerState::Installing.can_transition_to(ServerState::Available));
554        assert!(ServerState::Installing.can_transition_to(ServerState::Failed));
555    }
556
557    #[test]
558    fn can_transition_to_allows_failure_paths() {
559        assert!(ServerState::Starting.can_transition_to(ServerState::Failed));
560        assert!(ServerState::Running.can_transition_to(ServerState::Failed));
561        assert!(ServerState::Available.can_transition_to(ServerState::Failed));
562    }
563
564    #[test]
565    fn can_transition_to_allows_retry_from_failed() {
566        assert!(ServerState::Failed.can_transition_to(ServerState::Available));
567        assert!(ServerState::Failed.can_transition_to(ServerState::Installing));
568        assert!(ServerState::Failed.can_transition_to(ServerState::Starting));
569    }
570
571    #[test]
572    fn can_transition_to_rejects_invalid_jumps() {
573        assert!(!ServerState::Undetected.can_transition_to(ServerState::Running));
574        assert!(!ServerState::Undetected.can_transition_to(ServerState::Starting));
575        assert!(!ServerState::Missing.can_transition_to(ServerState::Running));
576        assert!(!ServerState::Available.can_transition_to(ServerState::Running));
577        assert!(!ServerState::Installing.can_transition_to(ServerState::Running));
578        assert!(!ServerState::Installing.can_transition_to(ServerState::Starting));
579        assert!(!ServerState::Running.can_transition_to(ServerState::Undetected));
580        assert!(!ServerState::Running.can_transition_to(ServerState::Available));
581        assert!(!ServerState::Running.can_transition_to(ServerState::Starting));
582        assert!(!ServerState::Stopped.can_transition_to(ServerState::Running));
583        assert!(!ServerState::Stopped.can_transition_to(ServerState::Failed));
584    }
585
586    #[test]
587    fn can_transition_to_rejects_self_transitions() {
588        for state in [
589            ServerState::Undetected,
590            ServerState::Missing,
591            ServerState::Available,
592            ServerState::Installing,
593            ServerState::Starting,
594            ServerState::Running,
595            ServerState::Stopped,
596            ServerState::Failed,
597        ] {
598            assert!(
599                !state.can_transition_to(state),
600                "{:?} → {:?} should not be allowed",
601                state,
602                state
603            );
604        }
605    }
606
607    #[test]
608    fn lsp_event_state_changed_fields() {
609        let event = LspEvent::StateChanged {
610            language: Language::Rust,
611            old: ServerState::Starting,
612            new: ServerState::Running,
613        };
614        match event {
615            LspEvent::StateChanged { language, old, new } => {
616                assert_eq!(language, Language::Rust);
617                assert_eq!(old, ServerState::Starting);
618                assert_eq!(new, ServerState::Running);
619            }
620            _ => panic!("wrong variant"),
621        }
622    }
623
624    #[test]
625    fn from_path_extension_detection_0009() {
626        use std::path::Path;
627        assert_eq!(
628            Language::from_path(Path::new("config.json")),
629            Some(Language::Json)
630        );
631        assert_eq!(
632            Language::from_path(Path::new("data.yaml")),
633            Some(Language::Yaml)
634        );
635        assert_eq!(
636            Language::from_path(Path::new("data.yml")),
637            Some(Language::Yaml)
638        );
639        assert_eq!(
640            Language::from_path(Path::new("config.toml")),
641            Some(Language::Toml)
642        );
643        assert_eq!(
644            Language::from_path(Path::new("schema.xml")),
645            Some(Language::Xml)
646        );
647        assert_eq!(
648            Language::from_path(Path::new("image.svg")),
649            Some(Language::Xml)
650        );
651        assert_eq!(
652            Language::from_path(Path::new("schema.xsd")),
653            Some(Language::Xml)
654        );
655        assert_eq!(Language::from_path(Path::new("unknown.foo")), None);
656    }
657
658    #[test]
659    fn from_path_dockerfile_stem_detection() {
660        use std::path::Path;
661        assert_eq!(
662            Language::from_path(Path::new("/project/Dockerfile")),
663            Some(Language::Dockerfile)
664        );
665        assert_eq!(
666            Language::from_path(Path::new("/project/dockerfile")),
667            Some(Language::Dockerfile)
668        );
669        assert_eq!(
670            Language::from_path(Path::new("/project/app.dockerfile")),
671            Some(Language::Dockerfile)
672        );
673        assert_eq!(
674            Language::from_path(Path::new("/project/main.go")),
675            Some(Language::Go)
676        );
677    }
678
679    #[test]
680    fn from_path_dockerfile_prefix_variants() {
681        use std::path::Path;
682        // Any filename starting with "Dockerfile" (case-insensitive) is Dockerfile.
683        for name in [
684            "Dockerfile.dev",
685            "Dockerfile.prod",
686            "Dockerfile-test",
687            "Dockerfile_ci",
688            "DOCKERFILE",
689            "dockerfile-utils",
690        ] {
691            assert_eq!(
692                Language::from_path(Path::new(name)),
693                Some(Language::Dockerfile),
694                "{name} should resolve to Dockerfile"
695            );
696        }
697        // Compose files keep their YAML mapping.
698        assert_eq!(
699            Language::from_path(Path::new("docker-compose.yml")),
700            Some(Language::Yaml)
701        );
702        // Names that don't start with "dockerfile" still fall through.
703        assert_eq!(Language::from_path(Path::new("README")), None);
704    }
705
706    #[test]
707    fn capabilities_new_variants_0009() {
708        for lang in [
709            Language::Json,
710            Language::Yaml,
711            Language::Toml,
712            Language::Dockerfile,
713            Language::Xml,
714        ] {
715            let caps = lang.capabilities();
716            assert!(
717                caps.has_tree_sitter,
718                "{} should have has_tree_sitter: true",
719                lang.name()
720            );
721            assert!(!caps.has_lsp, "{} should have has_lsp: false", lang.name());
722        }
723    }
724}