1use lattice_core::FoldMethod;
23
24use super::App;
25
26impl App {
27 pub fn show_line_numbers(&self) -> bool {
40 self.ad().option_cache.show_line_numbers
41 }
42
43 pub fn show_line_numbers_for(&self, buffer: crate::buffers::BufferId) -> bool {
48 *self.resolved_option::<lattice_config::Number>(buffer)
49 }
50
51 pub fn relative_line_numbers(&self) -> bool {
57 self.ad().option_cache.relative_line_numbers
58 }
59
60 pub fn relative_line_numbers_for(&self, buffer: crate::buffers::BufferId) -> bool {
63 *self.resolved_option::<lattice_config::RelativeNumber>(buffer)
64 }
65
66 pub fn wrap_lines(&self) -> bool {
69 self.ad().option_cache.wrap_lines
70 }
71
72 pub fn wrap_lines_for(&self, buffer: crate::buffers::BufferId) -> bool {
78 *self.resolved_option::<lattice_config::Wrap>(buffer)
79 }
80
81 pub fn sign_column_for(&self, buffer: crate::buffers::BufferId) -> bool {
88 self.resolved_option::<lattice_config::SignColumnOption>(buffer)
89 .reserved()
90 }
91
92 pub fn ignorecase(&self) -> bool {
94 self.ad().option_cache.ignorecase
95 }
96
97 pub fn tabstop(&self) -> u32 {
102 self.ad().option_cache.tabstop
103 }
104
105 pub fn scrolloff(&self) -> u32 {
108 self.ad().option_cache.scrolloff
109 }
110
111 pub fn foldmethod(&self) -> FoldMethod {
113 self.ad().option_cache.foldmethod
114 }
115
116 pub fn foldenable(&self) -> bool {
121 #[cfg(test)]
127 {
128 self.editor.foldenable()
129 }
130 #[cfg(not(test))]
131 {
132 self.ad().option_cache.foldenable
133 }
134 }
135
136 pub fn completion_auto_insert_single(&self) -> bool {
141 self.read_editor(move |e| e.completion_auto_insert_single())
142 }
143
144 pub(super) fn rebuild_option_cache(&mut self) {
162 self.mutate_editor(|e| e.rebuild_option_cache());
164 }
165
166 pub fn recompute_options_for_buffer(&mut self, buffer: crate::buffers::BufferId) {
171 self.mutate_editor(move |e| e.recompute_options_for_buffer(buffer));
173 }
174
175 pub fn recompute_active_completion_sources_for(&mut self, buffer: crate::buffers::BufferId) {
192 self.mutate_editor(move |e| e.recompute_active_completion_sources_for(buffer));
194 }
195
196 pub fn resolved_option<D>(&self, buffer: crate::buffers::BufferId) -> std::sync::Arc<D::Value>
201 where
202 D: lattice_config::OptionDecl + 'static,
203 D::Value: Clone + Send + Sync + 'static,
204 {
205 self.read_editor(move |e| e.resolved_option::<D>(buffer))
210 }
211
212 #[allow(dead_code)]
224 pub(super) fn do_set(&mut self, option: &str) {
225 let option_owned = option.to_string();
228 let signals = self.mutate_editor_with(move |e| e.do_set(&option_owned));
229 for sig in signals {
230 self.handle_renderer_signal(sig);
231 }
232 }
233
234 pub fn drain_option_changes(&mut self) {
245 let signals = self.mutate_editor_with(|e| e.drain_option_changes());
247 for sig in signals {
248 self.handle_renderer_signal(sig);
249 }
250 }
251
252 pub(super) fn take_pending_structural_section(
276 &mut self,
277 full_path: &str,
278 ) -> Option<toml::Table> {
279 let full_path = full_path.to_string();
282 self.mutate_editor_with(move |e| e.pending_config_structural_sections.remove(&full_path))
283 }
284
285 pub(super) fn pending_structural_section_paths(&self, namespace: &str) -> Vec<String> {
292 let prefix = format!("{namespace}.");
295 self.read_editor(move |e| {
296 e.pending_config_structural_sections
297 .keys()
298 .filter(|k| k.starts_with(&prefix))
299 .cloned()
300 .collect()
301 })
302 }
303
304 pub fn apply_per_language_toml_overrides(&mut self) {
316 self.mutate_editor(|e| e.apply_per_language_toml_overrides());
322 }
323}
324
325#[cfg(test)]
336impl App {
337 pub fn set_foldenable_for_test(&mut self, on: bool) {
338 let _ = self
339 .editor
340 .config
341 .set_typed::<lattice_config::FoldEnable>(on);
342 self.drain_option_changes();
343 }
344
345 pub fn set_completion_auto_insert_single_for_test(&mut self, on: bool) {
346 let _ = self
347 .editor
348 .config
349 .set_typed::<lattice_config::CompletionAutoInsertSingle>(on);
350 self.drain_option_changes();
351 }
352
353 pub fn set_foldmethod_for_test(&mut self, fm: FoldMethod) {
354 self.editor
355 .config
356 .set_typed::<lattice_config::FoldMethodOption>(fm)
357 .expect("set foldmethod");
358 self.drain_option_changes();
359 }
360}
361
362#[cfg(test)]
363mod tests {
364 #![allow(clippy::unwrap_used, clippy::panic)]
365
366 use crate::app::test_helpers::{
367 app_in_command_mode, app_with, submit_ex, subscribe_all_events,
368 };
369 use crate::app::*;
370 use lattice_grammar::ModalState;
371 use lattice_protocol::Event;
372
373 #[test]
383 fn event_bus_publishes_option_changed_on_set_assign() {
384 let mut a = app_with("xx", 10);
385 let mut rx = subscribe_all_events(&a);
386 a.editor.set_command_line_text("set tabstop=8");
387 a.editor.modal = ModalState::Command;
388 a.apply(Action::CommandLineSubmit);
389 let mut found_opt = None;
390 while let Ok(evt) = rx.try_recv() {
391 if let Event::OptionChanged { name, old, new } = evt {
392 found_opt = Some((name, old, new));
393 break;
394 }
395 }
396 let (name, old, new) = found_opt.expect("OptionChanged should fire on :set tabstop=8");
397 assert_eq!(name, "tabstop");
398 assert_eq!(old.as_deref(), Some("4"));
400 assert_eq!(new, "8");
401 }
402
403 #[test]
404 fn event_bus_publishes_option_changed_on_set_negate() {
405 let mut a = app_with("xx", 10);
406 let mut rx = subscribe_all_events(&a);
407 a.editor.set_command_line_text("set nonumber");
408 a.editor.modal = ModalState::Command;
409 a.apply(Action::CommandLineSubmit);
410 let mut found = false;
411 while let Ok(evt) = rx.try_recv() {
412 if let Event::OptionChanged { name, new, .. } = evt
413 && name == "number"
414 && new == "false"
415 {
416 found = true;
417 break;
418 }
419 }
420 assert!(found, ":set nonumber should publish OptionChanged");
421 }
422
423 #[test]
424 fn drain_option_changes_runs_foldmethod_cascade_for_direct_config_writes() {
425 let mut a = app_with("def f():\n pass\n pass\n", 10);
432 a.editor
434 .config
435 .set_typed::<lattice_config::FoldMethodOption>(FoldMethod::Indent)
436 .unwrap();
437 a.drain_option_changes();
440 assert_eq!(a.foldmethod(), FoldMethod::Indent);
441 assert!(
442 !a.editor.folds.is_empty(),
443 "drain_option_changes should run the foldmethod cascade and recompute folds"
444 );
445 }
446
447 #[test]
448 fn drain_option_changes_runs_relativenumber_to_number_cascade() {
449 let mut a = app_with("xx", 10);
450 a.editor
451 .config
452 .set_typed::<lattice_config::Number>(false)
453 .unwrap();
454 a.drain_option_changes();
455 assert!(!a.show_line_numbers());
456 a.editor
457 .config
458 .set_typed::<lattice_config::RelativeNumber>(true)
459 .unwrap();
460 a.drain_option_changes();
461 assert!(a.relative_line_numbers());
462 assert!(
463 a.show_line_numbers(),
464 "relativenumber=true should cascade to number=true via the bus subscription"
465 );
466 }
467
468 #[test]
469 fn rebuild_option_cache_picks_up_whitespace_glyphs() {
470 let a = app_with("xx", 10);
475 let cache = &a.editor.option_cache;
476 assert!(!cache.show_whitespace);
479 assert_eq!(cache.whitespace_tab, Some('→'));
481 assert_eq!(cache.whitespace_trailing, Some('·'));
482 assert_eq!(cache.whitespace_leading, Some('·'));
483 assert_eq!(cache.whitespace_space, None);
485 assert_eq!(cache.whitespace_eol, None);
486 }
487
488 #[test]
489 fn rebuild_option_cache_picks_up_current_line_highlight() {
490 let a = app_with("xx", 10);
493 assert!(!a.editor.option_cache.current_line_highlight);
494 }
495
496 #[test]
497 fn whitespace_glyph_set_via_set_propagates_to_cache() {
498 let mut a = app_with("xx", 10);
502 a.do_set("display.whitespace.tab=⇥");
503 a.drain_option_changes();
504 assert_eq!(a.editor.option_cache.whitespace_tab, Some('⇥'));
505 a.do_set("display.whitespace.tab=");
507 a.drain_option_changes();
508 assert_eq!(a.editor.option_cache.whitespace_tab, None);
509 }
510
511 #[test]
512 fn drain_option_changes_runs_ui_theme_sync_for_direct_writes() {
513 let mut a = app_with("xx", 10);
514 a.editor
515 .config
516 .set_typed::<lattice_host::ui::theme_options::UiDimInactive>(false)
517 .unwrap();
518 a.drain_option_changes();
519 assert!(
520 !a.theme.dim_inactive_panes,
521 "ui.dim_inactive=false should propagate to theme.dim_inactive_panes via the cascade"
522 );
523 }
524
525 #[test]
526 fn ui_set_cascade_keeps_host_theme_and_tui_theme_in_sync() {
527 let mut a = app_with("xx", 10);
535 a.editor
537 .config
538 .set_typed::<lattice_host::ui::theme_options::UiDimInactive>(false)
539 .unwrap();
540 a.drain_option_changes();
541 assert!(!a.theme.dim_inactive_panes, "tui: dim_inactive flipped");
542 a.editor
544 .config
545 .set_typed::<lattice_host::ui::theme_options::UiNerdFonts>(true)
546 .unwrap();
547 a.drain_option_changes();
548 assert!(a.theme.nerd_fonts);
549 a.editor
551 .config
552 .set_typed::<lattice_host::ui::theme_options::UiSeparator>("┃".to_string())
553 .unwrap();
554 a.drain_option_changes();
555 assert_eq!(a.theme.pane_separator_vertical, '┃');
556 a.editor
558 .config
559 .set_typed::<lattice_host::ui::theme_options::UiSeparatorColor>("red".to_string())
560 .unwrap();
561 a.drain_option_changes();
562 assert_eq!(
568 a.theme.pane_separator.fg,
569 Some(ratatui::style::Color::Red),
570 "tui: separator fg=red",
571 );
572 }
573
574 #[test]
575 fn drain_option_changes_handles_chained_cascade_writes() {
576 let mut a = app_with("xx", 10);
577 a.editor
578 .config
579 .set_typed::<lattice_config::Number>(false)
580 .unwrap();
581 a.drain_option_changes();
582 a.editor
583 .config
584 .set_typed::<lattice_config::RelativeNumber>(true)
585 .unwrap();
586 a.drain_option_changes();
587 assert!(a.relative_line_numbers());
588 assert!(a.show_line_numbers());
589 a.drain_option_changes();
590 assert!(a.relative_line_numbers());
591 assert!(a.show_line_numbers());
592 }
593
594 #[test]
595 fn event_bus_does_not_publish_option_changed_on_query() {
596 let mut a = app_with("xx", 10);
597 let mut rx = subscribe_all_events(&a);
598 a.editor.set_command_line_text("set number?");
599 a.editor.modal = ModalState::Command;
600 a.apply(Action::CommandLineSubmit);
601 while let Ok(evt) = rx.try_recv() {
602 assert!(
603 !matches!(evt, Event::OptionChanged { .. }),
604 "query should not publish OptionChanged"
605 );
606 }
607 }
608
609 #[test]
612 fn set_unknown_option_emits_error() {
613 let mut a = app_with("hello", 10);
614 submit_ex(&mut a, "set frobnicate");
615 let msg = a.editor.last_message.as_ref().unwrap();
616 assert_eq!(msg.level, EchoLevel::Error);
617 assert!(msg.text.contains("frobnicate"));
618 }
619
620 #[test]
621 fn set_unknown_option_errors() {
622 let mut a = app_with("xx", 10);
623 a.editor.set_command_line_text("set whatever");
624 a.editor.modal = ModalState::Command;
625 a.apply(Action::CommandLineSubmit);
626 let msg = a.editor.last_message.as_ref().expect("error");
627 assert!(msg.text.contains("Unknown option"), "got: {}", msg.text);
628 }
629
630 #[test]
631 fn set_no_form_clears_boolean() {
632 let mut a = app_with("xx", 10);
633 a.editor.set_command_line_text("set nonumber");
634 a.editor.modal = ModalState::Command;
635 a.apply(Action::CommandLineSubmit);
636 assert!(!a.show_line_numbers());
637 }
638
639 #[test]
640 fn set_no_form_rejects_non_boolean() {
641 let mut a = app_with("xx", 10);
642 a.editor.set_command_line_text("set notabstop");
643 a.editor.modal = ModalState::Command;
644 a.apply(Action::CommandLineSubmit);
645 let msg = a.editor.last_message.as_ref().expect("error");
646 assert!(msg.text.contains("not a boolean"), "got: {}", msg.text);
647 }
648
649 #[test]
650 fn set_int_out_of_range_errors() {
651 let mut a = app_with("xx", 10);
652 a.editor.set_command_line_text("set tabstop=999");
653 a.editor.modal = ModalState::Command;
654 a.apply(Action::CommandLineSubmit);
655 let msg = a.editor.last_message.as_ref().expect("error");
656 assert!(msg.text.contains("out of range"), "got: {}", msg.text);
657 }
658
659 #[test]
662 fn describe_option_renders_help_with_metadata() {
663 let mut a = app_with("xx", 10);
664 a.editor.set_command_line_text("describe-option tabstop");
665 a.editor.modal = ModalState::Command;
666 a.apply(Action::CommandLineSubmit);
667 let h = a.popup_help().expect("describe-option help");
668 let body = h.content.as_string();
669 assert!(body.contains("tabstop"));
670 assert!(body.contains("integer"));
671 assert!(body.contains("default"));
672 }
673
674 #[test]
675 fn list_options_includes_every_registered_option() {
676 let mut a = app_with("xx", 10);
677 a.editor.set_command_line_text("options");
678 a.editor.modal = ModalState::Command;
679 a.apply(Action::CommandLineSubmit);
680 let h = a.popup_help().expect("options help");
681 let body = h.content.as_string();
682 assert!(body.contains("number"));
683 assert!(body.contains("tabstop"));
684 assert!(body.contains("scrolloff"));
685 }
686
687 #[test]
688 fn list_options_groups_by_group_and_includes_docs() {
689 let mut a = app_with("xx", 10);
690 a.editor.set_command_line_text("options");
691 a.editor.modal = ModalState::Command;
692 a.apply(Action::CommandLineSubmit);
693 let h = a.popup_help().expect("options help");
694 let body = h.content.as_string();
695 assert!(body.contains("## editor"), "missing editor section\n{body}");
698 assert!(
699 body.contains("## completion"),
700 "missing completion section\n{body}"
701 );
702 assert!(
703 body.contains("## display"),
704 "missing display section\n{body}"
705 );
706 assert!(
709 body.contains("Number of columns a hard tab"),
710 "tabstop doc not rendered\n{body}",
711 );
712 assert!(body.contains("[ts]"), "tabstop alias missing\n{body}");
715 assert!(body.contains("[nu]"), "number alias missing\n{body}");
716 }
717
718 #[test]
719 fn list_options_hides_non_customizable_options() {
720 let mut a = app_with("xx", 10);
723 a.editor.set_command_line_text("options");
724 a.editor.modal = ModalState::Command;
725 a.apply(Action::CommandLineSubmit);
726 let h = a.popup_help().expect("options help");
727 let body = h.content.as_string();
728 assert!(
729 !body.contains("**read-only**"),
730 "read-only should be hidden from :options reference\n{body}",
731 );
732 }
733
734 struct OptionContributingMode {
739 id: lattice_mode::ModeId,
740 }
741
742 impl OptionContributingMode {
743 fn new() -> Self {
744 Self {
745 id: lattice_mode::ModeId::new("test-option-contrib-mode"),
746 }
747 }
748 }
749
750 impl lattice_mode::Mode for OptionContributingMode {
751 type Guard = ();
752 fn id(&self) -> lattice_mode::ModeId {
753 self.id
754 }
755 fn kind(&self) -> lattice_mode::ModeKind {
756 lattice_mode::ModeKind::Minor
757 }
758 fn options(&self) -> lattice_config::OptionOverrideSet {
759 lattice_config::overrides! {
760 lattice_config::Tabstop = 4i64,
761 lattice_config::Number = false,
762 }
763 }
764 fn on_activate(
765 &self,
766 _ctx: lattice_mode::ModeContext,
767 ) -> lattice_mode::LifecycleFuture<'_, ()> {
768 Box::pin(async { Ok(()) })
769 }
770 }
771
772 #[test]
773 fn recompute_options_uses_registry_defaults_when_no_modes() {
774 let mut a = app_with("hi", 5);
775 let buf = a.editor.document_buffer_id;
776 a.recompute_options_for_buffer(buf);
777 let v = a.resolved_option::<lattice_config::Tabstop>(buf);
778 assert_eq!(*v, 4);
779 let n = a.resolved_option::<lattice_config::Number>(buf);
780 assert!(*n);
781 }
782
783 #[test]
784 fn recompute_options_overlays_active_mode_contributions() {
785 let mut a = app_with("hi", 5);
786 let buf = a.editor.document_buffer_id;
787 let mode_id = {
789 let mut registry = (**a.editor.mode_registry.load()).clone();
790 let id = registry
791 .register(OptionContributingMode::new())
792 .expect("register");
793 a.editor.mode_registry.store(std::sync::Arc::new(registry));
794 id
795 };
796 let mut active = lattice_mode::ActiveModes::new();
797 let guards = lattice_mode::GuardStoreHandle::new();
798 a.editor
799 .mode_registry
800 .load_full()
801 .activate_minor(
802 &mut active,
803 &guards,
804 &a.editor.config,
805 &a.editor.event_bus,
806 &a.editor.services,
807 lattice_protocol::ids::BufferId::new(0),
808 mode_id,
809 lattice_mode::CapabilitySet::empty(),
810 )
811 .expect("activate");
812 a.editor.active_modes.insert(buf, active);
813
814 a.recompute_options_for_buffer(buf);
815
816 let v = a.resolved_option::<lattice_config::Tabstop>(buf);
817 assert_eq!(*v, 4);
818 let n = a.resolved_option::<lattice_config::Number>(buf);
819 assert!(!*n);
820 }
821
822 #[test]
823 fn buffer_local_override_beats_mode_contribution() {
824 let mut a = app_with("hi", 5);
825 let buf = a.editor.document_buffer_id;
826 let mode_id = {
828 let mut registry = (**a.editor.mode_registry.load()).clone();
829 let id = registry
830 .register(OptionContributingMode::new())
831 .expect("register");
832 a.editor.mode_registry.store(std::sync::Arc::new(registry));
833 id
834 };
835 let mut active = lattice_mode::ActiveModes::new();
836 let guards = lattice_mode::GuardStoreHandle::new();
837 a.editor
838 .mode_registry
839 .load_full()
840 .activate_minor(
841 &mut active,
842 &guards,
843 &a.editor.config,
844 &a.editor.event_bus,
845 &a.editor.services,
846 lattice_protocol::ids::BufferId::new(0),
847 mode_id,
848 lattice_mode::CapabilitySet::empty(),
849 )
850 .expect("activate");
851 a.editor.active_modes.insert(buf, active);
852
853 let mut local = lattice_config::OptionOverrideSet::new();
854 local.push(lattice_config::OptionOverride::new(
855 std::any::TypeId::of::<lattice_config::Tabstop>(),
856 16i64,
857 ));
858 a.editor.buffer_local_overrides.insert(buf, local);
859
860 a.recompute_options_for_buffer(buf);
861
862 let v = a.resolved_option::<lattice_config::Tabstop>(buf);
863 assert_eq!(*v, 16);
864 let n = a.resolved_option::<lattice_config::Number>(buf);
865 assert!(!*n);
866 }
867
868 #[test]
869 fn resolved_option_falls_back_to_registry_pre_recompute() {
870 let a = app_with("hi", 5);
871 let buf = a.editor.document_buffer_id;
872 let v = a.resolved_option::<lattice_config::Tabstop>(buf);
873 assert_eq!(*v, 4);
874 }
875
876 #[test]
877 fn show_line_numbers_for_resolves_per_buffer() {
878 use crate::buffer_registry::{BufferData, BufferEntry, DocumentEntry};
883 use crate::buffers::{BufferFlags, BufferId};
884 let mut a = app_with("hi", 5);
885 let active = a.editor.document_buffer_id;
886 let other = BufferId::next();
888 let handle = a.editor.document.as_arc();
889 a.editor.buffers.insert(BufferEntry {
890 id: other,
891 flags: BufferFlags::default(),
892 data: BufferData::Document(DocumentEntry { id: other, handle }),
893 name: None,
894 });
895 let mut local = lattice_config::OptionOverrideSet::new();
897 local.push(lattice_config::OptionOverride::new(
898 std::any::TypeId::of::<lattice_config::Number>(),
899 false,
900 ));
901 a.editor.buffer_local_overrides.insert(other, local);
902 a.recompute_options_for_buffer(other);
903 assert!(a.show_line_numbers_for(active));
906 assert!(!a.show_line_numbers_for(other));
907 }
908
909 #[test]
912 fn document_buffer_resolves_read_only_false() {
913 let a = app_with("hi", 5);
914 let buf = a.editor.document_buffer_id;
915 let read_only: bool = *a.resolved_option::<lattice_config::ReadOnly>(buf);
916 assert!(!read_only, "Document buffer should be writable by default");
917 }
918
919 #[test]
920 fn help_buffer_resolves_read_only_true() {
921 let mut a = app_with("hi", 5);
922 let help = crate::help::HelpContent::from_lines("test", vec!["line one".to_string()]);
923 let help_id = a.open_help_in_pane(help);
924 let read_only: bool = *a.resolved_option::<lattice_config::ReadOnly>(help_id);
925 assert!(
926 read_only,
927 "Help buffer should resolve ReadOnly = true via help-mode"
928 );
929 }
930
931 #[test]
932 fn boot_registers_the_builtin_signs() {
933 let a = app_with("hello", 10);
937 let registry = a
938 .editor
939 .services
940 .get::<lattice_mode::SignRegistryHandle>()
941 .expect("the sign registry is a boot service");
942 let snapshot = registry.load();
943 for name in [
944 "diagnostic.error",
945 "diagnostic.warning",
946 "diagnostic.info",
947 "diagnostic.hint",
948 "diff.add",
949 "diff.change",
950 "diff.remove",
951 "diff.conflict",
952 ] {
953 assert!(
954 snapshot.id_of(name).is_some(),
955 "`{name}` must be a registered sign"
956 );
957 }
958 let ids = a.editor.builtin_sign_ids;
961 assert_eq!(
962 snapshot.id_of("diagnostic.error"),
963 Some(ids.diagnostic_error)
964 );
965 assert_eq!(snapshot.id_of("diff.add"), Some(ids.diff_add));
966 }
967
968 #[test]
969 fn setting_a_diagnostic_glyph_rewrites_its_sign_without_moving_the_id() {
970 let mut a = app_with("hello", 10);
976 let registry = a
977 .editor
978 .services
979 .get::<lattice_mode::SignRegistryHandle>()
980 .expect("the sign registry is a boot service");
981 let before = a.editor.builtin_sign_ids.diagnostic_error;
982 assert_eq!(registry.load().get(before).unwrap().glyph_char(false), '■');
983
984 submit_ex(&mut a, "set ui.diagnostic-error-glyph=X");
985
986 let after = a.editor.builtin_sign_ids.diagnostic_error;
987 assert_eq!(before, after, "the id must survive the redefinition");
988 assert_eq!(
989 registry.load().get(after).unwrap().glyph_char(false),
990 'X',
991 "and the new glyph must be what the registry now answers"
992 );
993 }
994
995 #[test]
996 fn read_only_option_is_marked_internal() {
997 use lattice_config::OptionDecl;
998 assert!(
999 !lattice_config::ReadOnly::CUSTOMIZABLE,
1000 "ReadOnly should be non-customizable (mode-driven)"
1001 );
1002 }
1003
1004 #[test]
1005 fn set_number_and_nonumber_toggle_show_line_numbers() {
1006 let mut a = app_with("hello", 10);
1007 assert!(a.show_line_numbers());
1008 submit_ex(&mut a, "set nonumber");
1009 assert!(!a.show_line_numbers());
1010 submit_ex(&mut a, "set number");
1011 assert!(a.show_line_numbers());
1012 }
1013
1014 #[test]
1015 fn set_relativenumber_toggles_flag() {
1016 let mut a = app_with("hello\nworld", 10);
1017 assert!(!a.relative_line_numbers());
1018 submit_ex(&mut a, "set relativenumber");
1019 assert!(a.relative_line_numbers());
1020 assert!(a.show_line_numbers());
1021 submit_ex(&mut a, "set norelativenumber");
1022 assert!(!a.relative_line_numbers());
1023 }
1024
1025 #[test]
1026 fn typing_after_popup_open_live_refilters_candidates() {
1027 let mut a = app_in_command_mode("descr");
1030 a.apply(Action::CommandLineCompleteOrAdvance);
1031 assert!(a.editor.completion_state.is_some());
1032 assert_eq!(a.editor.command_line(), "describe-");
1038 let initial_count = a.editor.completion_state.as_ref().unwrap().candidates.len();
1039
1040 a.apply(Action::CommandLineAppend('k'));
1044 assert!(
1045 a.editor.completion_state.is_some(),
1046 "popup must stay open while filtering"
1047 );
1048 assert_eq!(a.editor.command_line(), "describe-k");
1049 let narrowed = a.editor.completion_state.as_ref().unwrap().candidates.len();
1052 assert!(narrowed <= initial_count);
1053 assert!(narrowed >= 1, "describe-key still matches `describe-k`");
1054 assert_eq!(a.editor.completion_state.as_ref().unwrap().selected, 0);
1057 }
1058
1059 #[test]
1060 fn typing_no_match_keeps_popup_open_with_empty_candidates() {
1061 let mut a = app_in_command_mode("descri");
1064 a.apply(Action::CommandLineCompleteOrAdvance);
1065 for c in "zxqzxqzxq".chars() {
1066 a.apply(Action::CommandLineAppend(c));
1067 }
1068 let state = a
1069 .editor
1070 .completion_state
1071 .as_ref()
1072 .expect("popup must stay open on no-match");
1073 assert!(state.candidates.is_empty());
1074 for _ in 0.."zxqzxqzxq".len() {
1076 a.apply(Action::CommandLineBackspace);
1077 }
1078 assert!(a.editor.completion_state.is_some());
1079 assert!(
1080 !a.editor
1081 .completion_state
1082 .as_ref()
1083 .unwrap()
1084 .candidates
1085 .is_empty()
1086 );
1087 }
1088
1089 #[test]
1090 fn typing_with_no_popup_open_does_not_open_one() {
1091 let mut a = app_in_command_mode("desc");
1094 a.apply(Action::CommandLineAppend('r'));
1095 assert!(a.editor.completion_state.is_none());
1096 assert_eq!(a.editor.command_line(), "descr");
1097 }
1098
1099 #[test]
1100 fn fresh_app_has_one_document_pane() {
1101 let a = app_with("xx", 10);
1102 assert_eq!(a.editor.pane_tree.len(), 1);
1103 assert_eq!(a.editor.active_buffer, BufferKind::Document);
1104 let active = a.editor.pane_tree.active();
1105 assert_eq!(active.buffer, BufferKind::Document);
1106 assert_eq!(active.buffer_id, a.editor.document_buffer_id);
1107 }
1108
1109 #[test]
1110 fn fresh_app_registers_initial_document() {
1111 let a = app_with("xx", 10);
1112 assert_eq!(a.editor.buffers.listed_ids_sorted().len(), 1);
1115 assert!(
1116 a.editor
1117 .buffers
1118 .contains_document(a.editor.document_buffer_id)
1119 );
1120 }
1121
1122 #[test]
1123 fn set_tabstop_assignment_updates_field() {
1124 let mut a = app_with("xx", 10);
1125 a.editor.set_command_line_text("set tabstop=4");
1126 a.editor.modal = ModalState::Command;
1127 a.apply(Action::CommandLineSubmit);
1128 assert_eq!(a.tabstop(), 4);
1129 }
1130
1131 #[test]
1132 fn set_tabstop_via_alias() {
1133 let mut a = app_with("xx", 10);
1134 a.editor.set_command_line_text("set ts=2");
1135 a.editor.modal = ModalState::Command;
1136 a.apply(Action::CommandLineSubmit);
1137 assert_eq!(a.tabstop(), 2);
1138 }
1139
1140 #[test]
1141 fn effective_completion_for_markdown_default_excludes_lsp() {
1142 let a = app_with("", 5);
1146 let eff = a.effective_completion_for("markdown");
1147 let lsp_id =
1148 lattice_completion::SourceId::new(lattice_completion::LSP_COMPLETION_SOURCE_ID);
1149 assert!(!eff.source_enabled(&lsp_id), "markdown default drops LSP");
1150 let snippet_id = lattice_completion::SourceId::new(lattice_completion::SNIPPET_SOURCE_ID);
1151 assert!(eff.source_enabled(&snippet_id), "markdown keeps snippet");
1152 assert!(!eff.auto_trigger);
1153 }
1154
1155 #[test]
1156 fn effective_completion_for_language_with_no_override_allows_all_sources() {
1157 let a = app_with("", 5);
1161 let eff = a.effective_completion_for("zigzig-not-a-language");
1162 let any_id = lattice_completion::SourceId::new("plugin:custom");
1163 assert!(eff.source_enabled(&any_id));
1164 assert!(eff.sources.is_none());
1165 }
1166
1167 #[test]
1168 fn reload_snippets_with_no_user_dirs_loads_builtins_only() {
1169 let mut a = app_with("", 10);
1170 a.editor.snippet_dirs.clear();
1174 a.do_reload_snippets();
1175 let builtins = lattice_snippet::load_builtins().len();
1179 assert!(builtins > 0, "built-in packs are non-empty");
1180 assert_eq!(a.editor.snippet_registry.load().len(), builtins);
1181 }
1182
1183 #[test]
1184 fn reload_snippets_walks_configured_dirs_and_keys_by_filename() {
1185 let dir = std::env::temp_dir().join(format!("lattice-snippet-test-{}", std::process::id()));
1189 let _ = std::fs::remove_dir_all(&dir);
1190 std::fs::create_dir_all(&dir).unwrap();
1191 std::fs::write(
1192 dir.join("_global.json"),
1193 r#"{ "anywhere": { "prefix": "any", "body": "anywhere" } }"#,
1194 )
1195 .unwrap();
1196 std::fs::write(
1197 dir.join("rust.json"),
1198 r#"{ "rust-for": { "prefix": "for", "body": "for $1 {}" } }"#,
1199 )
1200 .unwrap();
1201 let builtins = lattice_snippet::load_builtins().len();
1202 let mut a = app_with("", 10);
1203 a.editor.snippet_dirs.clear();
1206 a.editor.snippet_dirs.push(dir.clone());
1207 a.do_reload_snippets();
1208 assert_eq!(a.editor.snippet_registry.load().len(), builtins + 2);
1211 assert!(
1212 !a.editor
1213 .snippet_registry
1214 .load()
1215 .lookup("rust", "for")
1216 .is_empty()
1217 );
1218 assert!(
1219 !a.editor
1220 .snippet_registry
1221 .load()
1222 .lookup("*", "any")
1223 .is_empty()
1224 );
1225 assert!(
1228 !a.editor
1229 .snippet_registry
1230 .load()
1231 .lookup("rust", "any")
1232 .is_empty()
1233 );
1234 assert!(
1237 a.editor
1238 .snippet_registry
1239 .load()
1240 .lookup("python", "for")
1241 .is_empty()
1242 );
1243 let _ = std::fs::remove_dir_all(&dir);
1244 }
1245}