1pub use hjkl_buffer::ContentEdit;
14pub use hjkl_buffer::EngineEdit as Edit;
15pub use hjkl_buffer::FoldOp;
16pub use hjkl_buffer::Pos;
17
18use std::ops::Range;
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
31pub enum SelectionKind {
32 #[default]
33 Char,
34 Line,
35 Block,
36}
37
38#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
40pub struct Selection {
41 pub anchor: Pos,
42 pub head: Pos,
43 pub kind: SelectionKind,
44}
45
46impl Selection {
47 pub const fn caret(pos: Pos) -> Self {
49 Self {
50 anchor: pos,
51 head: pos,
52 kind: SelectionKind::Char,
53 }
54 }
55
56 pub const fn char_range(anchor: Pos, head: Pos) -> Self {
58 Self {
59 anchor,
60 head,
61 kind: SelectionKind::Char,
62 }
63 }
64
65 pub fn is_empty(&self) -> bool {
67 self.anchor == self.head
68 }
69}
70
71#[derive(Debug, Clone, PartialEq, Eq)]
74pub struct SelectionSet {
75 pub items: Vec<Selection>,
76 pub primary: usize,
77}
78
79impl SelectionSet {
80 pub fn caret(pos: Pos) -> Self {
82 Self {
83 items: vec![Selection::caret(pos)],
84 primary: 0,
85 }
86 }
87
88 pub fn primary(&self) -> &Selection {
91 self.items
92 .get(self.primary)
93 .or_else(|| self.items.first())
94 .expect("SelectionSet must contain at least one selection")
95 }
96}
97
98impl Default for SelectionSet {
99 fn default() -> Self {
100 Self::caret(Pos::ORIGIN)
101 }
102}
103
104#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
108pub enum Mode {
109 #[default]
110 Normal,
111 Insert,
112 Visual,
113 Replace,
114 Command,
115 OperatorPending,
116}
117
118#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
121pub enum CursorShape {
122 #[default]
123 Block,
124 Bar,
125 Underline,
126}
127
128#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
131pub struct Style {
132 pub fg: Option<Color>,
133 pub bg: Option<Color>,
134 pub attrs: Attrs,
135}
136
137#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
138pub struct Color(pub u8, pub u8, pub u8);
139
140bitflags::bitflags! {
141 #[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Hash)]
142 pub struct Attrs: u8 {
143 const BOLD = 1 << 0;
144 const ITALIC = 1 << 1;
145 const UNDERLINE = 1 << 2;
146 const REVERSE = 1 << 3;
147 const DIM = 1 << 4;
148 const STRIKE = 1 << 5;
149 }
150}
151
152#[derive(Debug, Clone, Copy, PartialEq, Eq)]
156pub enum HighlightKind {
157 Selection,
158 SearchMatch,
159 IncSearch,
160 MatchParen,
161 Syntax(u32),
162}
163
164#[derive(Debug, Clone, PartialEq, Eq)]
165pub struct Highlight {
166 pub range: Range<Pos>,
167 pub kind: HighlightKind,
168}
169
170#[derive(Debug, Clone, PartialEq, Eq)]
174pub struct Options {
175 pub tabstop: u32,
177 pub shiftwidth: u32,
179 pub expandtab: bool,
181 pub softtabstop: u32,
186 pub iskeyword: String,
190 pub ignorecase: bool,
192 pub smartcase: bool,
195 pub hlsearch: bool,
197 pub incsearch: bool,
199 pub wrapscan: bool,
201 pub autoindent: bool,
203 pub smartindent: bool,
210 pub timeout_len: core::time::Duration,
212 pub undo_levels: u32,
214 pub undo_break_on_motion: bool,
217 pub readonly: bool,
219 pub modifiable: bool,
223 pub wrap: WrapMode,
226 pub textwidth: u32,
228 pub number: bool,
231 pub relativenumber: bool,
234 pub numberwidth: usize,
238 pub cursorline: bool,
244 pub cursorcolumn: bool,
247 pub signcolumn: SignColumnMode,
250 pub foldcolumn: u32,
253 pub foldmethod: FoldMethod,
258 pub foldenable: bool,
262 pub foldlevelstart: u32,
265 pub foldmarker: String,
270 pub colorcolumn: String,
273 pub formatoptions: String,
278 pub filetype: String,
281 pub scrolloff: usize,
286 pub sidescrolloff: usize,
290 pub modeline: bool,
295 pub modelines: u32,
298 pub autoreload: bool,
303 pub motion_sneak: bool,
309 pub list: bool,
312 pub listchars: ListChars,
316 pub indent_guides: bool,
320 pub indent_guide_char: char,
323 pub colorizer: bool,
327 pub colorizer_filetypes: Vec<String>,
331 pub format_on_save: bool,
337 pub trim_trailing_whitespace: bool,
341 pub rainbow_brackets: bool,
344 pub updatetime: u32,
348 pub matchparen: bool,
354 pub fixendofline: bool,
364}
365
366pub use hjkl_buffer::ListChars;
371
372#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
375
376pub enum FoldMethod {
377 Manual,
379 #[default]
384 Expr,
385 Marker,
388}
389
390#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
393
394pub enum SignColumnMode {
395 No,
397 Yes,
399 #[default]
401 Auto,
402}
403
404#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
408
409pub enum DiagInlineMode {
410 Off,
412 Current,
414 #[default]
416 All,
417}
418
419#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
423
424pub enum WrapMode {
425 #[default]
428 None,
429 Char,
432 Word,
436}
437
438#[derive(Debug, Clone, PartialEq, Eq)]
444pub enum OptionValue {
445 Bool(bool),
446 Int(i64),
447 String(String),
448}
449
450impl Default for Options {
451 fn default() -> Self {
452 Self {
453 tabstop: 4,
454 shiftwidth: 4,
455 expandtab: true,
456 softtabstop: 4,
457 iskeyword: "@,48-57,_,192-255".to_string(),
458 ignorecase: true,
459 smartcase: true,
460 hlsearch: true,
461 incsearch: true,
462 wrapscan: true,
463 autoindent: true,
464 smartindent: true,
465 timeout_len: core::time::Duration::from_millis(1000),
466 undo_levels: 1000,
467 undo_break_on_motion: true,
468 readonly: false,
469 modifiable: true,
470 wrap: WrapMode::None,
471 textwidth: 79,
472 number: true,
473 relativenumber: false,
474 numberwidth: 4,
475 cursorline: false,
476 cursorcolumn: false,
477 signcolumn: SignColumnMode::Auto,
478 foldcolumn: 0,
479 foldmethod: FoldMethod::Expr,
480 foldenable: true,
481 foldlevelstart: 99,
482 foldmarker: "{{{,}}}".to_string(),
483 colorcolumn: String::new(),
484 formatoptions: "ro".to_string(),
485 filetype: String::new(),
486 scrolloff: 5,
487 sidescrolloff: 0,
488 modeline: true,
489 modelines: 5,
490 autoreload: true,
491 motion_sneak: true,
492 list: false,
493 listchars: ListChars::default(),
494 indent_guides: true,
495 indent_guide_char: '│',
496 colorizer: true,
497 colorizer_filetypes: vec![
498 "css".to_string(),
499 "scss".to_string(),
500 "sass".to_string(),
501 "less".to_string(),
502 "html".to_string(),
503 "vue".to_string(),
504 "svelte".to_string(),
505 "tailwindcss".to_string(),
506 "toml".to_string(),
507 "lua".to_string(),
508 "vim".to_string(),
509 ],
510 format_on_save: true,
511 trim_trailing_whitespace: false,
512 rainbow_brackets: true,
513 updatetime: 4000,
514 matchparen: true,
515 fixendofline: true,
516 }
517 }
518}
519
520impl Options {
521 pub fn set_by_name(&mut self, name: &str, val: OptionValue) -> Result<(), EngineError> {
528 macro_rules! set_bool {
529 ($field:ident) => {{
530 self.$field = match val {
531 OptionValue::Bool(b) => b,
532 OptionValue::Int(n) => n != 0,
533 other => {
534 return Err(EngineError::Ex(format!(
535 "option `{name}` expects bool, got {other:?}"
536 )));
537 }
538 };
539 Ok(())
540 }};
541 }
542 macro_rules! set_u32 {
543 ($field:ident) => {{
544 self.$field = match val {
545 OptionValue::Int(n) if n >= 0 && n <= u32::MAX as i64 => n as u32,
546 OptionValue::Int(n) => {
547 return Err(EngineError::Ex(format!(
548 "option `{name}` out of u32 range: {n}"
549 )));
550 }
551 other => {
552 return Err(EngineError::Ex(format!(
553 "option `{name}` expects int, got {other:?}"
554 )));
555 }
556 };
557 Ok(())
558 }};
559 }
560 macro_rules! set_string {
561 ($field:ident) => {{
562 self.$field = match val {
563 OptionValue::String(s) => s,
564 other => {
565 return Err(EngineError::Ex(format!(
566 "option `{name}` expects string, got {other:?}"
567 )));
568 }
569 };
570 Ok(())
571 }};
572 }
573 match name {
574 "tabstop" | "ts" => set_u32!(tabstop),
575 "shiftwidth" | "sw" => set_u32!(shiftwidth),
576 "softtabstop" | "sts" => set_u32!(softtabstop),
577 "textwidth" | "tw" => set_u32!(textwidth),
578 "expandtab" | "et" => set_bool!(expandtab),
579 "iskeyword" | "isk" => set_string!(iskeyword),
580 "ignorecase" | "ic" => set_bool!(ignorecase),
581 "smartcase" | "scs" => set_bool!(smartcase),
582 "hlsearch" | "hls" => set_bool!(hlsearch),
583 "incsearch" | "is" => set_bool!(incsearch),
584 "wrapscan" | "ws" => set_bool!(wrapscan),
585 "autoindent" | "ai" => set_bool!(autoindent),
586 "smartindent" | "si" => set_bool!(smartindent),
587 "timeoutlen" | "tm" => {
588 self.timeout_len = match val {
589 OptionValue::Int(n) if n >= 0 => core::time::Duration::from_millis(n as u64),
590 other => {
591 return Err(EngineError::Ex(format!(
592 "option `{name}` expects non-negative int (millis), got {other:?}"
593 )));
594 }
595 };
596 Ok(())
597 }
598 "undolevels" | "ul" => set_u32!(undo_levels),
599 "undobreak" => set_bool!(undo_break_on_motion),
600 "readonly" | "ro" => set_bool!(readonly),
601 "modifiable" | "ma" => set_bool!(modifiable),
602 "wrap" => {
603 let on = match val {
604 OptionValue::Bool(b) => b,
605 OptionValue::Int(n) => n != 0,
606 other => {
607 return Err(EngineError::Ex(format!(
608 "option `{name}` expects bool, got {other:?}"
609 )));
610 }
611 };
612 self.wrap = match (on, self.wrap) {
613 (false, _) => WrapMode::None,
614 (true, WrapMode::Word) => WrapMode::Word,
615 (true, _) => WrapMode::Char,
616 };
617 Ok(())
618 }
619 "linebreak" | "lbr" => {
620 let on = match val {
621 OptionValue::Bool(b) => b,
622 OptionValue::Int(n) => n != 0,
623 other => {
624 return Err(EngineError::Ex(format!(
625 "option `{name}` expects bool, got {other:?}"
626 )));
627 }
628 };
629 self.wrap = match (on, self.wrap) {
630 (true, _) => WrapMode::Word,
631 (false, WrapMode::Word) => WrapMode::Char,
632 (false, other) => other,
633 };
634 Ok(())
635 }
636 "number" | "nu" => set_bool!(number),
637 "relativenumber" | "rnu" => set_bool!(relativenumber),
638 "numberwidth" | "nuw" => {
639 self.numberwidth = match val {
640 OptionValue::Int(n) if (1..=20).contains(&n) => n as usize,
641 OptionValue::Int(n) => {
642 return Err(EngineError::Ex(format!(
643 "option `{name}` must be in range 1..=20, got {n}"
644 )));
645 }
646 other => {
647 return Err(EngineError::Ex(format!(
648 "option `{name}` expects int, got {other:?}"
649 )));
650 }
651 };
652 Ok(())
653 }
654 "cursorline" | "cul" => set_bool!(cursorline),
655 "cursorcolumn" | "cuc" => set_bool!(cursorcolumn),
656 "signcolumn" | "scl" => {
657 self.signcolumn = match val {
658 OptionValue::String(ref s) => match s.as_str() {
659 "yes" => SignColumnMode::Yes,
660 "no" => SignColumnMode::No,
661 "auto" => SignColumnMode::Auto,
662 other => {
663 return Err(EngineError::Ex(format!(
664 "option `{name}` must be `yes`, `no`, or `auto`, got {other:?}"
665 )));
666 }
667 },
668 other => {
669 return Err(EngineError::Ex(format!(
670 "option `{name}` expects string (yes/no/auto), got {other:?}"
671 )));
672 }
673 };
674 Ok(())
675 }
676 "foldcolumn" | "fdc" => {
677 self.foldcolumn = match val {
678 OptionValue::Int(n) if (0..=12).contains(&n) => n as u32,
679 OptionValue::Int(n) => {
680 return Err(EngineError::Ex(format!(
681 "option `{name}` must be in range 0..=12, got {n}"
682 )));
683 }
684 other => {
685 return Err(EngineError::Ex(format!(
686 "option `{name}` expects int (0-12), got {other:?}"
687 )));
688 }
689 };
690 Ok(())
691 }
692 "foldmethod" | "fdm" => {
693 self.foldmethod = match val {
694 OptionValue::String(ref s) => match s.as_str() {
695 "manual" => FoldMethod::Manual,
696 "expr" | "syntax" => FoldMethod::Expr,
697 "marker" => FoldMethod::Marker,
698 other => {
699 return Err(EngineError::Ex(format!(
700 "option `{name}` must be `manual`, `expr`, `syntax`, or `marker`, got `{other}`"
701 )));
702 }
703 },
704 other => {
705 return Err(EngineError::Ex(format!(
706 "option `{name}` expects string, got {other:?}"
707 )));
708 }
709 };
710 Ok(())
711 }
712 "foldenable" | "fen" => set_bool!(foldenable),
713 "foldlevelstart" | "fls" => set_u32!(foldlevelstart),
714 "colorcolumn" | "cc" => set_string!(colorcolumn),
715 "formatoptions" | "fo" => set_string!(formatoptions),
716 "filetype" | "ft" => set_string!(filetype),
717 "scrolloff" | "so" => {
718 self.scrolloff = match val {
719 OptionValue::Int(n) if n >= 0 => n as usize,
720 OptionValue::Int(n) => {
721 return Err(EngineError::Ex(format!(
722 "option `{name}` must be >= 0, got {n}"
723 )));
724 }
725 other => {
726 return Err(EngineError::Ex(format!(
727 "option `{name}` expects int, got {other:?}"
728 )));
729 }
730 };
731 Ok(())
732 }
733 "sidescrolloff" | "siso" => {
734 self.sidescrolloff = match val {
735 OptionValue::Int(n) if n >= 0 => n as usize,
736 OptionValue::Int(n) => {
737 return Err(EngineError::Ex(format!(
738 "option `{name}` must be >= 0, got {n}"
739 )));
740 }
741 other => {
742 return Err(EngineError::Ex(format!(
743 "option `{name}` expects int, got {other:?}"
744 )));
745 }
746 };
747 Ok(())
748 }
749 "modeline" | "ml" => set_bool!(modeline),
750 "autoreload" | "ar" => set_bool!(autoreload),
751 "modelines" | "mls" => set_u32!(modelines),
752 "motion_sneak" | "snk" => set_bool!(motion_sneak),
753 "list" => set_bool!(list),
754 "listchars" | "lcs" => {
755 let s = match val {
756 OptionValue::String(s) => s,
757 other => {
758 return Err(EngineError::Ex(format!(
759 "option `{name}` expects string, got {other:?}"
760 )));
761 }
762 };
763 self.listchars = ListChars::parse(&s).map_err(EngineError::Ex)?;
764 Ok(())
765 }
766 "indent_guides" | "ig" => set_bool!(indent_guides),
767 "colorizer" | "clz" => set_bool!(colorizer),
768 "colorizer_filetypes" | "clzft" => {
769 let s = match val {
770 OptionValue::String(s) => s,
771 other => {
772 return Err(EngineError::Ex(format!(
773 "option `{name}` expects string, got {other:?}"
774 )));
775 }
776 };
777 self.colorizer_filetypes = s
778 .split(',')
779 .map(|p| p.trim().to_string())
780 .filter(|p| !p.is_empty())
781 .collect();
782 Ok(())
783 }
784 "indent_guide_char" | "igc" => {
785 let s = match val {
786 OptionValue::String(s) => s,
787 other => {
788 return Err(EngineError::Ex(format!(
789 "option `{name}` expects a single-char string, got {other:?}"
790 )));
791 }
792 };
793 let mut chars = s.chars();
794 let (Some(ch), None) = (chars.next(), chars.next()) else {
795 return Err(EngineError::Ex(format!(
796 "option `{name}` expects exactly one character, got {s:?}"
797 )));
798 };
799 self.indent_guide_char = ch;
800 Ok(())
801 }
802 "format_on_save" | "fos" => set_bool!(format_on_save),
803 "trim_trailing_whitespace" | "tts" => set_bool!(trim_trailing_whitespace),
804 "rainbow_brackets" | "rb" => set_bool!(rainbow_brackets),
805 "updatetime" | "ut" => set_u32!(updatetime),
806 "matchparen" | "mps" => set_bool!(matchparen),
807 "fixendofline" | "fixeol" => set_bool!(fixendofline),
808 other => Err(EngineError::Ex(format!("unknown option `{other}`"))),
809 }
810 }
811
812 pub fn get_by_name(&self, name: &str) -> Option<OptionValue> {
814 Some(match name {
815 "tabstop" | "ts" => OptionValue::Int(self.tabstop as i64),
816 "shiftwidth" | "sw" => OptionValue::Int(self.shiftwidth as i64),
817 "softtabstop" | "sts" => OptionValue::Int(self.softtabstop as i64),
818 "textwidth" | "tw" => OptionValue::Int(self.textwidth as i64),
819 "expandtab" | "et" => OptionValue::Bool(self.expandtab),
820 "iskeyword" | "isk" => OptionValue::String(self.iskeyword.clone()),
821 "ignorecase" | "ic" => OptionValue::Bool(self.ignorecase),
822 "smartcase" | "scs" => OptionValue::Bool(self.smartcase),
823 "hlsearch" | "hls" => OptionValue::Bool(self.hlsearch),
824 "incsearch" | "is" => OptionValue::Bool(self.incsearch),
825 "wrapscan" | "ws" => OptionValue::Bool(self.wrapscan),
826 "autoindent" | "ai" => OptionValue::Bool(self.autoindent),
827 "smartindent" | "si" => OptionValue::Bool(self.smartindent),
828 "timeoutlen" | "tm" => OptionValue::Int(self.timeout_len.as_millis() as i64),
829 "undolevels" | "ul" => OptionValue::Int(self.undo_levels as i64),
830 "undobreak" => OptionValue::Bool(self.undo_break_on_motion),
831 "readonly" | "ro" => OptionValue::Bool(self.readonly),
832 "modifiable" | "ma" => OptionValue::Bool(self.modifiable),
833 "wrap" => OptionValue::Bool(!matches!(self.wrap, WrapMode::None)),
834 "linebreak" | "lbr" => OptionValue::Bool(matches!(self.wrap, WrapMode::Word)),
835 "number" | "nu" => OptionValue::Bool(self.number),
836 "relativenumber" | "rnu" => OptionValue::Bool(self.relativenumber),
837 "numberwidth" | "nuw" => OptionValue::Int(self.numberwidth as i64),
838 "cursorline" | "cul" => OptionValue::Bool(self.cursorline),
839 "cursorcolumn" | "cuc" => OptionValue::Bool(self.cursorcolumn),
840 "signcolumn" | "scl" => OptionValue::String(
841 match self.signcolumn {
842 SignColumnMode::Yes => "yes",
843 SignColumnMode::No => "no",
844 SignColumnMode::Auto => "auto",
845 }
846 .to_string(),
847 ),
848 "foldcolumn" | "fdc" => OptionValue::Int(self.foldcolumn as i64),
849 "foldmethod" | "fdm" => OptionValue::String(
850 match self.foldmethod {
851 FoldMethod::Manual => "manual",
852 FoldMethod::Expr => "expr",
853 FoldMethod::Marker => "marker",
854 }
855 .to_string(),
856 ),
857 "foldenable" | "fen" => OptionValue::Bool(self.foldenable),
858 "foldlevelstart" | "fls" => OptionValue::Int(self.foldlevelstart as i64),
859 "colorcolumn" | "cc" => OptionValue::String(self.colorcolumn.clone()),
860 "formatoptions" | "fo" => OptionValue::String(self.formatoptions.clone()),
861 "filetype" | "ft" => OptionValue::String(self.filetype.clone()),
862 "scrolloff" | "so" => OptionValue::Int(self.scrolloff as i64),
863 "sidescrolloff" | "siso" => OptionValue::Int(self.sidescrolloff as i64),
864 "modeline" | "ml" => OptionValue::Bool(self.modeline),
865 "autoreload" | "ar" => OptionValue::Bool(self.autoreload),
866 "modelines" | "mls" => OptionValue::Int(self.modelines as i64),
867 "motion_sneak" | "snk" => OptionValue::Bool(self.motion_sneak),
868 "list" => OptionValue::Bool(self.list),
869 "listchars" | "lcs" => OptionValue::String(self.listchars.to_canonical_string()),
870 "indent_guides" | "ig" => OptionValue::Bool(self.indent_guides),
871 "indent_guide_char" | "igc" => OptionValue::String(self.indent_guide_char.to_string()),
872 "colorizer" | "clz" => OptionValue::Bool(self.colorizer),
873 "colorizer_filetypes" | "clzft" => {
874 OptionValue::String(self.colorizer_filetypes.join(","))
875 }
876 "format_on_save" | "fos" => OptionValue::Bool(self.format_on_save),
877 "trim_trailing_whitespace" | "tts" => OptionValue::Bool(self.trim_trailing_whitespace),
878 "rainbow_brackets" | "rb" => OptionValue::Bool(self.rainbow_brackets),
879 "updatetime" | "ut" => OptionValue::Int(self.updatetime as i64),
880 "matchparen" | "mps" => OptionValue::Bool(self.matchparen),
881 "fixendofline" | "fixeol" => OptionValue::Bool(self.fixendofline),
882 _ => return None,
883 })
884 }
885}
886
887pub use hjkl_buffer::Viewport;
909
910#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
914pub struct BufferId(pub u64);
915
916#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
918pub struct Modifiers {
919 pub ctrl: bool,
920 pub shift: bool,
921 pub alt: bool,
922 pub super_: bool,
923}
924
925#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
927#[non_exhaustive]
928pub enum SpecialKey {
929 Esc,
930 Enter,
931 Backspace,
932 Tab,
933 BackTab,
934 Up,
935 Down,
936 Left,
937 Right,
938 Home,
939 End,
940 PageUp,
941 PageDown,
942 Insert,
943 Delete,
944 F(u8),
945}
946
947#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
948pub enum MouseKind {
949 Press,
950 Release,
951 Drag,
952 ScrollUp,
953 ScrollDown,
954}
955
956#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
957pub struct MouseEvent {
958 pub kind: MouseKind,
959 pub pos: Pos,
960 pub mods: Modifiers,
961}
962
963#[derive(Debug, Clone, PartialEq, Eq)]
968#[non_exhaustive]
969pub enum Input {
970 Char(char, Modifiers),
971 Key(SpecialKey, Modifiers),
972 Mouse(MouseEvent),
973 Paste(String),
974 FocusGained,
975 FocusLost,
976 Resize(u16, u16),
977}
978
979pub trait Host: Send {
988 type Intent;
992
993 fn write_clipboard(&mut self, text: String);
999
1000 fn read_clipboard(&mut self) -> Option<String>;
1003
1004 fn now(&self) -> core::time::Duration;
1010
1011 fn should_cancel(&self) -> bool {
1014 false
1015 }
1016
1017 fn prompt_search(&mut self) -> Option<String>;
1022
1023 fn display_line_for(&self, pos: Pos) -> u32 {
1028 pos.line
1029 }
1030
1031 fn pos_for_display(&self, line: u32, col: u32) -> Pos {
1033 Pos { line, col }
1034 }
1035
1036 fn syntax_highlights(&self, range: Range<Pos>) -> Vec<Highlight> {
1041 let _ = range;
1042 Vec::new()
1043 }
1044
1045 fn emit_cursor_shape(&mut self, shape: CursorShape);
1050
1051 fn viewport(&self) -> &Viewport;
1058
1059 fn viewport_mut(&mut self) -> &mut Viewport;
1062
1063 fn emit_intent(&mut self, intent: Self::Intent);
1068}
1069
1070#[derive(Debug)]
1084pub struct DefaultHost {
1085 clipboard: Option<String>,
1086 last_cursor_shape: CursorShape,
1087 started: std::time::Instant,
1088 viewport: Viewport,
1089}
1090
1091impl Default for DefaultHost {
1092 fn default() -> Self {
1093 Self::new()
1094 }
1095}
1096
1097impl DefaultHost {
1098 pub const DEFAULT_VIEWPORT: Viewport = Viewport {
1101 top_row: 0,
1102 top_col: 0,
1103 width: 80,
1104 height: 24,
1105 wrap: hjkl_buffer::Wrap::None,
1106 text_width: 80,
1107 tab_width: 0,
1108 };
1109
1110 pub fn new() -> Self {
1111 Self {
1112 clipboard: None,
1113 last_cursor_shape: CursorShape::Block,
1114 started: std::time::Instant::now(),
1115 viewport: Self::DEFAULT_VIEWPORT,
1116 }
1117 }
1118
1119 pub fn with_viewport(viewport: Viewport) -> Self {
1123 Self {
1124 clipboard: None,
1125 last_cursor_shape: CursorShape::Block,
1126 started: std::time::Instant::now(),
1127 viewport,
1128 }
1129 }
1130
1131 pub fn last_cursor_shape(&self) -> CursorShape {
1133 self.last_cursor_shape
1134 }
1135}
1136
1137impl Host for DefaultHost {
1138 type Intent = ();
1139
1140 fn write_clipboard(&mut self, text: String) {
1141 self.clipboard = Some(text);
1142 }
1143
1144 fn read_clipboard(&mut self) -> Option<String> {
1145 self.clipboard.clone()
1146 }
1147
1148 fn now(&self) -> core::time::Duration {
1149 self.started.elapsed()
1150 }
1151
1152 fn prompt_search(&mut self) -> Option<String> {
1153 None
1154 }
1155
1156 fn emit_cursor_shape(&mut self, shape: CursorShape) {
1157 self.last_cursor_shape = shape;
1158 }
1159
1160 fn viewport(&self) -> &Viewport {
1161 &self.viewport
1162 }
1163
1164 fn viewport_mut(&mut self) -> &mut Viewport {
1165 &mut self.viewport
1166 }
1167
1168 fn emit_intent(&mut self, _intent: Self::Intent) {}
1169}
1170
1171#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
1183pub struct RenderFrame {
1184 pub mode: SnapshotMode,
1185 pub cursor_row: u32,
1186 pub cursor_col: u32,
1187 pub cursor_shape: CursorShape,
1188 pub viewport_top: u32,
1189 pub line_count: u32,
1190}
1191
1192#[derive(Debug, Clone)]
1219
1220pub struct EditorSnapshot {
1221 pub version: u32,
1224 pub mode: SnapshotMode,
1226 pub cursor: (u32, u32),
1228 pub lines: Vec<String>,
1230 pub viewport_top: u32,
1232 pub registers: crate::Registers,
1236 pub marks: std::collections::BTreeMap<char, (u32, u32)>,
1243 pub global_marks: std::collections::BTreeMap<char, (u64, u32, u32)>,
1247}
1248
1249#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
1253
1254pub enum SnapshotMode {
1255 #[default]
1256 Normal,
1257 Insert,
1258 Visual,
1259 VisualLine,
1260 VisualBlock,
1261}
1262
1263impl EditorSnapshot {
1264 pub const VERSION: u32 = 5;
1285}
1286
1287#[derive(Debug, thiserror::Error)]
1291pub enum EngineError {
1292 #[error("regex compile error: {0}")]
1295 Regex(#[from] regex::Error),
1296
1297 #[error("invalid range: {0}")]
1299 InvalidRange(String),
1300
1301 #[error("ex parse: {0}")]
1303 Ex(String),
1304
1305 #[error("buffer is read-only")]
1307 ReadOnly,
1308
1309 #[error("position out of bounds: {0:?}")]
1311 OutOfBounds(Pos),
1312
1313 #[error("snapshot version mismatch: file={0}, expected={1}")]
1316 SnapshotVersion(u32, u32),
1317}
1318
1319pub(crate) mod sealed {
1320 pub trait Sealed {}
1330}
1331
1332pub trait Cursor: Send {
1338 fn cursor(&self) -> Pos;
1340 fn set_cursor(&mut self, pos: Pos);
1342 fn byte_offset(&self, pos: Pos) -> usize;
1344 fn pos_at_byte(&self, byte: usize) -> Pos;
1346}
1347
1348pub trait Query: Send {
1350 fn line_count(&self) -> u32;
1352 fn line(&self, idx: u32) -> String;
1355 fn len_bytes(&self) -> usize;
1357 fn slice(&self, range: core::ops::Range<Pos>) -> std::borrow::Cow<'_, str>;
1362 fn dirty_gen(&self) -> u64 {
1376 0
1377 }
1378
1379 fn byte_of_row(&self, row: usize) -> usize {
1390 let n = self.line_count() as usize;
1391 let row = row.min(n);
1392 let mut acc = 0usize;
1393 for r in 0..row {
1394 acc += self.line(r as u32).len();
1395 if r + 1 < n {
1400 acc += 1;
1401 }
1402 }
1403 acc
1404 }
1405
1406 fn content_joined(&self) -> std::sync::Arc<String> {
1415 let n = self.line_count() as usize;
1416 let mut acc = String::with_capacity(self.len_bytes());
1417 for r in 0..n {
1418 if r > 0 {
1419 acc.push('\n');
1420 }
1421 acc.push_str(&self.line(r as u32));
1422 }
1423 std::sync::Arc::new(acc)
1424 }
1425
1426 fn line_bytes(&self, row: usize) -> usize {
1434 let n = self.line_count() as usize;
1435 if row >= n {
1436 return 0;
1437 }
1438 self.line(row as u32).len()
1439 }
1440
1441 fn rope(&self) -> ropey::Rope {
1450 ropey::Rope::from_str(&self.content_joined())
1451 }
1452}
1453
1454pub trait BufferEdit: Send {
1458 fn insert_at(&mut self, pos: Pos, text: &str);
1461 fn delete_range(&mut self, range: core::ops::Range<Pos>);
1463 fn replace_range(&mut self, range: core::ops::Range<Pos>, replacement: &str);
1465 fn replace_all(&mut self, text: &str) {
1473 self.replace_range(
1474 Pos::ORIGIN..Pos {
1475 line: u32::MAX,
1476 col: u32::MAX,
1477 },
1478 text,
1479 );
1480 }
1481}
1482
1483pub trait Search: Send {
1486 fn find_next(&self, from: Pos, pat: ®ex::Regex) -> Option<core::ops::Range<Pos>>;
1488 fn find_prev(&self, from: Pos, pat: ®ex::Regex) -> Option<core::ops::Range<Pos>>;
1490}
1491
1492pub trait View: Cursor + Query + BufferEdit + Search + sealed::Sealed + Send {}
1500
1501pub trait FoldProvider: Send {
1522 fn next_visible_row(&self, row: usize, row_count: usize) -> Option<usize>;
1525 fn prev_visible_row(&self, row: usize) -> Option<usize>;
1527 fn is_row_hidden(&self, row: usize) -> bool;
1529 fn fold_at_row(&self, row: usize) -> Option<(usize, usize, bool)>;
1533
1534 fn apply(&mut self, op: FoldOp) {
1544 let _ = op;
1545 }
1546
1547 fn invalidate_range(&mut self, start_row: usize, end_row: usize) {
1552 self.apply(FoldOp::Invalidate { start_row, end_row });
1553 }
1554}
1555
1556#[derive(Debug, Default, Clone, Copy)]
1559pub struct NoopFoldProvider;
1560
1561impl FoldProvider for NoopFoldProvider {
1562 fn next_visible_row(&self, row: usize, row_count: usize) -> Option<usize> {
1563 let last = row_count.saturating_sub(1);
1564 if last == 0 && row == 0 {
1565 return None;
1566 }
1567 let r = row.checked_add(1)?;
1568 (r <= last).then_some(r)
1569 }
1570
1571 fn prev_visible_row(&self, row: usize) -> Option<usize> {
1572 row.checked_sub(1)
1573 }
1574
1575 fn is_row_hidden(&self, _row: usize) -> bool {
1576 false
1577 }
1578
1579 fn fold_at_row(&self, _row: usize) -> Option<(usize, usize, bool)> {
1580 None
1581 }
1582}
1583
1584#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1586pub enum InsertDir {
1587 Left,
1588 Right,
1589 Up,
1590 Down,
1591}
1592
1593#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1596pub enum ScrollDir {
1597 Down,
1599 Up,
1601}
1602
1603pub const SEARCH_HISTORY_MAX: usize = 100;
1604pub const CHANGE_LIST_MAX: usize = 100;
1605
1606pub const JUMPLIST_MAX: usize = 100;
1608
1609#[cfg(test)]
1610mod tests {
1611 use super::*;
1612
1613 #[test]
1614 fn caret_is_empty() {
1615 let sel = Selection::caret(Pos::new(2, 4));
1616 assert!(sel.is_empty());
1617 assert_eq!(sel.anchor, sel.head);
1618 }
1619
1620 #[test]
1621 fn selection_set_default_has_one_caret() {
1622 let set = SelectionSet::default();
1623 assert_eq!(set.items.len(), 1);
1624 assert_eq!(set.primary, 0);
1625 assert_eq!(set.primary().anchor, Pos::ORIGIN);
1626 }
1627
1628 #[test]
1629 fn edit_constructors() {
1630 let p = Pos::new(0, 5);
1631 assert_eq!(Edit::insert(p, "x").range, p..p);
1632 assert!(Edit::insert(p, "x").replacement == "x");
1633 assert!(Edit::delete(p..p).replacement.is_empty());
1634 }
1635
1636 #[test]
1637 fn attrs_flags() {
1638 let a = Attrs::BOLD | Attrs::UNDERLINE;
1639 assert!(a.contains(Attrs::BOLD));
1640 assert!(!a.contains(Attrs::ITALIC));
1641 }
1642
1643 #[test]
1644 fn options_set_get_roundtrip() {
1645 let mut o = Options::default();
1646 o.set_by_name("tabstop", OptionValue::Int(4)).unwrap();
1647 assert!(matches!(o.get_by_name("ts"), Some(OptionValue::Int(4))));
1648 o.set_by_name("expandtab", OptionValue::Bool(true)).unwrap();
1649 assert!(matches!(o.get_by_name("et"), Some(OptionValue::Bool(true))));
1650 o.set_by_name("iskeyword", OptionValue::String("a-z".into()))
1651 .unwrap();
1652 match o.get_by_name("iskeyword") {
1653 Some(OptionValue::String(s)) => assert_eq!(s, "a-z"),
1654 other => panic!("expected String, got {other:?}"),
1655 }
1656 }
1657
1658 #[test]
1659 fn options_unknown_name_errors_on_set() {
1660 let mut o = Options::default();
1661 assert!(matches!(
1662 o.set_by_name("frobnicate", OptionValue::Int(1)),
1663 Err(EngineError::Ex(_))
1664 ));
1665 assert!(o.get_by_name("frobnicate").is_none());
1666 }
1667
1668 #[test]
1678 fn set_by_name_rejects_makeprg_and_errorformat() {
1679 let mut o = Options::default();
1680 assert!(
1681 matches!(
1682 o.set_by_name("makeprg", OptionValue::String("rm -rf /".into())),
1683 Err(EngineError::Ex(_))
1684 ),
1685 "`makeprg` must never be a settable option — a modeline must \
1686 never be able to smuggle an arbitrary shell command into `:make`"
1687 );
1688 assert!(o.get_by_name("makeprg").is_none());
1689 assert!(
1690 matches!(
1691 o.set_by_name("errorformat", OptionValue::String("%f:%l:%m".into())),
1692 Err(EngineError::Ex(_))
1693 ),
1694 "`errorformat` rides the same `:make`/`:grep` shell-out surface \
1695 as `makeprg` and must stay unsettable too"
1696 );
1697 assert!(o.get_by_name("errorformat").is_none());
1698 }
1699
1700 #[test]
1701 fn options_type_mismatch_errors() {
1702 let mut o = Options::default();
1703 assert!(matches!(
1704 o.set_by_name("tabstop", OptionValue::String("nope".into())),
1705 Err(EngineError::Ex(_))
1706 ));
1707 assert!(matches!(
1708 o.set_by_name("iskeyword", OptionValue::Int(7)),
1709 Err(EngineError::Ex(_))
1710 ));
1711 }
1712
1713 #[test]
1716 fn default_options_ignorecase_and_smartcase_are_true() {
1717 let o = Options::default();
1718 assert!(o.ignorecase, "ignorecase must default to true");
1719 assert!(o.smartcase, "smartcase must default to true");
1720 }
1721
1722 #[test]
1723 fn options_int_to_bool_coercion() {
1724 let mut o = Options::default();
1727 o.set_by_name("ignorecase", OptionValue::Int(1)).unwrap();
1728 assert!(matches!(o.get_by_name("ic"), Some(OptionValue::Bool(true))));
1729 o.set_by_name("ignorecase", OptionValue::Int(0)).unwrap();
1730 assert!(matches!(
1731 o.get_by_name("ic"),
1732 Some(OptionValue::Bool(false))
1733 ));
1734 }
1735
1736 #[test]
1737 fn options_wrap_linebreak_roundtrip() {
1738 let mut o = Options::default();
1739 assert_eq!(o.wrap, WrapMode::None);
1740 o.set_by_name("wrap", OptionValue::Bool(true)).unwrap();
1741 assert_eq!(o.wrap, WrapMode::Char);
1742 o.set_by_name("linebreak", OptionValue::Bool(true)).unwrap();
1743 assert_eq!(o.wrap, WrapMode::Word);
1744 assert!(matches!(
1745 o.get_by_name("wrap"),
1746 Some(OptionValue::Bool(true))
1747 ));
1748 assert!(matches!(
1749 o.get_by_name("lbr"),
1750 Some(OptionValue::Bool(true))
1751 ));
1752 o.set_by_name("linebreak", OptionValue::Bool(false))
1753 .unwrap();
1754 assert_eq!(o.wrap, WrapMode::Char);
1755 o.set_by_name("wrap", OptionValue::Bool(false)).unwrap();
1756 assert_eq!(o.wrap, WrapMode::None);
1757 }
1758
1759 #[test]
1760 fn options_default_modern() {
1761 let o = Options::default();
1764 assert_eq!(o.tabstop, 4);
1765 assert_eq!(o.shiftwidth, 4);
1766 assert_eq!(o.softtabstop, 4);
1767 assert!(o.expandtab);
1768 assert!(o.hlsearch);
1769 assert!(o.wrapscan);
1770 assert!(o.smartindent);
1771 assert_eq!(o.timeout_len, core::time::Duration::from_millis(1000));
1772 }
1773
1774 #[test]
1775 fn editor_snapshot_version_const() {
1776 assert_eq!(EditorSnapshot::VERSION, 5);
1777 }
1778
1779 #[test]
1780 fn editor_snapshot_default_shape() {
1781 let s = EditorSnapshot {
1782 version: EditorSnapshot::VERSION,
1783 mode: SnapshotMode::Normal,
1784 cursor: (0, 0),
1785 lines: vec!["hello".to_string()],
1786 viewport_top: 0,
1787 registers: crate::Registers::default(),
1788 marks: Default::default(),
1789 global_marks: Default::default(),
1790 };
1791 assert_eq!(s.cursor, (0, 0));
1792 assert_eq!(s.lines.len(), 1);
1793 }
1794
1795 #[test]
1796 fn engine_error_display() {
1797 let e = EngineError::ReadOnly;
1798 assert_eq!(e.to_string(), "buffer is read-only");
1799 let e = EngineError::OutOfBounds(Pos::new(3, 7));
1800 assert!(e.to_string().contains("out of bounds"));
1801 }
1802
1803 #[test]
1806 fn options_cursorline_roundtrip() {
1807 let mut o = Options::default();
1808 assert!(!o.cursorline, "cursorline defaults to false (vim parity)");
1809 o.set_by_name("cursorline", OptionValue::Bool(true))
1810 .unwrap();
1811 assert!(matches!(
1812 o.get_by_name("cul"),
1813 Some(OptionValue::Bool(true))
1814 ));
1815 o.set_by_name("cul", OptionValue::Bool(false)).unwrap();
1816 assert!(matches!(
1817 o.get_by_name("cursorline"),
1818 Some(OptionValue::Bool(false))
1819 ));
1820 }
1821
1822 #[test]
1823 fn options_cursorcolumn_roundtrip() {
1824 let mut o = Options::default();
1825 assert!(!o.cursorcolumn, "cursorcolumn defaults to false");
1826 o.set_by_name("cuc", OptionValue::Bool(true)).unwrap();
1827 assert!(matches!(
1828 o.get_by_name("cursorcolumn"),
1829 Some(OptionValue::Bool(true))
1830 ));
1831 }
1832
1833 #[test]
1834 fn options_signcolumn_roundtrip() {
1835 let mut o = Options::default();
1836 assert_eq!(
1837 o.signcolumn,
1838 SignColumnMode::Auto,
1839 "signcolumn defaults to auto"
1840 );
1841 o.set_by_name("signcolumn", OptionValue::String("yes".into()))
1842 .unwrap();
1843 assert_eq!(o.signcolumn, SignColumnMode::Yes);
1844 assert_eq!(
1845 o.get_by_name("scl"),
1846 Some(OptionValue::String("yes".into()))
1847 );
1848 o.set_by_name("scl", OptionValue::String("no".into()))
1849 .unwrap();
1850 assert_eq!(o.signcolumn, SignColumnMode::No);
1851 o.set_by_name("scl", OptionValue::String("auto".into()))
1852 .unwrap();
1853 assert_eq!(o.signcolumn, SignColumnMode::Auto);
1854 }
1855
1856 #[test]
1857 fn options_signcolumn_rejects_invalid() {
1858 let mut o = Options::default();
1859 assert!(matches!(
1860 o.set_by_name("signcolumn", OptionValue::String("maybe".into())),
1861 Err(EngineError::Ex(_))
1862 ));
1863 assert!(matches!(
1865 o.set_by_name("signcolumn", OptionValue::Bool(true)),
1866 Err(EngineError::Ex(_))
1867 ));
1868 }
1869
1870 #[test]
1871 fn options_foldcolumn_roundtrip() {
1872 let mut o = Options::default();
1873 assert_eq!(o.foldcolumn, 0, "foldcolumn defaults to 0");
1874 o.set_by_name("fdc", OptionValue::Int(3)).unwrap();
1875 assert_eq!(o.foldcolumn, 3);
1876 assert_eq!(o.get_by_name("foldcolumn"), Some(OptionValue::Int(3)));
1877 }
1878
1879 #[test]
1880 fn options_foldcolumn_rejects_out_of_range() {
1881 let mut o = Options::default();
1882 assert!(matches!(
1883 o.set_by_name("foldcolumn", OptionValue::Int(13)),
1884 Err(EngineError::Ex(_))
1885 ));
1886 assert!(matches!(
1887 o.set_by_name("foldcolumn", OptionValue::Int(-1)),
1888 Err(EngineError::Ex(_))
1889 ));
1890 }
1891
1892 #[test]
1893 fn options_colorcolumn_roundtrip() {
1894 let mut o = Options::default();
1895 assert_eq!(o.colorcolumn, "", "colorcolumn defaults to empty string");
1896 o.set_by_name("cc", OptionValue::String("80,120".into()))
1897 .unwrap();
1898 assert_eq!(
1899 o.get_by_name("colorcolumn"),
1900 Some(OptionValue::String("80,120".into()))
1901 );
1902 o.set_by_name("colorcolumn", OptionValue::String(String::new()))
1903 .unwrap();
1904 assert_eq!(
1905 o.get_by_name("cc"),
1906 Some(OptionValue::String(String::new()))
1907 );
1908 }
1909
1910 #[test]
1911 fn options_cursorline_alias_cul() {
1912 let mut o = Options::default();
1913 o.set_by_name("cul", OptionValue::Bool(true)).unwrap();
1915 assert!(o.cursorline);
1916 o.set_by_name("cul", OptionValue::Bool(false)).unwrap();
1918 assert!(!o.cursorline);
1919 }
1920
1921 #[test]
1922 fn sign_column_mode_default_is_auto() {
1923 assert_eq!(SignColumnMode::default(), SignColumnMode::Auto);
1924 }
1925
1926 #[test]
1927 fn options_scrolloff_default_and_set() {
1928 let mut o = Options::default();
1929 assert_eq!(o.scrolloff, 5, "scrolloff defaults to 5");
1930 o.set_by_name("scrolloff", OptionValue::Int(0)).unwrap();
1931 assert_eq!(o.scrolloff, 0);
1932 o.set_by_name("scrolloff", OptionValue::Int(999)).unwrap();
1933 assert_eq!(o.scrolloff, 999);
1934 assert_eq!(o.get_by_name("scrolloff"), Some(OptionValue::Int(999)));
1935 }
1936
1937 #[test]
1938 fn options_sidescrolloff_default_and_set() {
1939 let mut o = Options::default();
1940 assert_eq!(o.sidescrolloff, 0, "sidescrolloff defaults to 0");
1941 o.set_by_name("sidescrolloff", OptionValue::Int(5)).unwrap();
1942 assert_eq!(o.sidescrolloff, 5);
1943 assert_eq!(o.get_by_name("sidescrolloff"), Some(OptionValue::Int(5)));
1944 }
1945
1946 #[test]
1947 fn options_alias_so_siso() {
1948 let mut o = Options::default();
1949 o.set_by_name("so", OptionValue::Int(3)).unwrap();
1951 assert_eq!(o.scrolloff, 3);
1952 assert_eq!(o.get_by_name("so"), Some(OptionValue::Int(3)));
1953 o.set_by_name("siso", OptionValue::Int(2)).unwrap();
1955 assert_eq!(o.sidescrolloff, 2);
1956 assert_eq!(o.get_by_name("siso"), Some(OptionValue::Int(2)));
1957 }
1958
1959 #[test]
1962 fn options_list_default_false_and_set() {
1963 let mut o = Options::default();
1964 assert!(!o.list, "list default is false");
1965 o.set_by_name("list", OptionValue::Bool(true)).unwrap();
1966 assert!(o.list);
1967 assert_eq!(o.get_by_name("list"), Some(OptionValue::Bool(true)));
1968 o.set_by_name("list", OptionValue::Bool(false)).unwrap();
1969 assert!(!o.list);
1970 }
1971
1972 #[test]
1973 fn options_listchars_default_matches_vim() {
1974 let o = Options::default();
1975 let lc = &o.listchars;
1976 assert_eq!(lc.tab_lead, '^');
1977 assert_eq!(lc.tab_fill, Some('I'));
1978 assert_eq!(lc.eol, Some('$'));
1979 assert_eq!(lc.space, None);
1980 assert_eq!(lc.trail, None);
1981 assert_eq!(lc.nbsp, None);
1982 }
1983
1984 #[test]
1985 fn options_listchars_set_and_get() {
1986 let mut o = Options::default();
1987 o.set_by_name("listchars", OptionValue::String("tab:>-,eol:$".to_string()))
1988 .unwrap();
1989 assert_eq!(o.listchars.tab_lead, '>');
1990 assert_eq!(o.listchars.tab_fill, Some('-'));
1991 assert_eq!(o.listchars.eol, Some('$'));
1992 }
1993
1994 #[test]
1995 fn options_lcs_alias_sets_listchars() {
1996 let mut o = Options::default();
1997 o.set_by_name("lcs", OptionValue::String("tab:>-,trail:~".to_string()))
1998 .unwrap();
1999 assert_eq!(o.listchars.tab_lead, '>');
2000 assert_eq!(o.listchars.trail, Some('~'));
2001 }
2002
2003 #[test]
2004 fn options_listchars_get_by_name_returns_string() {
2005 let o = Options::default();
2006 match o.get_by_name("listchars") {
2007 Some(OptionValue::String(s)) => {
2008 assert!(s.contains("tab:"), "canonical string should contain tab:");
2009 }
2010 other => panic!("expected String, got {other:?}"),
2011 }
2012 }
2013
2014 #[test]
2015 fn options_listchars_invalid_value_returns_err() {
2016 let mut o = Options::default();
2017 assert!(
2018 o.set_by_name("listchars", OptionValue::String("bogus:x".to_string()))
2019 .is_err()
2020 );
2021 }
2022
2023 #[test]
2026 fn indent_guides_default_true() {
2027 assert!(
2028 Options::default().indent_guides,
2029 "indent_guides must default to true"
2030 );
2031 }
2032
2033 #[test]
2034 fn options_indent_guides_set_and_get() {
2035 let mut opts = Options::default();
2036 opts.set_by_name("indent_guides", OptionValue::Bool(false))
2038 .unwrap();
2039 assert!(!opts.indent_guides);
2040 opts.set_by_name("ig", OptionValue::Bool(true)).unwrap();
2042 assert!(opts.indent_guides);
2043 assert_eq!(opts.get_by_name("ig"), Some(OptionValue::Bool(true)));
2045 assert_eq!(
2046 opts.get_by_name("indent_guides"),
2047 Some(OptionValue::Bool(true))
2048 );
2049 }
2050
2051 #[test]
2052 fn options_indent_guide_char_set_and_get() {
2053 let mut opts = Options::default();
2054 opts.set_by_name("indent_guide_char", OptionValue::String(":".to_string()))
2055 .unwrap();
2056 assert_eq!(opts.indent_guide_char, ':');
2057 opts.set_by_name("igc", OptionValue::String("┊".to_string()))
2059 .unwrap();
2060 assert_eq!(opts.indent_guide_char, '┊');
2061 assert_eq!(
2063 opts.get_by_name("igc"),
2064 Some(OptionValue::String("┊".to_string()))
2065 );
2066 assert_eq!(
2067 opts.get_by_name("indent_guide_char"),
2068 Some(OptionValue::String("┊".to_string()))
2069 );
2070 }
2071
2072 #[test]
2073 fn options_indent_guide_char_rejects_multi_char() {
2074 let mut opts = Options::default();
2075 assert!(
2076 opts.set_by_name("indent_guide_char", OptionValue::String("ab".to_string()))
2077 .is_err(),
2078 "multi-char value must be rejected"
2079 );
2080 }
2081
2082 #[test]
2083 fn options_indent_guide_char_rejects_empty() {
2084 let mut opts = Options::default();
2085 assert!(
2086 opts.set_by_name("indent_guide_char", OptionValue::String(String::new()))
2087 .is_err(),
2088 "empty string must be rejected"
2089 );
2090 }
2091
2092 #[test]
2095 fn colorizer_default_true() {
2096 assert!(
2097 Options::default().colorizer,
2098 "colorizer must default to true"
2099 );
2100 }
2101
2102 #[test]
2103 fn colorizer_filetypes_includes_css() {
2104 let o = Options::default();
2105 assert!(
2106 o.colorizer_filetypes.iter().any(|f| f == "css"),
2107 "default colorizer_filetypes must include 'css'"
2108 );
2109 }
2110
2111 #[test]
2112 fn options_colorizer_set_and_get() {
2113 let mut o = Options::default();
2114 o.set_by_name("colorizer", OptionValue::Bool(false))
2115 .unwrap();
2116 assert_eq!(o.get_by_name("colorizer"), Some(OptionValue::Bool(false)));
2117 o.set_by_name("clz", OptionValue::Bool(true)).unwrap();
2118 assert_eq!(o.get_by_name("clz"), Some(OptionValue::Bool(true)));
2119 }
2120
2121 #[test]
2122 fn options_colorizer_filetypes_set_and_get() {
2123 let mut o = Options::default();
2124 o.set_by_name(
2125 "colorizer_filetypes",
2126 OptionValue::String("css,scss,toml".into()),
2127 )
2128 .unwrap();
2129 assert_eq!(o.colorizer_filetypes, vec!["css", "scss", "toml"]);
2130 assert_eq!(
2131 o.get_by_name("clzft"),
2132 Some(OptionValue::String("css,scss,toml".into()))
2133 );
2134 }
2135
2136 #[test]
2139 fn format_on_save_default_true() {
2140 let o = Options::default();
2141 assert!(o.format_on_save, "format_on_save must default to true");
2142 }
2143
2144 #[test]
2145 fn trim_trailing_whitespace_default_false() {
2146 let o = Options::default();
2147 assert!(
2148 !o.trim_trailing_whitespace,
2149 "trim_trailing_whitespace must default to false"
2150 );
2151 }
2152
2153 #[test]
2154 fn options_fos_alias_sets_format_on_save() {
2155 let mut o = Options::default();
2156 o.set_by_name("fos", OptionValue::Bool(true)).unwrap();
2157 assert!(o.format_on_save, "fos alias must set format_on_save");
2158 assert_eq!(
2159 o.get_by_name("fos"),
2160 Some(OptionValue::Bool(true)),
2161 "get_by_name(fos) must reflect the new value"
2162 );
2163 assert_eq!(
2164 o.get_by_name("format_on_save"),
2165 Some(OptionValue::Bool(true)),
2166 "get_by_name(format_on_save) must also reflect the new value"
2167 );
2168 }
2169
2170 #[test]
2171 fn options_tts_alias_sets_trim_trailing_whitespace() {
2172 let mut o = Options::default();
2173 o.set_by_name("tts", OptionValue::Bool(true)).unwrap();
2174 assert!(
2175 o.trim_trailing_whitespace,
2176 "tts alias must set trim_trailing_whitespace"
2177 );
2178 assert_eq!(
2179 o.get_by_name("tts"),
2180 Some(OptionValue::Bool(true)),
2181 "get_by_name(tts) must reflect the new value"
2182 );
2183 assert_eq!(
2184 o.get_by_name("trim_trailing_whitespace"),
2185 Some(OptionValue::Bool(true)),
2186 "get_by_name(trim_trailing_whitespace) must also reflect the new value"
2187 );
2188 }
2189
2190 #[test]
2193 fn rainbow_brackets_default_true() {
2194 let o = Options::default();
2195 assert!(o.rainbow_brackets, "rainbow_brackets must default to true");
2196 }
2197
2198 #[test]
2199 fn options_rb_alias_sets_rainbow_brackets() {
2200 let mut o = Options::default();
2201 o.set_by_name("rb", OptionValue::Bool(false)).unwrap();
2202 assert!(
2203 !o.rainbow_brackets,
2204 "rb alias must set rainbow_brackets to false"
2205 );
2206 assert_eq!(
2207 o.get_by_name("rb"),
2208 Some(OptionValue::Bool(false)),
2209 "get_by_name(rb) must reflect the new value"
2210 );
2211 assert_eq!(
2212 o.get_by_name("rainbow_brackets"),
2213 Some(OptionValue::Bool(false)),
2214 "get_by_name(rainbow_brackets) must also reflect the new value"
2215 );
2216 }
2217
2218 #[test]
2219 fn autoreload_default_true() {
2220 assert!(
2221 Options::default().autoreload,
2222 "autoreload must default true"
2223 );
2224 }
2225
2226 #[test]
2227 fn options_ar_alias_sets_autoreload() {
2228 let mut o = Options::default();
2229 o.set_by_name("ar", OptionValue::Bool(false)).unwrap();
2230 assert!(!o.autoreload, "ar alias must set autoreload");
2231 assert_eq!(o.get_by_name("autoreload"), Some(OptionValue::Bool(false)));
2232 }
2233
2234 #[test]
2237 fn updatetime_default_4000() {
2238 let o = Options::default();
2239 assert_eq!(o.updatetime, 4000, "updatetime must default to 4000 ms");
2240 assert_eq!(
2241 o.get_by_name("updatetime"),
2242 Some(OptionValue::Int(4000)),
2243 "get_by_name(updatetime) must return Int(4000)"
2244 );
2245 }
2246
2247 #[test]
2248 fn options_ut_alias_sets_updatetime() {
2249 let mut o = Options::default();
2250 o.set_by_name("ut", OptionValue::Int(1000)).unwrap();
2251 assert_eq!(o.updatetime, 1000, "ut alias must set updatetime");
2252 assert_eq!(
2253 o.get_by_name("ut"),
2254 Some(OptionValue::Int(1000)),
2255 "get_by_name(ut) must reflect the new value"
2256 );
2257 assert_eq!(
2258 o.get_by_name("updatetime"),
2259 Some(OptionValue::Int(1000)),
2260 "get_by_name(updatetime) must also reflect the new value"
2261 );
2262 }
2263
2264 #[test]
2267 fn matchparen_default_true() {
2268 let o = Options::default();
2269 assert!(o.matchparen, "matchparen must default to true");
2270 assert_eq!(
2271 o.get_by_name("matchparen"),
2272 Some(OptionValue::Bool(true)),
2273 "get_by_name(matchparen) must return Bool(true)"
2274 );
2275 }
2276
2277 #[test]
2278 fn options_matchparen_set_and_get() {
2279 let mut o = Options::default();
2280 o.set_by_name("matchparen", OptionValue::Bool(false))
2281 .unwrap();
2282 assert!(!o.matchparen, "matchparen must be false after set");
2283 assert_eq!(
2284 o.get_by_name("matchparen"),
2285 Some(OptionValue::Bool(false)),
2286 "get_by_name(matchparen) must reflect false"
2287 );
2288 o.set_by_name("mps", OptionValue::Bool(true)).unwrap();
2290 assert!(o.matchparen, "mps alias must set matchparen to true");
2291 assert_eq!(
2292 o.get_by_name("mps"),
2293 Some(OptionValue::Bool(true)),
2294 "get_by_name(mps) must reflect true"
2295 );
2296 }
2297
2298 #[test]
2303 fn fixendofline_default_true() {
2304 let o = Options::default();
2305 assert!(o.fixendofline, "fixendofline must default to true");
2306 assert_eq!(
2307 o.get_by_name("fixendofline"),
2308 Some(OptionValue::Bool(true)),
2309 "get_by_name(fixendofline) must return Bool(true)"
2310 );
2311 }
2312
2313 #[test]
2314 fn options_fixendofline_set_and_get() {
2315 let mut o = Options::default();
2316 o.set_by_name("fixendofline", OptionValue::Bool(false))
2317 .unwrap();
2318 assert!(!o.fixendofline, "fixendofline must be false after set");
2319 assert_eq!(
2320 o.get_by_name("fixendofline"),
2321 Some(OptionValue::Bool(false)),
2322 "get_by_name(fixendofline) must reflect false"
2323 );
2324 o.set_by_name("fixeol", OptionValue::Bool(true)).unwrap();
2326 assert!(o.fixendofline, "fixeol alias must set fixendofline to true");
2327 assert_eq!(
2328 o.get_by_name("fixeol"),
2329 Some(OptionValue::Bool(true)),
2330 "get_by_name(fixeol) must reflect true"
2331 );
2332 }
2333
2334 #[test]
2337 fn foldmethod_default_expr() {
2338 let o = Options::default();
2339 assert_eq!(
2340 o.foldmethod,
2341 FoldMethod::Expr,
2342 "foldmethod must default to Expr (tree-sitter)"
2343 );
2344 assert_eq!(
2345 o.get_by_name("foldmethod"),
2346 Some(OptionValue::String("expr".into())),
2347 "get_by_name(foldmethod) must return \"expr\""
2348 );
2349 }
2350
2351 #[test]
2352 fn foldmethod_fdm_alias_roundtrip() {
2353 let mut o = Options::default();
2354 o.set_by_name("fdm", OptionValue::String("manual".into()))
2355 .unwrap();
2356 assert_eq!(o.foldmethod, FoldMethod::Manual);
2357 assert_eq!(
2358 o.get_by_name("fdm"),
2359 Some(OptionValue::String("manual".into()))
2360 );
2361 o.set_by_name("foldmethod", OptionValue::String("expr".into()))
2362 .unwrap();
2363 assert_eq!(o.foldmethod, FoldMethod::Expr);
2364 o.set_by_name("foldmethod", OptionValue::String("marker".into()))
2365 .unwrap();
2366 assert_eq!(o.foldmethod, FoldMethod::Marker);
2367 o.set_by_name("foldmethod", OptionValue::String("syntax".into()))
2369 .unwrap();
2370 assert_eq!(o.foldmethod, FoldMethod::Expr);
2371 }
2372
2373 #[test]
2374 fn foldmethod_rejects_invalid_value() {
2375 let mut o = Options::default();
2376 let err = o
2377 .set_by_name("foldmethod", OptionValue::String("bogus".into()))
2378 .unwrap_err();
2379 assert!(
2380 err.to_string().contains("must be"),
2381 "expected error about valid values, got: {err}"
2382 );
2383 }
2384
2385 #[test]
2386 fn foldenable_default_true() {
2387 let o = Options::default();
2388 assert!(o.foldenable, "foldenable must default to true");
2389 assert_eq!(
2390 o.get_by_name("foldenable"),
2391 Some(OptionValue::Bool(true)),
2392 "get_by_name(foldenable) must return Bool(true)"
2393 );
2394 }
2395
2396 #[test]
2397 fn foldenable_fen_alias_roundtrip() {
2398 let mut o = Options::default();
2399 o.set_by_name("fen", OptionValue::Bool(false)).unwrap();
2400 assert!(!o.foldenable, "fen alias must disable foldenable");
2401 assert_eq!(o.get_by_name("fen"), Some(OptionValue::Bool(false)));
2402 o.set_by_name("foldenable", OptionValue::Bool(true))
2403 .unwrap();
2404 assert!(o.foldenable);
2405 }
2406
2407 #[test]
2408 fn foldlevelstart_default_99() {
2409 let o = Options::default();
2410 assert_eq!(o.foldlevelstart, 99, "foldlevelstart must default to 99");
2411 assert_eq!(
2412 o.get_by_name("foldlevelstart"),
2413 Some(OptionValue::Int(99)),
2414 "get_by_name(foldlevelstart) must return Int(99)"
2415 );
2416 }
2417
2418 #[test]
2419 fn foldlevelstart_fls_alias_roundtrip() {
2420 let mut o = Options::default();
2421 o.set_by_name("fls", OptionValue::Int(0)).unwrap();
2422 assert_eq!(
2423 o.foldlevelstart, 0,
2424 "fls alias must set foldlevelstart to 0"
2425 );
2426 assert_eq!(o.get_by_name("fls"), Some(OptionValue::Int(0)));
2427 o.set_by_name("foldlevelstart", OptionValue::Int(5))
2428 .unwrap();
2429 assert_eq!(o.foldlevelstart, 5);
2430 }
2431}