1use 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
31macro_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 ($struct_name:ident, $mode_name:literal, options = { $($opt:tt)* }) => {
53 lang_mode!(@impl $struct_name, $mode_name, None, { $($opt)* });
54 };
55 (
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 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 lattice_config::Shiftwidth = 2,
124 }
125);
126lang_mode!(
127 GoMode,
128 "go-mode",
129 options = {
130 lattice_config::ExpandTab = false,
135 }
136);
137lang_mode!(
138 HtmlMode,
139 "html-mode",
140 options = {
141 lattice_config::Shiftwidth = 2,
146 }
147);
148lang_mode!(JavaMode, "java-mode");
149lang_mode!(
150 JsonMode,
151 "json-mode",
152 options = {
153 lattice_config::Shiftwidth = 2,
158 }
159);
160lang_mode!(LuaMode, "lua-mode");
161lang_mode!(
162 RubyMode,
163 "ruby-mode",
164 options = {
165 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 lattice_config::Shiftwidth = 2,
184 }
185);
186lang_mode!(
187 TsxMode,
188 "tsx-mode",
189 options = {
190 lattice_config::Shiftwidth = 2,
195 }
196);
197lang_mode!(
198 YamlMode,
199 "yaml-mode",
200 options = {
201 lattice_config::ExpandTab = true,
207 lattice_config::Shiftwidth = 2,
208 }
209);
210lang_mode!(
216 MarkdownMode,
217 "markdown-mode",
218 target_kind = BufferKind::Help,
219 options = {
220 lattice_config::AutoWrapOption = lattice_core::AutoWrap::All,
227 }
228);
229
230pub fn major_mode_id_for_lang(lang: Lang) -> Option<ModeId> {
235 match lang {
236 Lang::Plain => None,
237 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
264pub 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
293pub 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
366pub const TREE_SITTER_COMPLETION_SOURCE_ID: &str = lattice_completion::TREE_SITTER_SYMBOL_SOURCE_ID;
375
376#[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
398pub 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 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 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 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 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 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 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 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 #[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 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}