Skip to main content

lattice_syntax/
modes.rs

1//! Per-language major modes.
2//!
3//! Each variant of [`crate::lang::Lang`] (other than `Plain`)
4//! has a corresponding major mode declared here. The modes are
5//! pure declarations in this slice (M.3.0) -- their option
6//! contributions, keymap layers, and lifecycle hooks are
7//! empty / no-op. Real declarative content (indent rules,
8//! tree-sitter parser attach, default LSP attach, comment
9//! syntax) lands as the corresponding subsystems migrate to
10//! the mode model in later slices.
11//!
12//! `Plain` maps to `lattice_mode::TextMode`; no separate
13//! plain-mode declaration here.
14//!
15//! All language modes register through
16//! [`register_language_modes`].
17
18use std::sync::Arc;
19
20use lattice_completion::{
21    CandidateKind, CompletionSourceContribution, CompletionSourceKind, InsertContext, RawCandidate,
22    SourceId, SyncCompletionSource,
23};
24use lattice_core::BufferKind;
25use lattice_mode::{
26    CapabilitySet, LifecycleFuture, Mode, ModeContext, ModeId, ModeKind, ModeRegistry,
27};
28
29use crate::lang::Lang;
30
31/// Macro-internal helper: declare a unit struct + its `Mode`
32/// impl with the canonical name. Reduces boilerplate while
33/// keeping each mode's source plain Rust (no proc-macro
34/// indirection for now).
35///
36/// Second arm (with `target_kind = $kind`) overrides
37/// [`Mode::target_buffer_kind`] for language majors that are also
38/// the default major for a non-`Document` [`BufferKind`] — today
39/// only `markdown-mode`, which serves both `Document + Markdown`
40/// (via `Lang` detection) and `Help` (via H.2's kind dispatch).
41macro_rules! lang_mode {
42    ($struct_name:ident, $mode_name:literal) => {
43        lang_mode!(@impl $struct_name, $mode_name, None, {});
44    };
45    ($struct_name:ident, $mode_name:literal, target_kind = $kind:expr) => {
46        lang_mode!(@impl $struct_name, $mode_name, Some($kind), {});
47    };
48    // IN.11: a language whose tooling assumes different indent
49    // conventions from the global defaults states them here. No new
50    // mechanism -- `Mode::options()` is layer 4 of the buffer-local
51    // resolution stack and had simply never been used for indentation.
52    ($struct_name:ident, $mode_name:literal, options = { $($opt:tt)* }) => {
53        lang_mode!(@impl $struct_name, $mode_name, None, { $($opt)* });
54    };
55    // RF.4: the fourth combination. `markdown-mode` is the first mode to
56    // need BOTH a target kind and option overrides, and the macro had
57    // three arms covering every other pairing.
58    (
59        $struct_name:ident,
60        $mode_name:literal,
61        target_kind = $kind:expr,
62        options = { $($opt:tt)* }
63    ) => {
64        lang_mode!(@impl $struct_name, $mode_name, Some($kind), { $($opt)* });
65    };
66    (@impl $struct_name:ident, $mode_name:literal, $target_kind:expr, { $($opt:tt)* }) => {
67        pub struct $struct_name;
68
69        impl $struct_name {
70            pub fn mode_id() -> ModeId {
71                ModeId::new($mode_name)
72            }
73        }
74
75        impl Mode for $struct_name {
76            type Guard = ();
77            fn id(&self) -> ModeId {
78                Self::mode_id()
79            }
80            fn kind(&self) -> ModeKind {
81                ModeKind::Major
82            }
83            fn target_buffer_kind(&self) -> Option<BufferKind> {
84                $target_kind
85            }
86            fn options(&self) -> lattice_config::OptionOverrideSet {
87                lattice_config::overrides! { $($opt)* }
88            }
89            fn required_capabilities(&self) -> CapabilitySet {
90                CapabilitySet::empty()
91            }
92            fn on_activate(&self, _ctx: ModeContext) -> LifecycleFuture<'_, ()> {
93                Box::pin(async { Ok(()) })
94            }
95        }
96    };
97}
98
99lang_mode!(RustMode, "rust-mode");
100lang_mode!(PythonMode, "python-mode");
101lang_mode!(
102    JavascriptMode,
103    "javascript-mode",
104    options = {
105        // prettier is this language's bundled formatter (spec.rs) and
106        // its default is two spaces. Matching it is the same argument
107        // as Go's tabs: typing at one width and formatting at another
108        // means the tool rewrites the file on every save.
109        lattice_config::Shiftwidth = 2,
110    }
111);
112lang_mode!(BashMode, "bash-mode");
113lang_mode!(CMode, "c-mode");
114lang_mode!(CppMode, "cpp-mode");
115lang_mode!(
116    CssMode,
117    "css-mode",
118    options = {
119        // prettier is this language's bundled formatter (spec.rs) and
120        // its default is two spaces. Matching it is the same argument
121        // as Go's tabs: typing at one width and formatting at another
122        // means the tool rewrites the file on every save.
123        lattice_config::Shiftwidth = 2,
124    }
125);
126lang_mode!(
127    GoMode,
128    "go-mode",
129    options = {
130        // gofmt indents with TABS, unconditionally and without a
131        // configuration knob. Typing spaces here means every `:format`
132        // or format-on-save silently converts them, so the editor
133        // would be fighting the language's own tool on every write.
134        lattice_config::ExpandTab = false,
135    }
136);
137lang_mode!(
138    HtmlMode,
139    "html-mode",
140    options = {
141        // prettier is this language's bundled formatter (spec.rs) and
142        // its default is two spaces. Matching it is the same argument
143        // as Go's tabs: typing at one width and formatting at another
144        // means the tool rewrites the file on every save.
145        lattice_config::Shiftwidth = 2,
146    }
147);
148lang_mode!(JavaMode, "java-mode");
149lang_mode!(
150    JsonMode,
151    "json-mode",
152    options = {
153        // prettier is this language's bundled formatter (spec.rs) and
154        // its default is two spaces. Matching it is the same argument
155        // as Go's tabs: typing at one width and formatting at another
156        // means the tool rewrites the file on every save.
157        lattice_config::Shiftwidth = 2,
158    }
159);
160lang_mode!(LuaMode, "lua-mode");
161lang_mode!(
162    RubyMode,
163    "ruby-mode",
164    options = {
165        // Two spaces is the rubocop default and close to unanimous
166        // across the ecosystem. Ruby ships no bundled formatter here
167        // (no consensus between rubocop and standardrb), so nothing
168        // downstream would correct a 4-space habit.
169        lattice_config::Shiftwidth = 2,
170    }
171);
172lang_mode!(SqlMode, "sql-mode");
173lang_mode!(TomlMode, "toml-mode");
174lang_mode!(WitMode, "wit-mode");
175lang_mode!(
176    TypeScriptMode,
177    "typescript-mode",
178    options = {
179        // prettier is this language's bundled formatter (spec.rs) and
180        // its default is two spaces. Matching it is the same argument
181        // as Go's tabs: typing at one width and formatting at another
182        // means the tool rewrites the file on every save.
183        lattice_config::Shiftwidth = 2,
184    }
185);
186lang_mode!(
187    TsxMode,
188    "tsx-mode",
189    options = {
190        // prettier is this language's bundled formatter (spec.rs) and
191        // its default is two spaces. Matching it is the same argument
192        // as Go's tabs: typing at one width and formatting at another
193        // means the tool rewrites the file on every save.
194        lattice_config::Shiftwidth = 2,
195    }
196);
197lang_mode!(
198    YamlMode,
199    "yaml-mode",
200    options = {
201        // A CORRECTNESS override, not a style one: the YAML spec
202        // forbids tab characters in indentation outright, so
203        // `noexpandtab` in a YAML buffer produces a file that will not
204        // parse. Two spaces is also what essentially every YAML
205        // document in the wild uses.
206        lattice_config::ExpandTab = true,
207        lattice_config::Shiftwidth = 2,
208    }
209);
210// H.2 (2026-05-31): `markdown-mode` is the default major for two
211// dispatch paths — `Document + Lang::Markdown` (via language
212// detection) and `BufferKind::Help` (via the registry's kind
213// index). Both paths land on the same mode; the kind binding
214// here drives the latter.
215lang_mode!(
216    MarkdownMode,
217    "markdown-mode",
218    target_kind = BufferKind::Help,
219    options = {
220        // RF.4: markdown IS prose, so wrapping while typing applies to
221        // every line rather than only to comments (there are none). The
222        // global default is the conservative `comments` rung because a
223        // code line broken mid-expression is destructive in a way a
224        // broken sentence is not; a prose major has no such hazard and
225        // opts up.
226        lattice_config::AutoWrapOption = lattice_core::AutoWrap::All,
227    }
228);
229
230/// Resolve a [`Lang`] to its corresponding major-mode id.
231/// `Lang::Plain` returns `None` because `text-mode` (the
232/// fallback) is owned by `lattice-mode`; the caller falls
233/// through to that when the lookup misses.
234pub fn major_mode_id_for_lang(lang: Lang) -> Option<ModeId> {
235    match lang {
236        Lang::Plain => None,
237        // A plugin language's major mode is contributed through the
238        // `modes` seam by the plugin that owns it — the mode owns its
239        // full surface, so the host does not synthesise one here.
240        Lang::Plugin(_) => None,
241        Lang::Rust => Some(RustMode::mode_id()),
242        Lang::Python => Some(PythonMode::mode_id()),
243        Lang::JavaScript => Some(JavascriptMode::mode_id()),
244        Lang::Bash => Some(BashMode::mode_id()),
245        Lang::C => Some(CMode::mode_id()),
246        Lang::Cpp => Some(CppMode::mode_id()),
247        Lang::Css => Some(CssMode::mode_id()),
248        Lang::Go => Some(GoMode::mode_id()),
249        Lang::Html => Some(HtmlMode::mode_id()),
250        Lang::Java => Some(JavaMode::mode_id()),
251        Lang::Json => Some(JsonMode::mode_id()),
252        Lang::Lua => Some(LuaMode::mode_id()),
253        Lang::Ruby => Some(RubyMode::mode_id()),
254        Lang::Sql => Some(SqlMode::mode_id()),
255        Lang::Toml => Some(TomlMode::mode_id()),
256        Lang::Wit => Some(WitMode::mode_id()),
257        Lang::TypeScript => Some(TypeScriptMode::mode_id()),
258        Lang::Tsx => Some(TsxMode::mode_id()),
259        Lang::Yaml => Some(YamlMode::mode_id()),
260        Lang::Markdown => Some(MarkdownMode::mode_id()),
261    }
262}
263
264/// Resolve a major-mode id back to its corresponding [`Lang`].
265/// Returns `None` when the mode has no language binding (e.g.
266/// `TextMode`, minor modes).
267pub fn lang_for_mode_id(id: ModeId) -> Option<Lang> {
268    match id.as_str() {
269        "rust-mode" => Some(Lang::Rust),
270        "python-mode" => Some(Lang::Python),
271        "javascript-mode" => Some(Lang::JavaScript),
272        "bash-mode" => Some(Lang::Bash),
273        "c-mode" => Some(Lang::C),
274        "cpp-mode" => Some(Lang::Cpp),
275        "css-mode" => Some(Lang::Css),
276        "go-mode" => Some(Lang::Go),
277        "html-mode" => Some(Lang::Html),
278        "java-mode" => Some(Lang::Java),
279        "json-mode" => Some(Lang::Json),
280        "lua-mode" => Some(Lang::Lua),
281        "ruby-mode" => Some(Lang::Ruby),
282        "sql-mode" => Some(Lang::Sql),
283        "toml-mode" => Some(Lang::Toml),
284        "wit-mode" => Some(Lang::Wit),
285        "typescript-mode" => Some(Lang::TypeScript),
286        "tsx-mode" => Some(Lang::Tsx),
287        "yaml-mode" => Some(Lang::Yaml),
288        "markdown-mode" => Some(Lang::Markdown),
289        _ => None,
290    }
291}
292
293/// Register every language major mode against `registry`.
294/// Called from the App's mode-registry boot path. Idempotent
295/// only by duplication (registry's existing invariant).
296///
297/// Also registers [`TreeSitterCompletionMode`] (CSM.6) -- the
298/// syntax-feature minor that contributes
299/// `gen:tree-sitter-symbol` candidates to the completion popup.
300pub fn register_language_modes(registry: &mut ModeRegistry) {
301    registry
302        .register(RustMode)
303        .expect("rust-mode register without conflict");
304    registry
305        .register(PythonMode)
306        .expect("python-mode register without conflict");
307    registry
308        .register(JavascriptMode)
309        .expect("javascript-mode register without conflict");
310    registry
311        .register(BashMode)
312        .expect("bash-mode register without conflict");
313    registry
314        .register(CMode)
315        .expect("c-mode register without conflict");
316    registry
317        .register(CppMode)
318        .expect("cpp-mode register without conflict");
319    registry
320        .register(CssMode)
321        .expect("css-mode register without conflict");
322    registry
323        .register(GoMode)
324        .expect("go-mode register without conflict");
325    registry
326        .register(HtmlMode)
327        .expect("html-mode register without conflict");
328    registry
329        .register(JavaMode)
330        .expect("java-mode register without conflict");
331    registry
332        .register(JsonMode)
333        .expect("json-mode register without conflict");
334    registry
335        .register(LuaMode)
336        .expect("lua-mode register without conflict");
337    registry
338        .register(RubyMode)
339        .expect("ruby-mode register without conflict");
340    registry
341        .register(SqlMode)
342        .expect("sql-mode register without conflict");
343    registry
344        .register(TomlMode)
345        .expect("toml-mode register without conflict");
346    registry
347        .register(WitMode)
348        .expect("wit-mode register without conflict");
349    registry
350        .register(TypeScriptMode)
351        .expect("typescript-mode register without conflict");
352    registry
353        .register(TsxMode)
354        .expect("tsx-mode register without conflict");
355    registry
356        .register(YamlMode)
357        .expect("yaml-mode register without conflict");
358    registry
359        .register(MarkdownMode)
360        .expect("markdown-mode register without conflict");
361    registry
362        .register(TreeSitterCompletionMode)
363        .expect("tree-sitter-completion-mode register without conflict");
364}
365
366// ---------------------------------------------------------
367// CSM.6: tree-sitter completion source + mode.
368// ---------------------------------------------------------
369
370/// Stable id for the tree-sitter symbol completion source.
371/// Must match `lattice_completion::TREE_SITTER_SYMBOL_SOURCE_ID`
372/// -- the host's per-language allowlist and `:set
373/// completion.source.<id>.priority` key off this string.
374pub const TREE_SITTER_COMPLETION_SOURCE_ID: &str = lattice_completion::TREE_SITTER_SYMBOL_SOURCE_ID;
375
376/// The `SyncCompletionSource` impl that emits tree-sitter
377/// local-symbol candidates. Stateless -- reads the pre-computed
378/// symbol slice off `InsertContext::tree_sitter_symbols` (the
379/// host walks `collect_symbols()` once per populate). Filters
380/// out the cursor's own current query so the user doesn't get
381/// "complete this word with itself."
382#[derive(Debug, Clone, Default)]
383pub struct TreeSitterSymbolSource;
384
385impl SyncCompletionSource for TreeSitterSymbolSource {
386    fn produce(&self, ctx: &InsertContext<'_>) -> Vec<RawCandidate> {
387        ctx.tree_sitter_symbols
388            .iter()
389            .filter(|sym| sym.as_str() != ctx.query)
390            .map(|sym| {
391                RawCandidate::plain(sym.clone(), CandidateKind::Plain)
392                    .with_source(SourceId::new(TREE_SITTER_COMPLETION_SOURCE_ID))
393            })
394            .collect()
395    }
396}
397
398/// `tree-sitter-completion-mode` (CSM.6). Contributes the
399/// tree-sitter symbol source while active. Auto-activates on
400/// Document buffers via `auto_activated_minors_for_buffer_kind`
401/// in `lattice-ui-tui::modes`. `popup_filter_chord = Some('t')`
402/// ⇒ `<C-t>` inside `completion-popup-mode` narrows the popup
403/// to tree-sitter symbols only.
404pub struct TreeSitterCompletionMode;
405
406impl TreeSitterCompletionMode {
407    pub fn mode_id() -> ModeId {
408        ModeId::new("tree-sitter-completion-mode")
409    }
410}
411
412impl Mode for TreeSitterCompletionMode {
413    type Guard = ();
414    fn id(&self) -> ModeId {
415        Self::mode_id()
416    }
417    fn kind(&self) -> ModeKind {
418        ModeKind::Minor
419    }
420    fn required_capabilities(&self) -> CapabilitySet {
421        CapabilitySet::empty()
422    }
423    fn completion_sources(&self) -> Vec<CompletionSourceContribution> {
424        vec![CompletionSourceContribution {
425            accepts_non_word_query: false,
426            id: SourceId::new(TREE_SITTER_COMPLETION_SOURCE_ID),
427            // 80 per insert-completion.md §3.4 -- buffer-words
428            // (100) wins on ties because it's a superset of
429            // tree-sitter symbols.
430            default_priority: 80,
431            auto_trigger: true,
432            trigger_chars: Vec::new(),
433            popup_filter_chord: Some('t'),
434            kind: CompletionSourceKind::Sync(Arc::new(TreeSitterSymbolSource)),
435        }]
436    }
437    fn on_activate(&self, _ctx: ModeContext) -> LifecycleFuture<'_, ()> {
438        Box::pin(async { Ok(()) })
439    }
440}
441
442#[cfg(test)]
443mod tests {
444    #![allow(clippy::unwrap_used, clippy::panic)]
445    use super::*;
446
447    // ---- IN.11: per-language indent conventions ----
448
449    /// Read one option out of a mode's contribution.
450    ///
451    /// The set is `TypeId`-keyed with a type-erased value (the
452    /// resolver downcasts at resolution time), so a test reading it
453    /// back does the same.
454    fn override_of<D>(set: &lattice_config::OptionOverrideSet) -> Option<D::Value>
455    where
456        D: lattice_config::OptionDecl,
457        D::Value: Clone + Send + Sync + 'static,
458    {
459        set.iter()
460            .find(|o| o.option_type_id == std::any::TypeId::of::<D>())
461            .and_then(|o| o.value.downcast_ref::<D::Value>())
462            .cloned()
463    }
464
465    #[test]
466    fn go_indents_with_tabs_because_gofmt_does() {
467        // Not a style preference: gofmt emits tabs unconditionally and
468        // has no knob, so spaces here would be rewritten on every
469        // `:format` or save.
470        assert_eq!(
471            override_of::<lattice_config::ExpandTab>(&GoMode.options()),
472            Some(false)
473        );
474    }
475
476    #[test]
477    fn yaml_forbids_tabs_as_a_correctness_matter() {
478        // The YAML spec disallows tab characters in indentation, so
479        // `noexpandtab` in a YAML buffer produces a file that will not
480        // parse. This override is not about taste.
481        assert_eq!(
482            override_of::<lattice_config::ExpandTab>(&YamlMode.options()),
483            Some(true)
484        );
485        assert_eq!(
486            override_of::<lattice_config::Shiftwidth>(&YamlMode.options()),
487            Some(2)
488        );
489    }
490
491    #[test]
492    fn the_prettier_family_matches_its_formatter() {
493        // Same argument as Go: prettier is the bundled formatter for
494        // these languages and defaults to two spaces, so typing at
495        // four means the tool rewrites the file on every save.
496        for set in [
497            JavascriptMode.options(),
498            TypeScriptMode.options(),
499            TsxMode.options(),
500            CssMode.options(),
501            HtmlMode.options(),
502            JsonMode.options(),
503        ] {
504            assert_eq!(override_of::<lattice_config::Shiftwidth>(&set), Some(2));
505        }
506    }
507
508    #[test]
509    fn languages_whose_convention_matches_the_default_contribute_nothing() {
510        // Rust (rustfmt) and Python (black, PEP 8) both indent four
511        // spaces, which IS the global default. Contributing a
512        // redundant override would be noise that later reads as
513        // intent.
514        assert!(override_of::<lattice_config::Shiftwidth>(&RustMode.options()).is_none());
515        assert!(override_of::<lattice_config::ExpandTab>(&RustMode.options()).is_none());
516        assert!(override_of::<lattice_config::Shiftwidth>(&PythonMode.options()).is_none());
517    }
518
519    #[test]
520    fn contested_conventions_are_left_to_the_user() {
521        // C / C++ / Java / Lua / Bash / TOML have no dominant
522        // convention (clang-format's LLVM default is 2 while most C
523        // projects use 4; stylua and shfmt both default to tabs).
524        // Picking a side would be imposing taste rather than matching
525        // a tool the editor actually runs.
526        for set in [
527            CMode.options(),
528            CppMode.options(),
529            JavaMode.options(),
530            LuaMode.options(),
531            BashMode.options(),
532            TomlMode.options(),
533        ] {
534            assert!(override_of::<lattice_config::Shiftwidth>(&set).is_none());
535            assert!(override_of::<lattice_config::ExpandTab>(&set).is_none());
536        }
537    }
538
539    fn all_lang_mode_ids() -> Vec<ModeId> {
540        vec![
541            RustMode::mode_id(),
542            PythonMode::mode_id(),
543            JavascriptMode::mode_id(),
544            BashMode::mode_id(),
545            CMode::mode_id(),
546            CppMode::mode_id(),
547            CssMode::mode_id(),
548            GoMode::mode_id(),
549            HtmlMode::mode_id(),
550            JavaMode::mode_id(),
551            JsonMode::mode_id(),
552            LuaMode::mode_id(),
553            RubyMode::mode_id(),
554            SqlMode::mode_id(),
555            TomlMode::mode_id(),
556            WitMode::mode_id(),
557            TypeScriptMode::mode_id(),
558            TsxMode::mode_id(),
559            YamlMode::mode_id(),
560            MarkdownMode::mode_id(),
561        ]
562    }
563
564    #[test]
565    fn each_lang_mode_has_distinct_id() {
566        let ids = all_lang_mode_ids();
567        for (i, a) in ids.iter().enumerate() {
568            for b in &ids[i + 1..] {
569                assert_ne!(a, b);
570            }
571        }
572    }
573
574    #[test]
575    fn major_mode_id_for_lang_round_trips() {
576        let cases: Vec<(Lang, Option<ModeId>)> = vec![
577            (Lang::Plain, None),
578            (Lang::Rust, Some(RustMode::mode_id())),
579            (Lang::Python, Some(PythonMode::mode_id())),
580            (Lang::JavaScript, Some(JavascriptMode::mode_id())),
581            (Lang::Bash, Some(BashMode::mode_id())),
582            (Lang::C, Some(CMode::mode_id())),
583            (Lang::Cpp, Some(CppMode::mode_id())),
584            (Lang::Css, Some(CssMode::mode_id())),
585            (Lang::Go, Some(GoMode::mode_id())),
586            (Lang::Html, Some(HtmlMode::mode_id())),
587            (Lang::Java, Some(JavaMode::mode_id())),
588            (Lang::Json, Some(JsonMode::mode_id())),
589            (Lang::Lua, Some(LuaMode::mode_id())),
590            (Lang::Ruby, Some(RubyMode::mode_id())),
591            (Lang::Sql, Some(SqlMode::mode_id())),
592            (Lang::Toml, Some(TomlMode::mode_id())),
593            (Lang::Wit, Some(WitMode::mode_id())),
594            (Lang::TypeScript, Some(TypeScriptMode::mode_id())),
595            (Lang::Tsx, Some(TsxMode::mode_id())),
596            (Lang::Yaml, Some(YamlMode::mode_id())),
597            (Lang::Markdown, Some(MarkdownMode::mode_id())),
598        ];
599        for (lang, expected) in cases {
600            assert_eq!(major_mode_id_for_lang(lang), expected, "{lang:?}");
601        }
602    }
603
604    #[test]
605    fn register_language_modes_populates_registry() {
606        let mut registry = ModeRegistry::new();
607        register_language_modes(&mut registry);
608        for id in all_lang_mode_ids() {
609            assert!(registry.is_registered(id), "mode not registered: {id:?}");
610        }
611    }
612
613    /// RF.4: prose majors opt UP to `autowrap=all`.
614    ///
615    /// The global default is `comments`, the conservative rung — a code
616    /// line broken mid-expression is destructive in a way a broken
617    /// sentence is not. Markdown has no comments to speak of and is
618    /// entirely prose, so `comments` there would mean "never wrap",
619    /// which is the wrong answer for the one file type most likely to
620    /// want it.
621    #[test]
622    fn markdown_wraps_prose_while_code_majors_wrap_only_comments() {
623        use lattice_mode::Mode;
624        let opts = MarkdownMode.options();
625        let found = opts
626            .iter()
627            .any(|o| o.option_type_id == std::any::TypeId::of::<lattice_config::AutoWrapOption>());
628        assert!(found, "markdown-mode must override autowrap");
629
630        // A code major must NOT — it inherits the conservative default,
631        // and an override here would be the bug this test exists for.
632        assert!(
633            !RustMode
634                .options()
635                .iter()
636                .any(|o| o.option_type_id
637                    == std::any::TypeId::of::<lattice_config::AutoWrapOption>()),
638            "rust-mode must inherit `autowrap=comments`, not override it"
639        );
640    }
641
642    #[test]
643    fn lang_for_mode_id_round_trips() {
644        for (lang, mode_id) in [
645            (Lang::Rust, RustMode::mode_id()),
646            (Lang::Python, PythonMode::mode_id()),
647            (Lang::JavaScript, JavascriptMode::mode_id()),
648            (Lang::Bash, BashMode::mode_id()),
649            (Lang::C, CMode::mode_id()),
650            (Lang::Cpp, CppMode::mode_id()),
651            (Lang::Css, CssMode::mode_id()),
652            (Lang::Go, GoMode::mode_id()),
653            (Lang::Html, HtmlMode::mode_id()),
654            (Lang::Java, JavaMode::mode_id()),
655            (Lang::Json, JsonMode::mode_id()),
656            (Lang::Lua, LuaMode::mode_id()),
657            (Lang::Ruby, RubyMode::mode_id()),
658            (Lang::Sql, SqlMode::mode_id()),
659            (Lang::Toml, TomlMode::mode_id()),
660            (Lang::Wit, WitMode::mode_id()),
661            (Lang::TypeScript, TypeScriptMode::mode_id()),
662            (Lang::Tsx, TsxMode::mode_id()),
663            (Lang::Yaml, YamlMode::mode_id()),
664            (Lang::Markdown, MarkdownMode::mode_id()),
665        ] {
666            assert_eq!(
667                lang_for_mode_id(mode_id),
668                Some(lang),
669                "{}",
670                mode_id.as_str()
671            );
672        }
673        assert_eq!(lang_for_mode_id(ModeId::new("text-mode")), None);
674    }
675
676    #[test]
677    fn each_lang_mode_is_major() {
678        for id in all_lang_mode_ids() {
679            let mode: Box<dyn Mode<Guard = ()>> = match id.as_str() {
680                "rust-mode" => Box::new(RustMode),
681                "python-mode" => Box::new(PythonMode),
682                "javascript-mode" => Box::new(JavascriptMode),
683                "bash-mode" => Box::new(BashMode),
684                "c-mode" => Box::new(CMode),
685                "cpp-mode" => Box::new(CppMode),
686                "css-mode" => Box::new(CssMode),
687                "go-mode" => Box::new(GoMode),
688                "html-mode" => Box::new(HtmlMode),
689                "java-mode" => Box::new(JavaMode),
690                "json-mode" => Box::new(JsonMode),
691                "lua-mode" => Box::new(LuaMode),
692                "ruby-mode" => Box::new(RubyMode),
693                "sql-mode" => Box::new(SqlMode),
694                "toml-mode" => Box::new(TomlMode),
695                "wit-mode" => Box::new(WitMode),
696                "typescript-mode" => Box::new(TypeScriptMode),
697                "tsx-mode" => Box::new(TsxMode),
698                "yaml-mode" => Box::new(YamlMode),
699                "markdown-mode" => Box::new(MarkdownMode),
700                other => panic!("unexpected mode id: {other}"),
701            };
702            assert_eq!(mode.kind(), ModeKind::Major, "{} is not Major", id.as_str());
703        }
704    }
705}