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