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,
246 pub cursorcolumn: bool,
251 pub signcolumn: SignColumnMode,
254 pub foldcolumn: u32,
257 pub foldmethod: FoldMethod,
262 pub foldenable: bool,
266 pub foldlevelstart: u32,
269 pub foldmarker: String,
274 pub colorcolumn: String,
277 pub formatoptions: String,
282 pub filetype: String,
285 pub scrolloff: usize,
290 pub sidescrolloff: usize,
294 pub modeline: bool,
299 pub modelines: u32,
302 pub autoreload: bool,
307 pub motion_sneak: bool,
313 pub list: bool,
316 pub listchars: ListChars,
320 pub indent_guides: bool,
324 pub indent_guide_char: char,
327 pub colorizer: bool,
331 pub colorizer_filetypes: Vec<String>,
335 pub format_on_save: bool,
341 pub trim_trailing_whitespace: bool,
345 pub rainbow_brackets: bool,
348 pub updatetime: u32,
352 pub matchparen: bool,
358 pub fixendofline: bool,
368}
369
370pub use hjkl_buffer::ListChars;
375
376#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
379
380pub enum FoldMethod {
381 Manual,
383 #[default]
388 Expr,
389 Marker,
392}
393
394#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
397
398pub enum SignColumnMode {
399 No,
401 Yes,
403 #[default]
405 Auto,
406}
407
408#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
412
413pub enum DiagInlineMode {
414 Off,
416 Current,
418 #[default]
420 All,
421}
422
423#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
427
428pub enum WrapMode {
429 #[default]
432 None,
433 Char,
436 Word,
440}
441
442#[derive(Debug, Clone, PartialEq, Eq)]
448pub enum OptionValue {
449 Bool(bool),
450 Int(i64),
451 String(String),
452}
453
454impl Default for Options {
455 fn default() -> Self {
456 Self {
457 tabstop: 4,
458 shiftwidth: 4,
459 expandtab: true,
460 softtabstop: 4,
461 iskeyword: "@,48-57,_,192-255".to_string(),
462 ignorecase: true,
463 smartcase: true,
464 hlsearch: true,
465 incsearch: true,
466 wrapscan: true,
467 autoindent: true,
468 smartindent: true,
469 timeout_len: core::time::Duration::from_millis(1000),
470 undo_levels: 1000,
471 undo_break_on_motion: true,
472 readonly: false,
473 modifiable: true,
474 wrap: WrapMode::None,
475 textwidth: 79,
476 number: true,
477 relativenumber: false,
478 numberwidth: 4,
479 cursorline: true,
480 cursorcolumn: false,
481 signcolumn: SignColumnMode::Auto,
482 foldcolumn: 0,
483 foldmethod: FoldMethod::Expr,
484 foldenable: true,
485 foldlevelstart: 99,
486 foldmarker: "{{{,}}}".to_string(),
487 colorcolumn: String::new(),
488 formatoptions: "ro".to_string(),
489 filetype: String::new(),
490 scrolloff: 5,
491 sidescrolloff: 0,
492 modeline: true,
493 modelines: 5,
494 autoreload: true,
495 motion_sneak: true,
496 list: false,
497 listchars: ListChars::default(),
498 indent_guides: true,
499 indent_guide_char: '│',
500 colorizer: true,
501 colorizer_filetypes: vec![
502 "css".to_string(),
503 "scss".to_string(),
504 "sass".to_string(),
505 "less".to_string(),
506 "html".to_string(),
507 "vue".to_string(),
508 "svelte".to_string(),
509 "tailwindcss".to_string(),
510 "toml".to_string(),
511 "lua".to_string(),
512 "vim".to_string(),
513 ],
514 format_on_save: true,
515 trim_trailing_whitespace: false,
516 rainbow_brackets: true,
517 updatetime: 4000,
518 matchparen: true,
519 fixendofline: true,
520 }
521 }
522}
523
524impl Options {
525 pub fn set_by_name(&mut self, name: &str, val: OptionValue) -> Result<(), EngineError> {
532 macro_rules! set_bool {
533 ($field:ident) => {{
534 self.$field = match val {
535 OptionValue::Bool(b) => b,
536 OptionValue::Int(n) => n != 0,
537 other => {
538 return Err(EngineError::Ex(format!(
539 "option `{name}` expects bool, got {other:?}"
540 )));
541 }
542 };
543 Ok(())
544 }};
545 }
546 macro_rules! set_u32 {
547 ($field:ident) => {{
548 self.$field = match val {
549 OptionValue::Int(n) if n >= 0 && n <= u32::MAX as i64 => n as u32,
550 OptionValue::Int(n) => {
551 return Err(EngineError::Ex(format!(
552 "option `{name}` out of u32 range: {n}"
553 )));
554 }
555 other => {
556 return Err(EngineError::Ex(format!(
557 "option `{name}` expects int, got {other:?}"
558 )));
559 }
560 };
561 Ok(())
562 }};
563 }
564 macro_rules! set_string {
565 ($field:ident) => {{
566 self.$field = match val {
567 OptionValue::String(s) => s,
568 other => {
569 return Err(EngineError::Ex(format!(
570 "option `{name}` expects string, got {other:?}"
571 )));
572 }
573 };
574 Ok(())
575 }};
576 }
577 match name {
578 "tabstop" | "ts" => set_u32!(tabstop),
579 "shiftwidth" | "sw" => set_u32!(shiftwidth),
580 "softtabstop" | "sts" => set_u32!(softtabstop),
581 "textwidth" | "tw" => set_u32!(textwidth),
582 "expandtab" | "et" => set_bool!(expandtab),
583 "iskeyword" | "isk" => set_string!(iskeyword),
584 "ignorecase" | "ic" => set_bool!(ignorecase),
585 "smartcase" | "scs" => set_bool!(smartcase),
586 "hlsearch" | "hls" => set_bool!(hlsearch),
587 "incsearch" | "is" => set_bool!(incsearch),
588 "wrapscan" | "ws" => set_bool!(wrapscan),
589 "autoindent" | "ai" => set_bool!(autoindent),
590 "smartindent" | "si" => set_bool!(smartindent),
591 "timeoutlen" | "tm" => {
592 self.timeout_len = match val {
593 OptionValue::Int(n) if n >= 0 => core::time::Duration::from_millis(n as u64),
594 other => {
595 return Err(EngineError::Ex(format!(
596 "option `{name}` expects non-negative int (millis), got {other:?}"
597 )));
598 }
599 };
600 Ok(())
601 }
602 "undolevels" | "ul" => set_u32!(undo_levels),
603 "undobreak" => set_bool!(undo_break_on_motion),
604 "readonly" | "ro" => set_bool!(readonly),
605 "modifiable" | "ma" => set_bool!(modifiable),
606 "wrap" => {
607 let on = match val {
608 OptionValue::Bool(b) => b,
609 OptionValue::Int(n) => n != 0,
610 other => {
611 return Err(EngineError::Ex(format!(
612 "option `{name}` expects bool, got {other:?}"
613 )));
614 }
615 };
616 self.wrap = match (on, self.wrap) {
617 (false, _) => WrapMode::None,
618 (true, WrapMode::Word) => WrapMode::Word,
619 (true, _) => WrapMode::Char,
620 };
621 Ok(())
622 }
623 "linebreak" | "lbr" => {
624 let on = match val {
625 OptionValue::Bool(b) => b,
626 OptionValue::Int(n) => n != 0,
627 other => {
628 return Err(EngineError::Ex(format!(
629 "option `{name}` expects bool, got {other:?}"
630 )));
631 }
632 };
633 self.wrap = match (on, self.wrap) {
634 (true, _) => WrapMode::Word,
635 (false, WrapMode::Word) => WrapMode::Char,
636 (false, other) => other,
637 };
638 Ok(())
639 }
640 "number" | "nu" => set_bool!(number),
641 "relativenumber" | "rnu" => set_bool!(relativenumber),
642 "numberwidth" | "nuw" => {
643 self.numberwidth = match val {
644 OptionValue::Int(n) if (1..=20).contains(&n) => n as usize,
645 OptionValue::Int(n) => {
646 return Err(EngineError::Ex(format!(
647 "option `{name}` must be in range 1..=20, got {n}"
648 )));
649 }
650 other => {
651 return Err(EngineError::Ex(format!(
652 "option `{name}` expects int, got {other:?}"
653 )));
654 }
655 };
656 Ok(())
657 }
658 "cursorline" | "cul" => set_bool!(cursorline),
659 "cursorcolumn" | "cuc" => set_bool!(cursorcolumn),
660 "signcolumn" | "scl" => {
661 self.signcolumn = match val {
662 OptionValue::String(ref s) => match s.as_str() {
663 "yes" => SignColumnMode::Yes,
664 "no" => SignColumnMode::No,
665 "auto" => SignColumnMode::Auto,
666 other => {
667 return Err(EngineError::Ex(format!(
668 "option `{name}` must be `yes`, `no`, or `auto`, got {other:?}"
669 )));
670 }
671 },
672 other => {
673 return Err(EngineError::Ex(format!(
674 "option `{name}` expects string (yes/no/auto), got {other:?}"
675 )));
676 }
677 };
678 Ok(())
679 }
680 "foldcolumn" | "fdc" => {
681 self.foldcolumn = match val {
682 OptionValue::Int(n) if (0..=12).contains(&n) => n as u32,
683 OptionValue::Int(n) => {
684 return Err(EngineError::Ex(format!(
685 "option `{name}` must be in range 0..=12, got {n}"
686 )));
687 }
688 other => {
689 return Err(EngineError::Ex(format!(
690 "option `{name}` expects int (0-12), got {other:?}"
691 )));
692 }
693 };
694 Ok(())
695 }
696 "foldmethod" | "fdm" => {
697 self.foldmethod = match val {
698 OptionValue::String(ref s) => match s.as_str() {
699 "manual" => FoldMethod::Manual,
700 "expr" | "syntax" => FoldMethod::Expr,
701 "marker" => FoldMethod::Marker,
702 other => {
703 return Err(EngineError::Ex(format!(
704 "option `{name}` must be `manual`, `expr`, `syntax`, or `marker`, got `{other}`"
705 )));
706 }
707 },
708 other => {
709 return Err(EngineError::Ex(format!(
710 "option `{name}` expects string, got {other:?}"
711 )));
712 }
713 };
714 Ok(())
715 }
716 "foldenable" | "fen" => set_bool!(foldenable),
717 "foldlevelstart" | "fls" => set_u32!(foldlevelstart),
718 "colorcolumn" | "cc" => set_string!(colorcolumn),
719 "formatoptions" | "fo" => set_string!(formatoptions),
720 "filetype" | "ft" => set_string!(filetype),
721 "scrolloff" | "so" => {
722 self.scrolloff = match val {
723 OptionValue::Int(n) if n >= 0 => n as usize,
724 OptionValue::Int(n) => {
725 return Err(EngineError::Ex(format!(
726 "option `{name}` must be >= 0, got {n}"
727 )));
728 }
729 other => {
730 return Err(EngineError::Ex(format!(
731 "option `{name}` expects int, got {other:?}"
732 )));
733 }
734 };
735 Ok(())
736 }
737 "sidescrolloff" | "siso" => {
738 self.sidescrolloff = match val {
739 OptionValue::Int(n) if n >= 0 => n as usize,
740 OptionValue::Int(n) => {
741 return Err(EngineError::Ex(format!(
742 "option `{name}` must be >= 0, got {n}"
743 )));
744 }
745 other => {
746 return Err(EngineError::Ex(format!(
747 "option `{name}` expects int, got {other:?}"
748 )));
749 }
750 };
751 Ok(())
752 }
753 "modeline" | "ml" => set_bool!(modeline),
754 "autoreload" | "ar" => set_bool!(autoreload),
755 "modelines" | "mls" => set_u32!(modelines),
756 "motion_sneak" | "snk" => set_bool!(motion_sneak),
757 "list" => set_bool!(list),
758 "listchars" | "lcs" => {
759 let s = match val {
760 OptionValue::String(s) => s,
761 other => {
762 return Err(EngineError::Ex(format!(
763 "option `{name}` expects string, got {other:?}"
764 )));
765 }
766 };
767 self.listchars = ListChars::parse(&s).map_err(EngineError::Ex)?;
768 Ok(())
769 }
770 "indent_guides" | "ig" => set_bool!(indent_guides),
771 "colorizer" | "clz" => set_bool!(colorizer),
772 "colorizer_filetypes" | "clzft" => {
773 let s = match val {
774 OptionValue::String(s) => s,
775 other => {
776 return Err(EngineError::Ex(format!(
777 "option `{name}` expects string, got {other:?}"
778 )));
779 }
780 };
781 self.colorizer_filetypes = s
782 .split(',')
783 .map(|p| p.trim().to_string())
784 .filter(|p| !p.is_empty())
785 .collect();
786 Ok(())
787 }
788 "indent_guide_char" | "igc" => {
789 let s = match val {
790 OptionValue::String(s) => s,
791 other => {
792 return Err(EngineError::Ex(format!(
793 "option `{name}` expects a single-char string, got {other:?}"
794 )));
795 }
796 };
797 let mut chars = s.chars();
798 let (Some(ch), None) = (chars.next(), chars.next()) else {
799 return Err(EngineError::Ex(format!(
800 "option `{name}` expects exactly one character, got {s:?}"
801 )));
802 };
803 self.indent_guide_char = ch;
804 Ok(())
805 }
806 "format_on_save" | "fos" => set_bool!(format_on_save),
807 "trim_trailing_whitespace" | "tts" => set_bool!(trim_trailing_whitespace),
808 "rainbow_brackets" | "rb" => set_bool!(rainbow_brackets),
809 "updatetime" | "ut" => set_u32!(updatetime),
810 "matchparen" | "mps" => set_bool!(matchparen),
811 "fixendofline" | "fixeol" => set_bool!(fixendofline),
812 other => Err(EngineError::Ex(format!("unknown option `{other}`"))),
813 }
814 }
815
816 pub fn get_by_name(&self, name: &str) -> Option<OptionValue> {
818 Some(match name {
819 "tabstop" | "ts" => OptionValue::Int(self.tabstop as i64),
820 "shiftwidth" | "sw" => OptionValue::Int(self.shiftwidth as i64),
821 "softtabstop" | "sts" => OptionValue::Int(self.softtabstop as i64),
822 "textwidth" | "tw" => OptionValue::Int(self.textwidth as i64),
823 "expandtab" | "et" => OptionValue::Bool(self.expandtab),
824 "iskeyword" | "isk" => OptionValue::String(self.iskeyword.clone()),
825 "ignorecase" | "ic" => OptionValue::Bool(self.ignorecase),
826 "smartcase" | "scs" => OptionValue::Bool(self.smartcase),
827 "hlsearch" | "hls" => OptionValue::Bool(self.hlsearch),
828 "incsearch" | "is" => OptionValue::Bool(self.incsearch),
829 "wrapscan" | "ws" => OptionValue::Bool(self.wrapscan),
830 "autoindent" | "ai" => OptionValue::Bool(self.autoindent),
831 "smartindent" | "si" => OptionValue::Bool(self.smartindent),
832 "timeoutlen" | "tm" => OptionValue::Int(self.timeout_len.as_millis() as i64),
833 "undolevels" | "ul" => OptionValue::Int(self.undo_levels as i64),
834 "undobreak" => OptionValue::Bool(self.undo_break_on_motion),
835 "readonly" | "ro" => OptionValue::Bool(self.readonly),
836 "modifiable" | "ma" => OptionValue::Bool(self.modifiable),
837 "wrap" => OptionValue::Bool(!matches!(self.wrap, WrapMode::None)),
838 "linebreak" | "lbr" => OptionValue::Bool(matches!(self.wrap, WrapMode::Word)),
839 "number" | "nu" => OptionValue::Bool(self.number),
840 "relativenumber" | "rnu" => OptionValue::Bool(self.relativenumber),
841 "numberwidth" | "nuw" => OptionValue::Int(self.numberwidth as i64),
842 "cursorline" | "cul" => OptionValue::Bool(self.cursorline),
843 "cursorcolumn" | "cuc" => OptionValue::Bool(self.cursorcolumn),
844 "signcolumn" | "scl" => OptionValue::String(
845 match self.signcolumn {
846 SignColumnMode::Yes => "yes",
847 SignColumnMode::No => "no",
848 SignColumnMode::Auto => "auto",
849 }
850 .to_string(),
851 ),
852 "foldcolumn" | "fdc" => OptionValue::Int(self.foldcolumn as i64),
853 "foldmethod" | "fdm" => OptionValue::String(
854 match self.foldmethod {
855 FoldMethod::Manual => "manual",
856 FoldMethod::Expr => "expr",
857 FoldMethod::Marker => "marker",
858 }
859 .to_string(),
860 ),
861 "foldenable" | "fen" => OptionValue::Bool(self.foldenable),
862 "foldlevelstart" | "fls" => OptionValue::Int(self.foldlevelstart as i64),
863 "colorcolumn" | "cc" => OptionValue::String(self.colorcolumn.clone()),
864 "formatoptions" | "fo" => OptionValue::String(self.formatoptions.clone()),
865 "filetype" | "ft" => OptionValue::String(self.filetype.clone()),
866 "scrolloff" | "so" => OptionValue::Int(self.scrolloff as i64),
867 "sidescrolloff" | "siso" => OptionValue::Int(self.sidescrolloff as i64),
868 "modeline" | "ml" => OptionValue::Bool(self.modeline),
869 "autoreload" | "ar" => OptionValue::Bool(self.autoreload),
870 "modelines" | "mls" => OptionValue::Int(self.modelines as i64),
871 "motion_sneak" | "snk" => OptionValue::Bool(self.motion_sneak),
872 "list" => OptionValue::Bool(self.list),
873 "listchars" | "lcs" => OptionValue::String(self.listchars.to_canonical_string()),
874 "indent_guides" | "ig" => OptionValue::Bool(self.indent_guides),
875 "indent_guide_char" | "igc" => OptionValue::String(self.indent_guide_char.to_string()),
876 "colorizer" | "clz" => OptionValue::Bool(self.colorizer),
877 "colorizer_filetypes" | "clzft" => {
878 OptionValue::String(self.colorizer_filetypes.join(","))
879 }
880 "format_on_save" | "fos" => OptionValue::Bool(self.format_on_save),
881 "trim_trailing_whitespace" | "tts" => OptionValue::Bool(self.trim_trailing_whitespace),
882 "rainbow_brackets" | "rb" => OptionValue::Bool(self.rainbow_brackets),
883 "updatetime" | "ut" => OptionValue::Int(self.updatetime as i64),
884 "matchparen" | "mps" => OptionValue::Bool(self.matchparen),
885 "fixendofline" | "fixeol" => OptionValue::Bool(self.fixendofline),
886 _ => return None,
887 })
888 }
889}
890
891pub use hjkl_buffer::Viewport;
913
914#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
918pub struct BufferId(pub u64);
919
920#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
922pub struct Modifiers {
923 pub ctrl: bool,
924 pub shift: bool,
925 pub alt: bool,
926 pub super_: bool,
927}
928
929#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
931#[non_exhaustive]
932pub enum SpecialKey {
933 Esc,
934 Enter,
935 Backspace,
936 Tab,
937 BackTab,
938 Up,
939 Down,
940 Left,
941 Right,
942 Home,
943 End,
944 PageUp,
945 PageDown,
946 Insert,
947 Delete,
948 F(u8),
949}
950
951#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
952pub enum MouseKind {
953 Press,
954 Release,
955 Drag,
956 ScrollUp,
957 ScrollDown,
958}
959
960#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
961pub struct MouseEvent {
962 pub kind: MouseKind,
963 pub pos: Pos,
964 pub mods: Modifiers,
965}
966
967#[derive(Debug, Clone, PartialEq, Eq)]
972#[non_exhaustive]
973pub enum Input {
974 Char(char, Modifiers),
975 Key(SpecialKey, Modifiers),
976 Mouse(MouseEvent),
977 Paste(String),
978 FocusGained,
979 FocusLost,
980 Resize(u16, u16),
981}
982
983pub trait Host: Send {
992 type Intent;
996
997 fn write_clipboard(&mut self, text: String);
1003
1004 fn read_clipboard(&mut self) -> Option<String>;
1007
1008 fn now(&self) -> core::time::Duration;
1014
1015 fn should_cancel(&self) -> bool {
1018 false
1019 }
1020
1021 fn prompt_search(&mut self) -> Option<String>;
1026
1027 fn display_line_for(&self, pos: Pos) -> u32 {
1032 pos.line
1033 }
1034
1035 fn pos_for_display(&self, line: u32, col: u32) -> Pos {
1037 Pos { line, col }
1038 }
1039
1040 fn syntax_highlights(&self, range: Range<Pos>) -> Vec<Highlight> {
1045 let _ = range;
1046 Vec::new()
1047 }
1048
1049 fn emit_cursor_shape(&mut self, shape: CursorShape);
1054
1055 fn viewport(&self) -> &Viewport;
1062
1063 fn viewport_mut(&mut self) -> &mut Viewport;
1066
1067 fn emit_intent(&mut self, intent: Self::Intent);
1072}
1073
1074#[derive(Debug)]
1088pub struct DefaultHost {
1089 clipboard: Option<String>,
1090 last_cursor_shape: CursorShape,
1091 started: std::time::Instant,
1092 viewport: Viewport,
1093}
1094
1095impl Default for DefaultHost {
1096 fn default() -> Self {
1097 Self::new()
1098 }
1099}
1100
1101impl DefaultHost {
1102 pub const DEFAULT_VIEWPORT: Viewport = Viewport {
1105 top_row: 0,
1106 top_col: 0,
1107 width: 80,
1108 height: 24,
1109 wrap: hjkl_buffer::Wrap::None,
1110 text_width: 80,
1111 tab_width: 0,
1112 };
1113
1114 pub fn new() -> Self {
1115 Self {
1116 clipboard: None,
1117 last_cursor_shape: CursorShape::Block,
1118 started: std::time::Instant::now(),
1119 viewport: Self::DEFAULT_VIEWPORT,
1120 }
1121 }
1122
1123 pub fn with_viewport(viewport: Viewport) -> Self {
1127 Self {
1128 clipboard: None,
1129 last_cursor_shape: CursorShape::Block,
1130 started: std::time::Instant::now(),
1131 viewport,
1132 }
1133 }
1134
1135 pub fn last_cursor_shape(&self) -> CursorShape {
1137 self.last_cursor_shape
1138 }
1139}
1140
1141impl Host for DefaultHost {
1142 type Intent = ();
1143
1144 fn write_clipboard(&mut self, text: String) {
1145 self.clipboard = Some(text);
1146 }
1147
1148 fn read_clipboard(&mut self) -> Option<String> {
1149 self.clipboard.clone()
1150 }
1151
1152 fn now(&self) -> core::time::Duration {
1153 self.started.elapsed()
1154 }
1155
1156 fn prompt_search(&mut self) -> Option<String> {
1157 None
1158 }
1159
1160 fn emit_cursor_shape(&mut self, shape: CursorShape) {
1161 self.last_cursor_shape = shape;
1162 }
1163
1164 fn viewport(&self) -> &Viewport {
1165 &self.viewport
1166 }
1167
1168 fn viewport_mut(&mut self) -> &mut Viewport {
1169 &mut self.viewport
1170 }
1171
1172 fn emit_intent(&mut self, _intent: Self::Intent) {}
1173}
1174
1175#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
1187pub struct RenderFrame {
1188 pub mode: SnapshotMode,
1189 pub cursor_row: u32,
1190 pub cursor_col: u32,
1191 pub cursor_shape: CursorShape,
1192 pub viewport_top: u32,
1193 pub line_count: u32,
1194}
1195
1196#[derive(Debug, Clone)]
1223
1224pub struct EditorSnapshot {
1225 pub version: u32,
1228 pub mode: SnapshotMode,
1230 pub cursor: (u32, u32),
1232 pub lines: Vec<String>,
1234 pub viewport_top: u32,
1236 pub registers: crate::Registers,
1240 pub marks: std::collections::BTreeMap<char, (u32, u32)>,
1247 pub global_marks: std::collections::BTreeMap<char, (u64, u32, u32)>,
1251}
1252
1253#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
1257
1258pub enum SnapshotMode {
1259 #[default]
1260 Normal,
1261 Insert,
1262 Visual,
1263 VisualLine,
1264 VisualBlock,
1265}
1266
1267impl EditorSnapshot {
1268 pub const VERSION: u32 = 5;
1289}
1290
1291#[derive(Debug, thiserror::Error)]
1295pub enum EngineError {
1296 #[error("regex compile error: {0}")]
1299 Regex(#[from] regex::Error),
1300
1301 #[error("invalid range: {0}")]
1303 InvalidRange(String),
1304
1305 #[error("ex parse: {0}")]
1307 Ex(String),
1308
1309 #[error("buffer is read-only")]
1311 ReadOnly,
1312
1313 #[error("position out of bounds: {0:?}")]
1315 OutOfBounds(Pos),
1316
1317 #[error("snapshot version mismatch: file={0}, expected={1}")]
1320 SnapshotVersion(u32, u32),
1321}
1322
1323pub(crate) mod sealed {
1324 pub trait Sealed {}
1334}
1335
1336pub trait Cursor: Send {
1342 fn cursor(&self) -> Pos;
1344 fn set_cursor(&mut self, pos: Pos);
1346 fn byte_offset(&self, pos: Pos) -> usize;
1348 fn pos_at_byte(&self, byte: usize) -> Pos;
1350}
1351
1352pub trait Query: Send {
1354 fn line_count(&self) -> u32;
1356 fn line(&self, idx: u32) -> String;
1359 fn len_bytes(&self) -> usize;
1361 fn slice(&self, range: core::ops::Range<Pos>) -> std::borrow::Cow<'_, str>;
1366 fn dirty_gen(&self) -> u64 {
1380 0
1381 }
1382
1383 fn byte_of_row(&self, row: usize) -> usize {
1394 let n = self.line_count() as usize;
1395 let row = row.min(n);
1396 let mut acc = 0usize;
1397 for r in 0..row {
1398 acc += self.line(r as u32).len();
1399 if r + 1 < n {
1404 acc += 1;
1405 }
1406 }
1407 acc
1408 }
1409
1410 fn content_joined(&self) -> std::sync::Arc<String> {
1419 let n = self.line_count() as usize;
1420 let mut acc = String::with_capacity(self.len_bytes());
1421 for r in 0..n {
1422 if r > 0 {
1423 acc.push('\n');
1424 }
1425 acc.push_str(&self.line(r as u32));
1426 }
1427 std::sync::Arc::new(acc)
1428 }
1429
1430 fn line_bytes(&self, row: usize) -> usize {
1438 let n = self.line_count() as usize;
1439 if row >= n {
1440 return 0;
1441 }
1442 self.line(row as u32).len()
1443 }
1444
1445 fn rope(&self) -> ropey::Rope {
1454 ropey::Rope::from_str(&self.content_joined())
1455 }
1456}
1457
1458pub trait BufferEdit: Send {
1462 fn insert_at(&mut self, pos: Pos, text: &str);
1465 fn delete_range(&mut self, range: core::ops::Range<Pos>);
1467 fn replace_range(&mut self, range: core::ops::Range<Pos>, replacement: &str);
1469 fn replace_all(&mut self, text: &str) {
1477 self.replace_range(
1478 Pos::ORIGIN..Pos {
1479 line: u32::MAX,
1480 col: u32::MAX,
1481 },
1482 text,
1483 );
1484 }
1485}
1486
1487pub trait Search: Send {
1490 fn find_next(&self, from: Pos, pat: ®ex::Regex) -> Option<core::ops::Range<Pos>>;
1492 fn find_prev(&self, from: Pos, pat: ®ex::Regex) -> Option<core::ops::Range<Pos>>;
1494}
1495
1496pub trait View: Cursor + Query + BufferEdit + Search + sealed::Sealed + Send {}
1504
1505pub trait FoldProvider: Send {
1526 fn next_visible_row(&self, row: usize, row_count: usize) -> Option<usize>;
1529 fn prev_visible_row(&self, row: usize) -> Option<usize>;
1531 fn is_row_hidden(&self, row: usize) -> bool;
1533 fn fold_at_row(&self, row: usize) -> Option<(usize, usize, bool)>;
1537
1538 fn apply(&mut self, op: FoldOp) {
1548 let _ = op;
1549 }
1550
1551 fn invalidate_range(&mut self, start_row: usize, end_row: usize) {
1556 self.apply(FoldOp::Invalidate { start_row, end_row });
1557 }
1558}
1559
1560#[derive(Debug, Default, Clone, Copy)]
1563pub struct NoopFoldProvider;
1564
1565impl FoldProvider for NoopFoldProvider {
1566 fn next_visible_row(&self, row: usize, row_count: usize) -> Option<usize> {
1567 let last = row_count.saturating_sub(1);
1568 if last == 0 && row == 0 {
1569 return None;
1570 }
1571 let r = row.checked_add(1)?;
1572 (r <= last).then_some(r)
1573 }
1574
1575 fn prev_visible_row(&self, row: usize) -> Option<usize> {
1576 row.checked_sub(1)
1577 }
1578
1579 fn is_row_hidden(&self, _row: usize) -> bool {
1580 false
1581 }
1582
1583 fn fold_at_row(&self, _row: usize) -> Option<(usize, usize, bool)> {
1584 None
1585 }
1586}
1587
1588#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1590pub enum InsertDir {
1591 Left,
1592 Right,
1593 Up,
1594 Down,
1595}
1596
1597#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1600pub enum ScrollDir {
1601 Down,
1603 Up,
1605}
1606
1607pub const SEARCH_HISTORY_MAX: usize = 100;
1608pub const CHANGE_LIST_MAX: usize = 100;
1609
1610pub const JUMPLIST_MAX: usize = 100;
1612
1613#[cfg(test)]
1614mod tests {
1615 use super::*;
1616
1617 #[test]
1618 fn caret_is_empty() {
1619 let sel = Selection::caret(Pos::new(2, 4));
1620 assert!(sel.is_empty());
1621 assert_eq!(sel.anchor, sel.head);
1622 }
1623
1624 #[test]
1625 fn selection_set_default_has_one_caret() {
1626 let set = SelectionSet::default();
1627 assert_eq!(set.items.len(), 1);
1628 assert_eq!(set.primary, 0);
1629 assert_eq!(set.primary().anchor, Pos::ORIGIN);
1630 }
1631
1632 #[test]
1633 fn edit_constructors() {
1634 let p = Pos::new(0, 5);
1635 assert_eq!(Edit::insert(p, "x").range, p..p);
1636 assert!(Edit::insert(p, "x").replacement == "x");
1637 assert!(Edit::delete(p..p).replacement.is_empty());
1638 }
1639
1640 #[test]
1641 fn attrs_flags() {
1642 let a = Attrs::BOLD | Attrs::UNDERLINE;
1643 assert!(a.contains(Attrs::BOLD));
1644 assert!(!a.contains(Attrs::ITALIC));
1645 }
1646
1647 #[test]
1648 fn options_set_get_roundtrip() {
1649 let mut o = Options::default();
1650 o.set_by_name("tabstop", OptionValue::Int(4)).unwrap();
1651 assert!(matches!(o.get_by_name("ts"), Some(OptionValue::Int(4))));
1652 o.set_by_name("expandtab", OptionValue::Bool(true)).unwrap();
1653 assert!(matches!(o.get_by_name("et"), Some(OptionValue::Bool(true))));
1654 o.set_by_name("iskeyword", OptionValue::String("a-z".into()))
1655 .unwrap();
1656 match o.get_by_name("iskeyword") {
1657 Some(OptionValue::String(s)) => assert_eq!(s, "a-z"),
1658 other => panic!("expected String, got {other:?}"),
1659 }
1660 }
1661
1662 #[test]
1663 fn options_unknown_name_errors_on_set() {
1664 let mut o = Options::default();
1665 assert!(matches!(
1666 o.set_by_name("frobnicate", OptionValue::Int(1)),
1667 Err(EngineError::Ex(_))
1668 ));
1669 assert!(o.get_by_name("frobnicate").is_none());
1670 }
1671
1672 #[test]
1682 fn set_by_name_rejects_makeprg_and_errorformat() {
1683 let mut o = Options::default();
1684 assert!(
1685 matches!(
1686 o.set_by_name("makeprg", OptionValue::String("rm -rf /".into())),
1687 Err(EngineError::Ex(_))
1688 ),
1689 "`makeprg` must never be a settable option — a modeline must \
1690 never be able to smuggle an arbitrary shell command into `:make`"
1691 );
1692 assert!(o.get_by_name("makeprg").is_none());
1693 assert!(
1694 matches!(
1695 o.set_by_name("errorformat", OptionValue::String("%f:%l:%m".into())),
1696 Err(EngineError::Ex(_))
1697 ),
1698 "`errorformat` rides the same `:make`/`:grep` shell-out surface \
1699 as `makeprg` and must stay unsettable too"
1700 );
1701 assert!(o.get_by_name("errorformat").is_none());
1702 }
1703
1704 #[test]
1705 fn options_type_mismatch_errors() {
1706 let mut o = Options::default();
1707 assert!(matches!(
1708 o.set_by_name("tabstop", OptionValue::String("nope".into())),
1709 Err(EngineError::Ex(_))
1710 ));
1711 assert!(matches!(
1712 o.set_by_name("iskeyword", OptionValue::Int(7)),
1713 Err(EngineError::Ex(_))
1714 ));
1715 }
1716
1717 #[test]
1720 fn default_options_ignorecase_and_smartcase_are_true() {
1721 let o = Options::default();
1722 assert!(o.ignorecase, "ignorecase must default to true");
1723 assert!(o.smartcase, "smartcase must default to true");
1724 }
1725
1726 #[test]
1727 fn options_int_to_bool_coercion() {
1728 let mut o = Options::default();
1731 o.set_by_name("ignorecase", OptionValue::Int(1)).unwrap();
1732 assert!(matches!(o.get_by_name("ic"), Some(OptionValue::Bool(true))));
1733 o.set_by_name("ignorecase", OptionValue::Int(0)).unwrap();
1734 assert!(matches!(
1735 o.get_by_name("ic"),
1736 Some(OptionValue::Bool(false))
1737 ));
1738 }
1739
1740 #[test]
1741 fn options_wrap_linebreak_roundtrip() {
1742 let mut o = Options::default();
1743 assert_eq!(o.wrap, WrapMode::None);
1744 o.set_by_name("wrap", OptionValue::Bool(true)).unwrap();
1745 assert_eq!(o.wrap, WrapMode::Char);
1746 o.set_by_name("linebreak", OptionValue::Bool(true)).unwrap();
1747 assert_eq!(o.wrap, WrapMode::Word);
1748 assert!(matches!(
1749 o.get_by_name("wrap"),
1750 Some(OptionValue::Bool(true))
1751 ));
1752 assert!(matches!(
1753 o.get_by_name("lbr"),
1754 Some(OptionValue::Bool(true))
1755 ));
1756 o.set_by_name("linebreak", OptionValue::Bool(false))
1757 .unwrap();
1758 assert_eq!(o.wrap, WrapMode::Char);
1759 o.set_by_name("wrap", OptionValue::Bool(false)).unwrap();
1760 assert_eq!(o.wrap, WrapMode::None);
1761 }
1762
1763 #[test]
1764 fn options_default_modern() {
1765 let o = Options::default();
1768 assert_eq!(o.tabstop, 4);
1769 assert_eq!(o.shiftwidth, 4);
1770 assert_eq!(o.softtabstop, 4);
1771 assert!(o.expandtab);
1772 assert!(o.hlsearch);
1773 assert!(o.wrapscan);
1774 assert!(o.smartindent);
1775 assert_eq!(o.timeout_len, core::time::Duration::from_millis(1000));
1776 }
1777
1778 #[test]
1779 fn editor_snapshot_version_const() {
1780 assert_eq!(EditorSnapshot::VERSION, 5);
1781 }
1782
1783 #[test]
1784 fn editor_snapshot_default_shape() {
1785 let s = EditorSnapshot {
1786 version: EditorSnapshot::VERSION,
1787 mode: SnapshotMode::Normal,
1788 cursor: (0, 0),
1789 lines: vec!["hello".to_string()],
1790 viewport_top: 0,
1791 registers: crate::Registers::default(),
1792 marks: Default::default(),
1793 global_marks: Default::default(),
1794 };
1795 assert_eq!(s.cursor, (0, 0));
1796 assert_eq!(s.lines.len(), 1);
1797 }
1798
1799 #[test]
1800 fn engine_error_display() {
1801 let e = EngineError::ReadOnly;
1802 assert_eq!(e.to_string(), "buffer is read-only");
1803 let e = EngineError::OutOfBounds(Pos::new(3, 7));
1804 assert!(e.to_string().contains("out of bounds"));
1805 }
1806
1807 #[test]
1810 fn options_cursorline_roundtrip() {
1811 let mut o = Options::default();
1812 assert!(o.cursorline, "cursorline defaults to true in hjkl");
1814 o.set_by_name("cursorline", OptionValue::Bool(true))
1815 .unwrap();
1816 assert!(matches!(
1817 o.get_by_name("cul"),
1818 Some(OptionValue::Bool(true))
1819 ));
1820 o.set_by_name("cul", OptionValue::Bool(false)).unwrap();
1821 assert!(matches!(
1822 o.get_by_name("cursorline"),
1823 Some(OptionValue::Bool(false))
1824 ));
1825 }
1826
1827 #[test]
1828 fn options_cursorcolumn_roundtrip() {
1829 let mut o = Options::default();
1830 assert!(
1831 !o.cursorcolumn,
1832 "cursorcolumn defaults to false (vim parity)"
1833 );
1834 o.set_by_name("cuc", OptionValue::Bool(true)).unwrap();
1835 assert!(matches!(
1836 o.get_by_name("cursorcolumn"),
1837 Some(OptionValue::Bool(true))
1838 ));
1839 o.set_by_name("cuc", OptionValue::Bool(false)).unwrap();
1840 assert!(matches!(
1841 o.get_by_name("cursorcolumn"),
1842 Some(OptionValue::Bool(false))
1843 ));
1844 o.set_by_name("cuc", OptionValue::Bool(true)).unwrap();
1845 assert!(matches!(
1846 o.get_by_name("cursorcolumn"),
1847 Some(OptionValue::Bool(true))
1848 ));
1849 }
1850
1851 #[test]
1852 fn options_signcolumn_roundtrip() {
1853 let mut o = Options::default();
1854 assert_eq!(
1855 o.signcolumn,
1856 SignColumnMode::Auto,
1857 "signcolumn defaults to auto"
1858 );
1859 o.set_by_name("signcolumn", OptionValue::String("yes".into()))
1860 .unwrap();
1861 assert_eq!(o.signcolumn, SignColumnMode::Yes);
1862 assert_eq!(
1863 o.get_by_name("scl"),
1864 Some(OptionValue::String("yes".into()))
1865 );
1866 o.set_by_name("scl", OptionValue::String("no".into()))
1867 .unwrap();
1868 assert_eq!(o.signcolumn, SignColumnMode::No);
1869 o.set_by_name("scl", OptionValue::String("auto".into()))
1870 .unwrap();
1871 assert_eq!(o.signcolumn, SignColumnMode::Auto);
1872 }
1873
1874 #[test]
1875 fn options_signcolumn_rejects_invalid() {
1876 let mut o = Options::default();
1877 assert!(matches!(
1878 o.set_by_name("signcolumn", OptionValue::String("maybe".into())),
1879 Err(EngineError::Ex(_))
1880 ));
1881 assert!(matches!(
1883 o.set_by_name("signcolumn", OptionValue::Bool(true)),
1884 Err(EngineError::Ex(_))
1885 ));
1886 }
1887
1888 #[test]
1889 fn options_foldcolumn_roundtrip() {
1890 let mut o = Options::default();
1891 assert_eq!(o.foldcolumn, 0, "foldcolumn defaults to 0");
1892 o.set_by_name("fdc", OptionValue::Int(3)).unwrap();
1893 assert_eq!(o.foldcolumn, 3);
1894 assert_eq!(o.get_by_name("foldcolumn"), Some(OptionValue::Int(3)));
1895 }
1896
1897 #[test]
1898 fn options_foldcolumn_rejects_out_of_range() {
1899 let mut o = Options::default();
1900 assert!(matches!(
1901 o.set_by_name("foldcolumn", OptionValue::Int(13)),
1902 Err(EngineError::Ex(_))
1903 ));
1904 assert!(matches!(
1905 o.set_by_name("foldcolumn", OptionValue::Int(-1)),
1906 Err(EngineError::Ex(_))
1907 ));
1908 }
1909
1910 #[test]
1911 fn options_colorcolumn_roundtrip() {
1912 let mut o = Options::default();
1913 assert_eq!(o.colorcolumn, "", "colorcolumn defaults to empty string");
1914 o.set_by_name("cc", OptionValue::String("80,120".into()))
1915 .unwrap();
1916 assert_eq!(
1917 o.get_by_name("colorcolumn"),
1918 Some(OptionValue::String("80,120".into()))
1919 );
1920 o.set_by_name("colorcolumn", OptionValue::String(String::new()))
1921 .unwrap();
1922 assert_eq!(
1923 o.get_by_name("cc"),
1924 Some(OptionValue::String(String::new()))
1925 );
1926 }
1927
1928 #[test]
1929 fn options_cursorline_alias_cul() {
1930 let mut o = Options::default();
1931 o.set_by_name("cul", OptionValue::Bool(true)).unwrap();
1933 assert!(o.cursorline);
1934 o.set_by_name("cul", OptionValue::Bool(false)).unwrap();
1936 assert!(!o.cursorline);
1937 }
1938
1939 #[test]
1940 fn sign_column_mode_default_is_auto() {
1941 assert_eq!(SignColumnMode::default(), SignColumnMode::Auto);
1942 }
1943
1944 #[test]
1945 fn options_scrolloff_default_and_set() {
1946 let mut o = Options::default();
1947 assert_eq!(o.scrolloff, 5, "scrolloff defaults to 5");
1948 o.set_by_name("scrolloff", OptionValue::Int(0)).unwrap();
1949 assert_eq!(o.scrolloff, 0);
1950 o.set_by_name("scrolloff", OptionValue::Int(999)).unwrap();
1951 assert_eq!(o.scrolloff, 999);
1952 assert_eq!(o.get_by_name("scrolloff"), Some(OptionValue::Int(999)));
1953 }
1954
1955 #[test]
1956 fn options_sidescrolloff_default_and_set() {
1957 let mut o = Options::default();
1958 assert_eq!(o.sidescrolloff, 0, "sidescrolloff defaults to 0");
1959 o.set_by_name("sidescrolloff", OptionValue::Int(5)).unwrap();
1960 assert_eq!(o.sidescrolloff, 5);
1961 assert_eq!(o.get_by_name("sidescrolloff"), Some(OptionValue::Int(5)));
1962 }
1963
1964 #[test]
1965 fn options_alias_so_siso() {
1966 let mut o = Options::default();
1967 o.set_by_name("so", OptionValue::Int(3)).unwrap();
1969 assert_eq!(o.scrolloff, 3);
1970 assert_eq!(o.get_by_name("so"), Some(OptionValue::Int(3)));
1971 o.set_by_name("siso", OptionValue::Int(2)).unwrap();
1973 assert_eq!(o.sidescrolloff, 2);
1974 assert_eq!(o.get_by_name("siso"), Some(OptionValue::Int(2)));
1975 }
1976
1977 #[test]
1980 fn options_list_default_false_and_set() {
1981 let mut o = Options::default();
1982 assert!(!o.list, "list default is false");
1983 o.set_by_name("list", OptionValue::Bool(true)).unwrap();
1984 assert!(o.list);
1985 assert_eq!(o.get_by_name("list"), Some(OptionValue::Bool(true)));
1986 o.set_by_name("list", OptionValue::Bool(false)).unwrap();
1987 assert!(!o.list);
1988 }
1989
1990 #[test]
1991 fn options_listchars_default_matches_vim() {
1992 let o = Options::default();
1993 let lc = &o.listchars;
1994 assert_eq!(lc.tab_lead, '^');
1995 assert_eq!(lc.tab_fill, Some('I'));
1996 assert_eq!(lc.eol, Some('$'));
1997 assert_eq!(lc.space, None);
1998 assert_eq!(lc.trail, None);
1999 assert_eq!(lc.nbsp, None);
2000 }
2001
2002 #[test]
2003 fn options_listchars_set_and_get() {
2004 let mut o = Options::default();
2005 o.set_by_name("listchars", OptionValue::String("tab:>-,eol:$".to_string()))
2006 .unwrap();
2007 assert_eq!(o.listchars.tab_lead, '>');
2008 assert_eq!(o.listchars.tab_fill, Some('-'));
2009 assert_eq!(o.listchars.eol, Some('$'));
2010 }
2011
2012 #[test]
2013 fn options_lcs_alias_sets_listchars() {
2014 let mut o = Options::default();
2015 o.set_by_name("lcs", OptionValue::String("tab:>-,trail:~".to_string()))
2016 .unwrap();
2017 assert_eq!(o.listchars.tab_lead, '>');
2018 assert_eq!(o.listchars.trail, Some('~'));
2019 }
2020
2021 #[test]
2022 fn options_listchars_get_by_name_returns_string() {
2023 let o = Options::default();
2024 match o.get_by_name("listchars") {
2025 Some(OptionValue::String(s)) => {
2026 assert!(s.contains("tab:"), "canonical string should contain tab:");
2027 }
2028 other => panic!("expected String, got {other:?}"),
2029 }
2030 }
2031
2032 #[test]
2033 fn options_listchars_invalid_value_returns_err() {
2034 let mut o = Options::default();
2035 assert!(
2036 o.set_by_name("listchars", OptionValue::String("bogus:x".to_string()))
2037 .is_err()
2038 );
2039 }
2040
2041 #[test]
2044 fn indent_guides_default_true() {
2045 assert!(
2046 Options::default().indent_guides,
2047 "indent_guides must default to true"
2048 );
2049 }
2050
2051 #[test]
2052 fn options_indent_guides_set_and_get() {
2053 let mut opts = Options::default();
2054 opts.set_by_name("indent_guides", OptionValue::Bool(false))
2056 .unwrap();
2057 assert!(!opts.indent_guides);
2058 opts.set_by_name("ig", OptionValue::Bool(true)).unwrap();
2060 assert!(opts.indent_guides);
2061 assert_eq!(opts.get_by_name("ig"), Some(OptionValue::Bool(true)));
2063 assert_eq!(
2064 opts.get_by_name("indent_guides"),
2065 Some(OptionValue::Bool(true))
2066 );
2067 }
2068
2069 #[test]
2070 fn options_indent_guide_char_set_and_get() {
2071 let mut opts = Options::default();
2072 opts.set_by_name("indent_guide_char", OptionValue::String(":".to_string()))
2073 .unwrap();
2074 assert_eq!(opts.indent_guide_char, ':');
2075 opts.set_by_name("igc", OptionValue::String("┊".to_string()))
2077 .unwrap();
2078 assert_eq!(opts.indent_guide_char, '┊');
2079 assert_eq!(
2081 opts.get_by_name("igc"),
2082 Some(OptionValue::String("┊".to_string()))
2083 );
2084 assert_eq!(
2085 opts.get_by_name("indent_guide_char"),
2086 Some(OptionValue::String("┊".to_string()))
2087 );
2088 }
2089
2090 #[test]
2091 fn options_indent_guide_char_rejects_multi_char() {
2092 let mut opts = Options::default();
2093 assert!(
2094 opts.set_by_name("indent_guide_char", OptionValue::String("ab".to_string()))
2095 .is_err(),
2096 "multi-char value must be rejected"
2097 );
2098 }
2099
2100 #[test]
2101 fn options_indent_guide_char_rejects_empty() {
2102 let mut opts = Options::default();
2103 assert!(
2104 opts.set_by_name("indent_guide_char", OptionValue::String(String::new()))
2105 .is_err(),
2106 "empty string must be rejected"
2107 );
2108 }
2109
2110 #[test]
2113 fn colorizer_default_true() {
2114 assert!(
2115 Options::default().colorizer,
2116 "colorizer must default to true"
2117 );
2118 }
2119
2120 #[test]
2121 fn colorizer_filetypes_includes_css() {
2122 let o = Options::default();
2123 assert!(
2124 o.colorizer_filetypes.iter().any(|f| f == "css"),
2125 "default colorizer_filetypes must include 'css'"
2126 );
2127 }
2128
2129 #[test]
2130 fn options_colorizer_set_and_get() {
2131 let mut o = Options::default();
2132 o.set_by_name("colorizer", OptionValue::Bool(false))
2133 .unwrap();
2134 assert_eq!(o.get_by_name("colorizer"), Some(OptionValue::Bool(false)));
2135 o.set_by_name("clz", OptionValue::Bool(true)).unwrap();
2136 assert_eq!(o.get_by_name("clz"), Some(OptionValue::Bool(true)));
2137 }
2138
2139 #[test]
2140 fn options_colorizer_filetypes_set_and_get() {
2141 let mut o = Options::default();
2142 o.set_by_name(
2143 "colorizer_filetypes",
2144 OptionValue::String("css,scss,toml".into()),
2145 )
2146 .unwrap();
2147 assert_eq!(o.colorizer_filetypes, vec!["css", "scss", "toml"]);
2148 assert_eq!(
2149 o.get_by_name("clzft"),
2150 Some(OptionValue::String("css,scss,toml".into()))
2151 );
2152 }
2153
2154 #[test]
2157 fn format_on_save_default_true() {
2158 let o = Options::default();
2159 assert!(o.format_on_save, "format_on_save must default to true");
2160 }
2161
2162 #[test]
2163 fn trim_trailing_whitespace_default_false() {
2164 let o = Options::default();
2165 assert!(
2166 !o.trim_trailing_whitespace,
2167 "trim_trailing_whitespace must default to false"
2168 );
2169 }
2170
2171 #[test]
2172 fn options_fos_alias_sets_format_on_save() {
2173 let mut o = Options::default();
2174 o.set_by_name("fos", OptionValue::Bool(true)).unwrap();
2175 assert!(o.format_on_save, "fos alias must set format_on_save");
2176 assert_eq!(
2177 o.get_by_name("fos"),
2178 Some(OptionValue::Bool(true)),
2179 "get_by_name(fos) must reflect the new value"
2180 );
2181 assert_eq!(
2182 o.get_by_name("format_on_save"),
2183 Some(OptionValue::Bool(true)),
2184 "get_by_name(format_on_save) must also reflect the new value"
2185 );
2186 }
2187
2188 #[test]
2189 fn options_tts_alias_sets_trim_trailing_whitespace() {
2190 let mut o = Options::default();
2191 o.set_by_name("tts", OptionValue::Bool(true)).unwrap();
2192 assert!(
2193 o.trim_trailing_whitespace,
2194 "tts alias must set trim_trailing_whitespace"
2195 );
2196 assert_eq!(
2197 o.get_by_name("tts"),
2198 Some(OptionValue::Bool(true)),
2199 "get_by_name(tts) must reflect the new value"
2200 );
2201 assert_eq!(
2202 o.get_by_name("trim_trailing_whitespace"),
2203 Some(OptionValue::Bool(true)),
2204 "get_by_name(trim_trailing_whitespace) must also reflect the new value"
2205 );
2206 }
2207
2208 #[test]
2211 fn rainbow_brackets_default_true() {
2212 let o = Options::default();
2213 assert!(o.rainbow_brackets, "rainbow_brackets must default to true");
2214 }
2215
2216 #[test]
2217 fn options_rb_alias_sets_rainbow_brackets() {
2218 let mut o = Options::default();
2219 o.set_by_name("rb", OptionValue::Bool(false)).unwrap();
2220 assert!(
2221 !o.rainbow_brackets,
2222 "rb alias must set rainbow_brackets to false"
2223 );
2224 assert_eq!(
2225 o.get_by_name("rb"),
2226 Some(OptionValue::Bool(false)),
2227 "get_by_name(rb) must reflect the new value"
2228 );
2229 assert_eq!(
2230 o.get_by_name("rainbow_brackets"),
2231 Some(OptionValue::Bool(false)),
2232 "get_by_name(rainbow_brackets) must also reflect the new value"
2233 );
2234 }
2235
2236 #[test]
2237 fn autoreload_default_true() {
2238 assert!(
2239 Options::default().autoreload,
2240 "autoreload must default true"
2241 );
2242 }
2243
2244 #[test]
2245 fn options_ar_alias_sets_autoreload() {
2246 let mut o = Options::default();
2247 o.set_by_name("ar", OptionValue::Bool(false)).unwrap();
2248 assert!(!o.autoreload, "ar alias must set autoreload");
2249 assert_eq!(o.get_by_name("autoreload"), Some(OptionValue::Bool(false)));
2250 }
2251
2252 #[test]
2255 fn updatetime_default_4000() {
2256 let o = Options::default();
2257 assert_eq!(o.updatetime, 4000, "updatetime must default to 4000 ms");
2258 assert_eq!(
2259 o.get_by_name("updatetime"),
2260 Some(OptionValue::Int(4000)),
2261 "get_by_name(updatetime) must return Int(4000)"
2262 );
2263 }
2264
2265 #[test]
2266 fn options_ut_alias_sets_updatetime() {
2267 let mut o = Options::default();
2268 o.set_by_name("ut", OptionValue::Int(1000)).unwrap();
2269 assert_eq!(o.updatetime, 1000, "ut alias must set updatetime");
2270 assert_eq!(
2271 o.get_by_name("ut"),
2272 Some(OptionValue::Int(1000)),
2273 "get_by_name(ut) must reflect the new value"
2274 );
2275 assert_eq!(
2276 o.get_by_name("updatetime"),
2277 Some(OptionValue::Int(1000)),
2278 "get_by_name(updatetime) must also reflect the new value"
2279 );
2280 }
2281
2282 #[test]
2285 fn matchparen_default_true() {
2286 let o = Options::default();
2287 assert!(o.matchparen, "matchparen must default to true");
2288 assert_eq!(
2289 o.get_by_name("matchparen"),
2290 Some(OptionValue::Bool(true)),
2291 "get_by_name(matchparen) must return Bool(true)"
2292 );
2293 }
2294
2295 #[test]
2296 fn options_matchparen_set_and_get() {
2297 let mut o = Options::default();
2298 o.set_by_name("matchparen", OptionValue::Bool(false))
2299 .unwrap();
2300 assert!(!o.matchparen, "matchparen must be false after set");
2301 assert_eq!(
2302 o.get_by_name("matchparen"),
2303 Some(OptionValue::Bool(false)),
2304 "get_by_name(matchparen) must reflect false"
2305 );
2306 o.set_by_name("mps", OptionValue::Bool(true)).unwrap();
2308 assert!(o.matchparen, "mps alias must set matchparen to true");
2309 assert_eq!(
2310 o.get_by_name("mps"),
2311 Some(OptionValue::Bool(true)),
2312 "get_by_name(mps) must reflect true"
2313 );
2314 }
2315
2316 #[test]
2321 fn fixendofline_default_true() {
2322 let o = Options::default();
2323 assert!(o.fixendofline, "fixendofline must default to true");
2324 assert_eq!(
2325 o.get_by_name("fixendofline"),
2326 Some(OptionValue::Bool(true)),
2327 "get_by_name(fixendofline) must return Bool(true)"
2328 );
2329 }
2330
2331 #[test]
2332 fn options_fixendofline_set_and_get() {
2333 let mut o = Options::default();
2334 o.set_by_name("fixendofline", OptionValue::Bool(false))
2335 .unwrap();
2336 assert!(!o.fixendofline, "fixendofline must be false after set");
2337 assert_eq!(
2338 o.get_by_name("fixendofline"),
2339 Some(OptionValue::Bool(false)),
2340 "get_by_name(fixendofline) must reflect false"
2341 );
2342 o.set_by_name("fixeol", OptionValue::Bool(true)).unwrap();
2344 assert!(o.fixendofline, "fixeol alias must set fixendofline to true");
2345 assert_eq!(
2346 o.get_by_name("fixeol"),
2347 Some(OptionValue::Bool(true)),
2348 "get_by_name(fixeol) must reflect true"
2349 );
2350 }
2351
2352 #[test]
2355 fn foldmethod_default_expr() {
2356 let o = Options::default();
2357 assert_eq!(
2358 o.foldmethod,
2359 FoldMethod::Expr,
2360 "foldmethod must default to Expr (tree-sitter)"
2361 );
2362 assert_eq!(
2363 o.get_by_name("foldmethod"),
2364 Some(OptionValue::String("expr".into())),
2365 "get_by_name(foldmethod) must return \"expr\""
2366 );
2367 }
2368
2369 #[test]
2370 fn foldmethod_fdm_alias_roundtrip() {
2371 let mut o = Options::default();
2372 o.set_by_name("fdm", OptionValue::String("manual".into()))
2373 .unwrap();
2374 assert_eq!(o.foldmethod, FoldMethod::Manual);
2375 assert_eq!(
2376 o.get_by_name("fdm"),
2377 Some(OptionValue::String("manual".into()))
2378 );
2379 o.set_by_name("foldmethod", OptionValue::String("expr".into()))
2380 .unwrap();
2381 assert_eq!(o.foldmethod, FoldMethod::Expr);
2382 o.set_by_name("foldmethod", OptionValue::String("marker".into()))
2383 .unwrap();
2384 assert_eq!(o.foldmethod, FoldMethod::Marker);
2385 o.set_by_name("foldmethod", OptionValue::String("syntax".into()))
2387 .unwrap();
2388 assert_eq!(o.foldmethod, FoldMethod::Expr);
2389 }
2390
2391 #[test]
2392 fn foldmethod_rejects_invalid_value() {
2393 let mut o = Options::default();
2394 let err = o
2395 .set_by_name("foldmethod", OptionValue::String("bogus".into()))
2396 .unwrap_err();
2397 assert!(
2398 err.to_string().contains("must be"),
2399 "expected error about valid values, got: {err}"
2400 );
2401 }
2402
2403 #[test]
2404 fn foldenable_default_true() {
2405 let o = Options::default();
2406 assert!(o.foldenable, "foldenable must default to true");
2407 assert_eq!(
2408 o.get_by_name("foldenable"),
2409 Some(OptionValue::Bool(true)),
2410 "get_by_name(foldenable) must return Bool(true)"
2411 );
2412 }
2413
2414 #[test]
2415 fn foldenable_fen_alias_roundtrip() {
2416 let mut o = Options::default();
2417 o.set_by_name("fen", OptionValue::Bool(false)).unwrap();
2418 assert!(!o.foldenable, "fen alias must disable foldenable");
2419 assert_eq!(o.get_by_name("fen"), Some(OptionValue::Bool(false)));
2420 o.set_by_name("foldenable", OptionValue::Bool(true))
2421 .unwrap();
2422 assert!(o.foldenable);
2423 }
2424
2425 #[test]
2426 fn foldlevelstart_default_99() {
2427 let o = Options::default();
2428 assert_eq!(o.foldlevelstart, 99, "foldlevelstart must default to 99");
2429 assert_eq!(
2430 o.get_by_name("foldlevelstart"),
2431 Some(OptionValue::Int(99)),
2432 "get_by_name(foldlevelstart) must return Int(99)"
2433 );
2434 }
2435
2436 #[test]
2437 fn foldlevelstart_fls_alias_roundtrip() {
2438 let mut o = Options::default();
2439 o.set_by_name("fls", OptionValue::Int(0)).unwrap();
2440 assert_eq!(
2441 o.foldlevelstart, 0,
2442 "fls alias must set foldlevelstart to 0"
2443 );
2444 assert_eq!(o.get_by_name("fls"), Some(OptionValue::Int(0)));
2445 o.set_by_name("foldlevelstart", OptionValue::Int(5))
2446 .unwrap();
2447 assert_eq!(o.foldlevelstart, 5);
2448 }
2449}