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 Selection {
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 Selection {
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 SelectionSet {
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 SelectionSet::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,
242 pub cursorcolumn: bool,
245 pub signcolumn: SignColumnMode,
248 pub foldcolumn: u32,
251 pub foldmethod: FoldMethod,
256 pub foldenable: bool,
260 pub foldlevelstart: u32,
263 pub foldmarker: String,
268 pub colorcolumn: String,
271 pub formatoptions: String,
276 pub filetype: String,
279 pub scrolloff: usize,
284 pub sidescrolloff: usize,
288 pub modeline: bool,
293 pub modelines: u32,
296 pub autoreload: bool,
301 pub motion_sneak: bool,
307 pub list: bool,
310 pub listchars: ListChars,
314 pub indent_guides: bool,
318 pub indent_guide_char: char,
321 pub colorizer: bool,
325 pub colorizer_filetypes: Vec<String>,
329 pub format_on_save: bool,
335 pub trim_trailing_whitespace: bool,
339 pub rainbow_brackets: bool,
342 pub updatetime: u32,
346 pub matchparen: bool,
352}
353
354pub use hjkl_buffer::ListChars;
359
360#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
363#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
364pub enum FoldMethod {
365 Manual,
367 #[default]
372 Expr,
373 Marker,
376}
377
378#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
381#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
382pub enum SignColumnMode {
383 No,
385 Yes,
387 #[default]
389 Auto,
390}
391
392#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
396#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
397pub enum DiagInlineMode {
398 Off,
400 Current,
402 #[default]
404 All,
405}
406
407#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
411#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
412pub enum WrapMode {
413 #[default]
416 None,
417 Char,
420 Word,
424}
425
426#[derive(Debug, Clone, PartialEq, Eq)]
432pub enum OptionValue {
433 Bool(bool),
434 Int(i64),
435 String(String),
436}
437
438impl Default for Options {
439 fn default() -> Self {
440 Options {
441 tabstop: 4,
442 shiftwidth: 4,
443 expandtab: true,
444 softtabstop: 4,
445 iskeyword: "@,48-57,_,192-255".to_string(),
446 ignorecase: true,
447 smartcase: true,
448 hlsearch: true,
449 incsearch: true,
450 wrapscan: true,
451 autoindent: true,
452 smartindent: true,
453 timeout_len: core::time::Duration::from_millis(1000),
454 undo_levels: 1000,
455 undo_break_on_motion: true,
456 readonly: false,
457 modifiable: true,
458 wrap: WrapMode::None,
459 textwidth: 79,
460 number: true,
461 relativenumber: false,
462 numberwidth: 4,
463 cursorline: true,
464 cursorcolumn: false,
465 signcolumn: SignColumnMode::Auto,
466 foldcolumn: 0,
467 foldmethod: FoldMethod::Expr,
468 foldenable: true,
469 foldlevelstart: 99,
470 foldmarker: "{{{,}}}".to_string(),
471 colorcolumn: String::new(),
472 formatoptions: "ro".to_string(),
473 filetype: String::new(),
474 scrolloff: 5,
475 sidescrolloff: 0,
476 modeline: true,
477 modelines: 5,
478 autoreload: true,
479 motion_sneak: true,
480 list: false,
481 listchars: ListChars::default(),
482 indent_guides: true,
483 indent_guide_char: '│',
484 colorizer: true,
485 colorizer_filetypes: vec![
486 "css".to_string(),
487 "scss".to_string(),
488 "sass".to_string(),
489 "less".to_string(),
490 "html".to_string(),
491 "vue".to_string(),
492 "svelte".to_string(),
493 "tailwindcss".to_string(),
494 "toml".to_string(),
495 "lua".to_string(),
496 "vim".to_string(),
497 ],
498 format_on_save: true,
499 trim_trailing_whitespace: false,
500 rainbow_brackets: true,
501 updatetime: 4000,
502 matchparen: true,
503 }
504 }
505}
506
507impl Options {
508 pub fn set_by_name(&mut self, name: &str, val: OptionValue) -> Result<(), EngineError> {
515 macro_rules! set_bool {
516 ($field:ident) => {{
517 self.$field = match val {
518 OptionValue::Bool(b) => b,
519 OptionValue::Int(n) => n != 0,
520 other => {
521 return Err(EngineError::Ex(format!(
522 "option `{name}` expects bool, got {other:?}"
523 )));
524 }
525 };
526 Ok(())
527 }};
528 }
529 macro_rules! set_u32 {
530 ($field:ident) => {{
531 self.$field = match val {
532 OptionValue::Int(n) if n >= 0 && n <= u32::MAX as i64 => n as u32,
533 OptionValue::Int(n) => {
534 return Err(EngineError::Ex(format!(
535 "option `{name}` out of u32 range: {n}"
536 )));
537 }
538 other => {
539 return Err(EngineError::Ex(format!(
540 "option `{name}` expects int, got {other:?}"
541 )));
542 }
543 };
544 Ok(())
545 }};
546 }
547 macro_rules! set_string {
548 ($field:ident) => {{
549 self.$field = match val {
550 OptionValue::String(s) => s,
551 other => {
552 return Err(EngineError::Ex(format!(
553 "option `{name}` expects string, got {other:?}"
554 )));
555 }
556 };
557 Ok(())
558 }};
559 }
560 match name {
561 "tabstop" | "ts" => set_u32!(tabstop),
562 "shiftwidth" | "sw" => set_u32!(shiftwidth),
563 "softtabstop" | "sts" => set_u32!(softtabstop),
564 "textwidth" | "tw" => set_u32!(textwidth),
565 "expandtab" | "et" => set_bool!(expandtab),
566 "iskeyword" | "isk" => set_string!(iskeyword),
567 "ignorecase" | "ic" => set_bool!(ignorecase),
568 "smartcase" | "scs" => set_bool!(smartcase),
569 "hlsearch" | "hls" => set_bool!(hlsearch),
570 "incsearch" | "is" => set_bool!(incsearch),
571 "wrapscan" | "ws" => set_bool!(wrapscan),
572 "autoindent" | "ai" => set_bool!(autoindent),
573 "smartindent" | "si" => set_bool!(smartindent),
574 "timeoutlen" | "tm" => {
575 self.timeout_len = match val {
576 OptionValue::Int(n) if n >= 0 => core::time::Duration::from_millis(n as u64),
577 other => {
578 return Err(EngineError::Ex(format!(
579 "option `{name}` expects non-negative int (millis), got {other:?}"
580 )));
581 }
582 };
583 Ok(())
584 }
585 "undolevels" | "ul" => set_u32!(undo_levels),
586 "undobreak" => set_bool!(undo_break_on_motion),
587 "readonly" | "ro" => set_bool!(readonly),
588 "modifiable" | "ma" => set_bool!(modifiable),
589 "wrap" => {
590 let on = match val {
591 OptionValue::Bool(b) => b,
592 OptionValue::Int(n) => n != 0,
593 other => {
594 return Err(EngineError::Ex(format!(
595 "option `{name}` expects bool, got {other:?}"
596 )));
597 }
598 };
599 self.wrap = match (on, self.wrap) {
600 (false, _) => WrapMode::None,
601 (true, WrapMode::Word) => WrapMode::Word,
602 (true, _) => WrapMode::Char,
603 };
604 Ok(())
605 }
606 "linebreak" | "lbr" => {
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 (true, _) => WrapMode::Word,
618 (false, WrapMode::Word) => WrapMode::Char,
619 (false, other) => other,
620 };
621 Ok(())
622 }
623 "number" | "nu" => set_bool!(number),
624 "relativenumber" | "rnu" => set_bool!(relativenumber),
625 "numberwidth" | "nuw" => {
626 self.numberwidth = match val {
627 OptionValue::Int(n) if (1..=20).contains(&n) => n as usize,
628 OptionValue::Int(n) => {
629 return Err(EngineError::Ex(format!(
630 "option `{name}` must be in range 1..=20, got {n}"
631 )));
632 }
633 other => {
634 return Err(EngineError::Ex(format!(
635 "option `{name}` expects int, got {other:?}"
636 )));
637 }
638 };
639 Ok(())
640 }
641 "cursorline" | "cul" => set_bool!(cursorline),
642 "cursorcolumn" | "cuc" => set_bool!(cursorcolumn),
643 "signcolumn" | "scl" => {
644 self.signcolumn = match val {
645 OptionValue::String(ref s) => match s.as_str() {
646 "yes" => SignColumnMode::Yes,
647 "no" => SignColumnMode::No,
648 "auto" => SignColumnMode::Auto,
649 other => {
650 return Err(EngineError::Ex(format!(
651 "option `{name}` must be `yes`, `no`, or `auto`, got {other:?}"
652 )));
653 }
654 },
655 other => {
656 return Err(EngineError::Ex(format!(
657 "option `{name}` expects string (yes/no/auto), got {other:?}"
658 )));
659 }
660 };
661 Ok(())
662 }
663 "foldcolumn" | "fdc" => {
664 self.foldcolumn = match val {
665 OptionValue::Int(n) if (0..=12).contains(&n) => n as u32,
666 OptionValue::Int(n) => {
667 return Err(EngineError::Ex(format!(
668 "option `{name}` must be in range 0..=12, got {n}"
669 )));
670 }
671 other => {
672 return Err(EngineError::Ex(format!(
673 "option `{name}` expects int (0-12), got {other:?}"
674 )));
675 }
676 };
677 Ok(())
678 }
679 "foldmethod" | "fdm" => {
680 self.foldmethod = match val {
681 OptionValue::String(ref s) => match s.as_str() {
682 "manual" => FoldMethod::Manual,
683 "expr" | "syntax" => FoldMethod::Expr,
684 "marker" => FoldMethod::Marker,
685 other => {
686 return Err(EngineError::Ex(format!(
687 "option `{name}` must be `manual`, `expr`, `syntax`, or `marker`, got `{other}`"
688 )));
689 }
690 },
691 other => {
692 return Err(EngineError::Ex(format!(
693 "option `{name}` expects string, got {other:?}"
694 )));
695 }
696 };
697 Ok(())
698 }
699 "foldenable" | "fen" => set_bool!(foldenable),
700 "foldlevelstart" | "fls" => set_u32!(foldlevelstart),
701 "colorcolumn" | "cc" => set_string!(colorcolumn),
702 "formatoptions" | "fo" => set_string!(formatoptions),
703 "filetype" | "ft" => set_string!(filetype),
704 "scrolloff" | "so" => {
705 self.scrolloff = match val {
706 OptionValue::Int(n) if n >= 0 => n as usize,
707 OptionValue::Int(n) => {
708 return Err(EngineError::Ex(format!(
709 "option `{name}` must be >= 0, got {n}"
710 )));
711 }
712 other => {
713 return Err(EngineError::Ex(format!(
714 "option `{name}` expects int, got {other:?}"
715 )));
716 }
717 };
718 Ok(())
719 }
720 "sidescrolloff" | "siso" => {
721 self.sidescrolloff = match val {
722 OptionValue::Int(n) if n >= 0 => n as usize,
723 OptionValue::Int(n) => {
724 return Err(EngineError::Ex(format!(
725 "option `{name}` must be >= 0, got {n}"
726 )));
727 }
728 other => {
729 return Err(EngineError::Ex(format!(
730 "option `{name}` expects int, got {other:?}"
731 )));
732 }
733 };
734 Ok(())
735 }
736 "modeline" | "ml" => set_bool!(modeline),
737 "autoreload" | "ar" => set_bool!(autoreload),
738 "modelines" | "mls" => set_u32!(modelines),
739 "motion_sneak" | "snk" => set_bool!(motion_sneak),
740 "list" => set_bool!(list),
741 "listchars" | "lcs" => {
742 let s = match val {
743 OptionValue::String(s) => s,
744 other => {
745 return Err(EngineError::Ex(format!(
746 "option `{name}` expects string, got {other:?}"
747 )));
748 }
749 };
750 self.listchars = ListChars::parse(&s).map_err(EngineError::Ex)?;
751 Ok(())
752 }
753 "indent_guides" | "ig" => set_bool!(indent_guides),
754 "colorizer" | "clz" => set_bool!(colorizer),
755 "colorizer_filetypes" | "clzft" => {
756 let s = match val {
757 OptionValue::String(s) => s,
758 other => {
759 return Err(EngineError::Ex(format!(
760 "option `{name}` expects string, got {other:?}"
761 )));
762 }
763 };
764 self.colorizer_filetypes = s
765 .split(',')
766 .map(|p| p.trim().to_string())
767 .filter(|p| !p.is_empty())
768 .collect();
769 Ok(())
770 }
771 "indent_guide_char" | "igc" => {
772 let s = match val {
773 OptionValue::String(s) => s,
774 other => {
775 return Err(EngineError::Ex(format!(
776 "option `{name}` expects a single-char string, got {other:?}"
777 )));
778 }
779 };
780 let mut chars = s.chars();
781 let ch = match (chars.next(), chars.next()) {
782 (Some(c), None) => c,
783 _ => {
784 return Err(EngineError::Ex(format!(
785 "option `{name}` expects exactly one character, got {s:?}"
786 )));
787 }
788 };
789 self.indent_guide_char = ch;
790 Ok(())
791 }
792 "format_on_save" | "fos" => set_bool!(format_on_save),
793 "trim_trailing_whitespace" | "tts" => set_bool!(trim_trailing_whitespace),
794 "rainbow_brackets" | "rb" => set_bool!(rainbow_brackets),
795 "updatetime" | "ut" => set_u32!(updatetime),
796 "matchparen" | "mps" => set_bool!(matchparen),
797 other => Err(EngineError::Ex(format!("unknown option `{other}`"))),
798 }
799 }
800
801 pub fn get_by_name(&self, name: &str) -> Option<OptionValue> {
803 Some(match name {
804 "tabstop" | "ts" => OptionValue::Int(self.tabstop as i64),
805 "shiftwidth" | "sw" => OptionValue::Int(self.shiftwidth as i64),
806 "softtabstop" | "sts" => OptionValue::Int(self.softtabstop as i64),
807 "textwidth" | "tw" => OptionValue::Int(self.textwidth as i64),
808 "expandtab" | "et" => OptionValue::Bool(self.expandtab),
809 "iskeyword" | "isk" => OptionValue::String(self.iskeyword.clone()),
810 "ignorecase" | "ic" => OptionValue::Bool(self.ignorecase),
811 "smartcase" | "scs" => OptionValue::Bool(self.smartcase),
812 "hlsearch" | "hls" => OptionValue::Bool(self.hlsearch),
813 "incsearch" | "is" => OptionValue::Bool(self.incsearch),
814 "wrapscan" | "ws" => OptionValue::Bool(self.wrapscan),
815 "autoindent" | "ai" => OptionValue::Bool(self.autoindent),
816 "smartindent" | "si" => OptionValue::Bool(self.smartindent),
817 "timeoutlen" | "tm" => OptionValue::Int(self.timeout_len.as_millis() as i64),
818 "undolevels" | "ul" => OptionValue::Int(self.undo_levels as i64),
819 "undobreak" => OptionValue::Bool(self.undo_break_on_motion),
820 "readonly" | "ro" => OptionValue::Bool(self.readonly),
821 "modifiable" | "ma" => OptionValue::Bool(self.modifiable),
822 "wrap" => OptionValue::Bool(!matches!(self.wrap, WrapMode::None)),
823 "linebreak" | "lbr" => OptionValue::Bool(matches!(self.wrap, WrapMode::Word)),
824 "number" | "nu" => OptionValue::Bool(self.number),
825 "relativenumber" | "rnu" => OptionValue::Bool(self.relativenumber),
826 "numberwidth" | "nuw" => OptionValue::Int(self.numberwidth as i64),
827 "cursorline" | "cul" => OptionValue::Bool(self.cursorline),
828 "cursorcolumn" | "cuc" => OptionValue::Bool(self.cursorcolumn),
829 "signcolumn" | "scl" => OptionValue::String(
830 match self.signcolumn {
831 SignColumnMode::Yes => "yes",
832 SignColumnMode::No => "no",
833 SignColumnMode::Auto => "auto",
834 }
835 .to_string(),
836 ),
837 "foldcolumn" | "fdc" => OptionValue::Int(self.foldcolumn as i64),
838 "foldmethod" | "fdm" => OptionValue::String(
839 match self.foldmethod {
840 FoldMethod::Manual => "manual",
841 FoldMethod::Expr => "expr",
842 FoldMethod::Marker => "marker",
843 }
844 .to_string(),
845 ),
846 "foldenable" | "fen" => OptionValue::Bool(self.foldenable),
847 "foldlevelstart" | "fls" => OptionValue::Int(self.foldlevelstart as i64),
848 "colorcolumn" | "cc" => OptionValue::String(self.colorcolumn.clone()),
849 "formatoptions" | "fo" => OptionValue::String(self.formatoptions.clone()),
850 "filetype" | "ft" => OptionValue::String(self.filetype.clone()),
851 "scrolloff" | "so" => OptionValue::Int(self.scrolloff as i64),
852 "sidescrolloff" | "siso" => OptionValue::Int(self.sidescrolloff as i64),
853 "modeline" | "ml" => OptionValue::Bool(self.modeline),
854 "autoreload" | "ar" => OptionValue::Bool(self.autoreload),
855 "modelines" | "mls" => OptionValue::Int(self.modelines as i64),
856 "motion_sneak" | "snk" => OptionValue::Bool(self.motion_sneak),
857 "list" => OptionValue::Bool(self.list),
858 "listchars" | "lcs" => OptionValue::String(self.listchars.to_canonical_string()),
859 "indent_guides" | "ig" => OptionValue::Bool(self.indent_guides),
860 "indent_guide_char" | "igc" => OptionValue::String(self.indent_guide_char.to_string()),
861 "colorizer" | "clz" => OptionValue::Bool(self.colorizer),
862 "colorizer_filetypes" | "clzft" => {
863 OptionValue::String(self.colorizer_filetypes.join(","))
864 }
865 "format_on_save" | "fos" => OptionValue::Bool(self.format_on_save),
866 "trim_trailing_whitespace" | "tts" => OptionValue::Bool(self.trim_trailing_whitespace),
867 "rainbow_brackets" | "rb" => OptionValue::Bool(self.rainbow_brackets),
868 "updatetime" | "ut" => OptionValue::Int(self.updatetime as i64),
869 "matchparen" | "mps" => OptionValue::Bool(self.matchparen),
870 _ => return None,
871 })
872 }
873}
874
875pub use hjkl_buffer::Viewport;
897
898#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
902pub struct BufferId(pub u64);
903
904#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
906pub struct Modifiers {
907 pub ctrl: bool,
908 pub shift: bool,
909 pub alt: bool,
910 pub super_: bool,
911}
912
913#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
915#[non_exhaustive]
916pub enum SpecialKey {
917 Esc,
918 Enter,
919 Backspace,
920 Tab,
921 BackTab,
922 Up,
923 Down,
924 Left,
925 Right,
926 Home,
927 End,
928 PageUp,
929 PageDown,
930 Insert,
931 Delete,
932 F(u8),
933}
934
935#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
936pub enum MouseKind {
937 Press,
938 Release,
939 Drag,
940 ScrollUp,
941 ScrollDown,
942}
943
944#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
945pub struct MouseEvent {
946 pub kind: MouseKind,
947 pub pos: Pos,
948 pub mods: Modifiers,
949}
950
951#[derive(Debug, Clone, PartialEq, Eq)]
956#[non_exhaustive]
957pub enum Input {
958 Char(char, Modifiers),
959 Key(SpecialKey, Modifiers),
960 Mouse(MouseEvent),
961 Paste(String),
962 FocusGained,
963 FocusLost,
964 Resize(u16, u16),
965}
966
967pub trait Host: Send {
976 type Intent;
980
981 fn write_clipboard(&mut self, text: String);
987
988 fn read_clipboard(&mut self) -> Option<String>;
991
992 fn now(&self) -> core::time::Duration;
998
999 fn should_cancel(&self) -> bool {
1002 false
1003 }
1004
1005 fn prompt_search(&mut self) -> Option<String>;
1010
1011 fn display_line_for(&self, pos: Pos) -> u32 {
1016 pos.line
1017 }
1018
1019 fn pos_for_display(&self, line: u32, col: u32) -> Pos {
1021 Pos { line, col }
1022 }
1023
1024 fn syntax_highlights(&self, range: Range<Pos>) -> Vec<Highlight> {
1029 let _ = range;
1030 Vec::new()
1031 }
1032
1033 fn emit_cursor_shape(&mut self, shape: CursorShape);
1038
1039 fn viewport(&self) -> &Viewport;
1046
1047 fn viewport_mut(&mut self) -> &mut Viewport;
1050
1051 fn emit_intent(&mut self, intent: Self::Intent);
1056}
1057
1058#[derive(Debug)]
1072pub struct DefaultHost {
1073 clipboard: Option<String>,
1074 last_cursor_shape: CursorShape,
1075 started: std::time::Instant,
1076 viewport: Viewport,
1077}
1078
1079impl Default for DefaultHost {
1080 fn default() -> Self {
1081 Self::new()
1082 }
1083}
1084
1085impl DefaultHost {
1086 pub const DEFAULT_VIEWPORT: Viewport = Viewport {
1089 top_row: 0,
1090 top_col: 0,
1091 width: 80,
1092 height: 24,
1093 wrap: hjkl_buffer::Wrap::None,
1094 text_width: 80,
1095 tab_width: 0,
1096 };
1097
1098 pub fn new() -> Self {
1099 Self {
1100 clipboard: None,
1101 last_cursor_shape: CursorShape::Block,
1102 started: std::time::Instant::now(),
1103 viewport: Self::DEFAULT_VIEWPORT,
1104 }
1105 }
1106
1107 pub fn with_viewport(viewport: Viewport) -> Self {
1111 Self {
1112 clipboard: None,
1113 last_cursor_shape: CursorShape::Block,
1114 started: std::time::Instant::now(),
1115 viewport,
1116 }
1117 }
1118
1119 pub fn last_cursor_shape(&self) -> CursorShape {
1121 self.last_cursor_shape
1122 }
1123}
1124
1125impl Host for DefaultHost {
1126 type Intent = ();
1127
1128 fn write_clipboard(&mut self, text: String) {
1129 self.clipboard = Some(text);
1130 }
1131
1132 fn read_clipboard(&mut self) -> Option<String> {
1133 self.clipboard.clone()
1134 }
1135
1136 fn now(&self) -> core::time::Duration {
1137 self.started.elapsed()
1138 }
1139
1140 fn prompt_search(&mut self) -> Option<String> {
1141 None
1142 }
1143
1144 fn emit_cursor_shape(&mut self, shape: CursorShape) {
1145 self.last_cursor_shape = shape;
1146 }
1147
1148 fn viewport(&self) -> &Viewport {
1149 &self.viewport
1150 }
1151
1152 fn viewport_mut(&mut self) -> &mut Viewport {
1153 &mut self.viewport
1154 }
1155
1156 fn emit_intent(&mut self, _intent: Self::Intent) {}
1157}
1158
1159#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
1171pub struct RenderFrame {
1172 pub mode: SnapshotMode,
1173 pub cursor_row: u32,
1174 pub cursor_col: u32,
1175 pub cursor_shape: CursorShape,
1176 pub viewport_top: u32,
1177 pub line_count: u32,
1178}
1179
1180#[derive(Debug, Clone)]
1207#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1208pub struct EditorSnapshot {
1209 pub version: u32,
1212 pub mode: SnapshotMode,
1214 pub cursor: (u32, u32),
1216 pub lines: Vec<String>,
1218 pub viewport_top: u32,
1220 pub registers: crate::Registers,
1224 pub marks: std::collections::BTreeMap<char, (u32, u32)>,
1231 pub global_marks: std::collections::BTreeMap<char, (u64, u32, u32)>,
1235}
1236
1237#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
1241#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1242pub enum SnapshotMode {
1243 #[default]
1244 Normal,
1245 Insert,
1246 Visual,
1247 VisualLine,
1248 VisualBlock,
1249}
1250
1251impl EditorSnapshot {
1252 pub const VERSION: u32 = 5;
1273}
1274
1275#[derive(Debug, thiserror::Error)]
1279pub enum EngineError {
1280 #[error("regex compile error: {0}")]
1283 Regex(#[from] regex::Error),
1284
1285 #[error("invalid range: {0}")]
1287 InvalidRange(String),
1288
1289 #[error("ex parse: {0}")]
1291 Ex(String),
1292
1293 #[error("buffer is read-only")]
1295 ReadOnly,
1296
1297 #[error("position out of bounds: {0:?}")]
1299 OutOfBounds(Pos),
1300
1301 #[error("snapshot version mismatch: file={0}, expected={1}")]
1304 SnapshotVersion(u32, u32),
1305}
1306
1307pub(crate) mod sealed {
1308 pub trait Sealed {}
1318}
1319
1320pub trait Cursor: Send {
1326 fn cursor(&self) -> Pos;
1328 fn set_cursor(&mut self, pos: Pos);
1330 fn byte_offset(&self, pos: Pos) -> usize;
1332 fn pos_at_byte(&self, byte: usize) -> Pos;
1334}
1335
1336pub trait Query: Send {
1338 fn line_count(&self) -> u32;
1340 fn line(&self, idx: u32) -> String;
1343 fn len_bytes(&self) -> usize;
1345 fn slice(&self, range: core::ops::Range<Pos>) -> std::borrow::Cow<'_, str>;
1350 fn dirty_gen(&self) -> u64 {
1364 0
1365 }
1366
1367 fn byte_of_row(&self, row: usize) -> usize {
1378 let n = self.line_count() as usize;
1379 let row = row.min(n);
1380 let mut acc = 0usize;
1381 for r in 0..row {
1382 acc += self.line(r as u32).len();
1383 if r + 1 < n {
1388 acc += 1;
1389 }
1390 }
1391 acc
1392 }
1393
1394 fn content_joined(&self) -> std::sync::Arc<String> {
1403 let n = self.line_count() as usize;
1404 let mut acc = String::with_capacity(self.len_bytes());
1405 for r in 0..n {
1406 if r > 0 {
1407 acc.push('\n');
1408 }
1409 acc.push_str(&self.line(r as u32));
1410 }
1411 std::sync::Arc::new(acc)
1412 }
1413
1414 fn line_bytes(&self, row: usize) -> usize {
1422 let n = self.line_count() as usize;
1423 if row >= n {
1424 return 0;
1425 }
1426 self.line(row as u32).len()
1427 }
1428
1429 fn rope(&self) -> ropey::Rope {
1438 ropey::Rope::from_str(&self.content_joined())
1439 }
1440}
1441
1442pub trait BufferEdit: Send {
1446 fn insert_at(&mut self, pos: Pos, text: &str);
1449 fn delete_range(&mut self, range: core::ops::Range<Pos>);
1451 fn replace_range(&mut self, range: core::ops::Range<Pos>, replacement: &str);
1453 fn replace_all(&mut self, text: &str) {
1461 self.replace_range(
1462 Pos::ORIGIN..Pos {
1463 line: u32::MAX,
1464 col: u32::MAX,
1465 },
1466 text,
1467 );
1468 }
1469}
1470
1471pub trait Search: Send {
1474 fn find_next(&self, from: Pos, pat: ®ex::Regex) -> Option<core::ops::Range<Pos>>;
1476 fn find_prev(&self, from: Pos, pat: ®ex::Regex) -> Option<core::ops::Range<Pos>>;
1478}
1479
1480pub trait View: Cursor + Query + BufferEdit + Search + sealed::Sealed + Send {}
1488
1489pub trait FoldProvider: Send {
1510 fn next_visible_row(&self, row: usize, row_count: usize) -> Option<usize>;
1513 fn prev_visible_row(&self, row: usize) -> Option<usize>;
1515 fn is_row_hidden(&self, row: usize) -> bool;
1517 fn fold_at_row(&self, row: usize) -> Option<(usize, usize, bool)>;
1521
1522 fn apply(&mut self, op: FoldOp) {
1532 let _ = op;
1533 }
1534
1535 fn invalidate_range(&mut self, start_row: usize, end_row: usize) {
1540 self.apply(FoldOp::Invalidate { start_row, end_row });
1541 }
1542}
1543
1544#[derive(Debug, Default, Clone, Copy)]
1547pub struct NoopFoldProvider;
1548
1549impl FoldProvider for NoopFoldProvider {
1550 fn next_visible_row(&self, row: usize, row_count: usize) -> Option<usize> {
1551 let last = row_count.saturating_sub(1);
1552 if last == 0 && row == 0 {
1553 return None;
1554 }
1555 let r = row.checked_add(1)?;
1556 (r <= last).then_some(r)
1557 }
1558
1559 fn prev_visible_row(&self, row: usize) -> Option<usize> {
1560 row.checked_sub(1)
1561 }
1562
1563 fn is_row_hidden(&self, _row: usize) -> bool {
1564 false
1565 }
1566
1567 fn fold_at_row(&self, _row: usize) -> Option<(usize, usize, bool)> {
1568 None
1569 }
1570}
1571
1572#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1574pub enum InsertDir {
1575 Left,
1576 Right,
1577 Up,
1578 Down,
1579}
1580
1581#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1584pub enum ScrollDir {
1585 Down,
1587 Up,
1589}
1590
1591pub const SEARCH_HISTORY_MAX: usize = 100;
1592pub const CHANGE_LIST_MAX: usize = 100;
1593
1594pub const JUMPLIST_MAX: usize = 100;
1596
1597#[cfg(test)]
1598mod tests {
1599 use super::*;
1600
1601 #[test]
1602 fn caret_is_empty() {
1603 let sel = Selection::caret(Pos::new(2, 4));
1604 assert!(sel.is_empty());
1605 assert_eq!(sel.anchor, sel.head);
1606 }
1607
1608 #[test]
1609 fn selection_set_default_has_one_caret() {
1610 let set = SelectionSet::default();
1611 assert_eq!(set.items.len(), 1);
1612 assert_eq!(set.primary, 0);
1613 assert_eq!(set.primary().anchor, Pos::ORIGIN);
1614 }
1615
1616 #[test]
1617 fn edit_constructors() {
1618 let p = Pos::new(0, 5);
1619 assert_eq!(Edit::insert(p, "x").range, p..p);
1620 assert!(Edit::insert(p, "x").replacement == "x");
1621 assert!(Edit::delete(p..p).replacement.is_empty());
1622 }
1623
1624 #[test]
1625 fn attrs_flags() {
1626 let a = Attrs::BOLD | Attrs::UNDERLINE;
1627 assert!(a.contains(Attrs::BOLD));
1628 assert!(!a.contains(Attrs::ITALIC));
1629 }
1630
1631 #[test]
1632 fn options_set_get_roundtrip() {
1633 let mut o = Options::default();
1634 o.set_by_name("tabstop", OptionValue::Int(4)).unwrap();
1635 assert!(matches!(o.get_by_name("ts"), Some(OptionValue::Int(4))));
1636 o.set_by_name("expandtab", OptionValue::Bool(true)).unwrap();
1637 assert!(matches!(o.get_by_name("et"), Some(OptionValue::Bool(true))));
1638 o.set_by_name("iskeyword", OptionValue::String("a-z".into()))
1639 .unwrap();
1640 match o.get_by_name("iskeyword") {
1641 Some(OptionValue::String(s)) => assert_eq!(s, "a-z"),
1642 other => panic!("expected String, got {other:?}"),
1643 }
1644 }
1645
1646 #[test]
1647 fn options_unknown_name_errors_on_set() {
1648 let mut o = Options::default();
1649 assert!(matches!(
1650 o.set_by_name("frobnicate", OptionValue::Int(1)),
1651 Err(EngineError::Ex(_))
1652 ));
1653 assert!(o.get_by_name("frobnicate").is_none());
1654 }
1655
1656 #[test]
1666 fn set_by_name_rejects_makeprg_and_errorformat() {
1667 let mut o = Options::default();
1668 assert!(
1669 matches!(
1670 o.set_by_name("makeprg", OptionValue::String("rm -rf /".into())),
1671 Err(EngineError::Ex(_))
1672 ),
1673 "`makeprg` must never be a settable option — a modeline must \
1674 never be able to smuggle an arbitrary shell command into `:make`"
1675 );
1676 assert!(o.get_by_name("makeprg").is_none());
1677 assert!(
1678 matches!(
1679 o.set_by_name("errorformat", OptionValue::String("%f:%l:%m".into())),
1680 Err(EngineError::Ex(_))
1681 ),
1682 "`errorformat` rides the same `:make`/`:grep` shell-out surface \
1683 as `makeprg` and must stay unsettable too"
1684 );
1685 assert!(o.get_by_name("errorformat").is_none());
1686 }
1687
1688 #[test]
1689 fn options_type_mismatch_errors() {
1690 let mut o = Options::default();
1691 assert!(matches!(
1692 o.set_by_name("tabstop", OptionValue::String("nope".into())),
1693 Err(EngineError::Ex(_))
1694 ));
1695 assert!(matches!(
1696 o.set_by_name("iskeyword", OptionValue::Int(7)),
1697 Err(EngineError::Ex(_))
1698 ));
1699 }
1700
1701 #[test]
1704 fn default_options_ignorecase_and_smartcase_are_true() {
1705 let o = Options::default();
1706 assert!(o.ignorecase, "ignorecase must default to true");
1707 assert!(o.smartcase, "smartcase must default to true");
1708 }
1709
1710 #[test]
1711 fn options_int_to_bool_coercion() {
1712 let mut o = Options::default();
1715 o.set_by_name("ignorecase", OptionValue::Int(1)).unwrap();
1716 assert!(matches!(o.get_by_name("ic"), Some(OptionValue::Bool(true))));
1717 o.set_by_name("ignorecase", OptionValue::Int(0)).unwrap();
1718 assert!(matches!(
1719 o.get_by_name("ic"),
1720 Some(OptionValue::Bool(false))
1721 ));
1722 }
1723
1724 #[test]
1725 fn options_wrap_linebreak_roundtrip() {
1726 let mut o = Options::default();
1727 assert_eq!(o.wrap, WrapMode::None);
1728 o.set_by_name("wrap", OptionValue::Bool(true)).unwrap();
1729 assert_eq!(o.wrap, WrapMode::Char);
1730 o.set_by_name("linebreak", OptionValue::Bool(true)).unwrap();
1731 assert_eq!(o.wrap, WrapMode::Word);
1732 assert!(matches!(
1733 o.get_by_name("wrap"),
1734 Some(OptionValue::Bool(true))
1735 ));
1736 assert!(matches!(
1737 o.get_by_name("lbr"),
1738 Some(OptionValue::Bool(true))
1739 ));
1740 o.set_by_name("linebreak", OptionValue::Bool(false))
1741 .unwrap();
1742 assert_eq!(o.wrap, WrapMode::Char);
1743 o.set_by_name("wrap", OptionValue::Bool(false)).unwrap();
1744 assert_eq!(o.wrap, WrapMode::None);
1745 }
1746
1747 #[test]
1748 fn options_default_modern() {
1749 let o = Options::default();
1752 assert_eq!(o.tabstop, 4);
1753 assert_eq!(o.shiftwidth, 4);
1754 assert_eq!(o.softtabstop, 4);
1755 assert!(o.expandtab);
1756 assert!(o.hlsearch);
1757 assert!(o.wrapscan);
1758 assert!(o.smartindent);
1759 assert_eq!(o.timeout_len, core::time::Duration::from_millis(1000));
1760 }
1761
1762 #[test]
1763 fn editor_snapshot_version_const() {
1764 assert_eq!(EditorSnapshot::VERSION, 5);
1765 }
1766
1767 #[test]
1768 fn editor_snapshot_default_shape() {
1769 let s = EditorSnapshot {
1770 version: EditorSnapshot::VERSION,
1771 mode: SnapshotMode::Normal,
1772 cursor: (0, 0),
1773 lines: vec!["hello".to_string()],
1774 viewport_top: 0,
1775 registers: crate::Registers::default(),
1776 marks: Default::default(),
1777 global_marks: Default::default(),
1778 };
1779 assert_eq!(s.cursor, (0, 0));
1780 assert_eq!(s.lines.len(), 1);
1781 }
1782
1783 #[cfg(feature = "serde")]
1784 #[test]
1785 fn editor_snapshot_roundtrip() {
1786 let mut marks = std::collections::BTreeMap::new();
1787 marks.insert('a', (1u32, 0u32));
1788 let mut global_marks = std::collections::BTreeMap::new();
1789 global_marks.insert('A', (42u64, 5u32, 2u32));
1790 let s = EditorSnapshot {
1791 version: EditorSnapshot::VERSION,
1792 mode: SnapshotMode::Insert,
1793 cursor: (3, 7),
1794 lines: vec!["alpha".into(), "beta".into()],
1795 viewport_top: 2,
1796 registers: crate::Registers::default(),
1797 marks,
1798 global_marks,
1799 };
1800 let json = serde_json::to_string(&s).unwrap();
1801 let back: EditorSnapshot = serde_json::from_str(&json).unwrap();
1802 assert_eq!(s.cursor, back.cursor);
1803 assert_eq!(s.lines, back.lines);
1804 assert_eq!(s.viewport_top, back.viewport_top);
1805 assert_eq!(s.global_marks, back.global_marks);
1806 }
1807
1808 #[test]
1809 fn engine_error_display() {
1810 let e = EngineError::ReadOnly;
1811 assert_eq!(e.to_string(), "buffer is read-only");
1812 let e = EngineError::OutOfBounds(Pos::new(3, 7));
1813 assert!(e.to_string().contains("out of bounds"));
1814 }
1815
1816 #[test]
1819 fn options_cursorline_roundtrip() {
1820 let mut o = Options::default();
1821 assert!(o.cursorline, "cursorline defaults to true");
1822 o.set_by_name("cursorline", OptionValue::Bool(false))
1823 .unwrap();
1824 assert!(matches!(
1825 o.get_by_name("cul"),
1826 Some(OptionValue::Bool(false))
1827 ));
1828 o.set_by_name("cul", OptionValue::Bool(true)).unwrap();
1829 assert!(matches!(
1830 o.get_by_name("cursorline"),
1831 Some(OptionValue::Bool(true))
1832 ));
1833 }
1834
1835 #[test]
1836 fn options_cursorcolumn_roundtrip() {
1837 let mut o = Options::default();
1838 assert!(!o.cursorcolumn, "cursorcolumn defaults to false");
1839 o.set_by_name("cuc", OptionValue::Bool(true)).unwrap();
1840 assert!(matches!(
1841 o.get_by_name("cursorcolumn"),
1842 Some(OptionValue::Bool(true))
1843 ));
1844 }
1845
1846 #[test]
1847 fn options_signcolumn_roundtrip() {
1848 let mut o = Options::default();
1849 assert_eq!(
1850 o.signcolumn,
1851 SignColumnMode::Auto,
1852 "signcolumn defaults to auto"
1853 );
1854 o.set_by_name("signcolumn", OptionValue::String("yes".into()))
1855 .unwrap();
1856 assert_eq!(o.signcolumn, SignColumnMode::Yes);
1857 assert_eq!(
1858 o.get_by_name("scl"),
1859 Some(OptionValue::String("yes".into()))
1860 );
1861 o.set_by_name("scl", OptionValue::String("no".into()))
1862 .unwrap();
1863 assert_eq!(o.signcolumn, SignColumnMode::No);
1864 o.set_by_name("scl", OptionValue::String("auto".into()))
1865 .unwrap();
1866 assert_eq!(o.signcolumn, SignColumnMode::Auto);
1867 }
1868
1869 #[test]
1870 fn options_signcolumn_rejects_invalid() {
1871 let mut o = Options::default();
1872 assert!(matches!(
1873 o.set_by_name("signcolumn", OptionValue::String("maybe".into())),
1874 Err(EngineError::Ex(_))
1875 ));
1876 assert!(matches!(
1878 o.set_by_name("signcolumn", OptionValue::Bool(true)),
1879 Err(EngineError::Ex(_))
1880 ));
1881 }
1882
1883 #[test]
1884 fn options_foldcolumn_roundtrip() {
1885 let mut o = Options::default();
1886 assert_eq!(o.foldcolumn, 0, "foldcolumn defaults to 0");
1887 o.set_by_name("fdc", OptionValue::Int(3)).unwrap();
1888 assert_eq!(o.foldcolumn, 3);
1889 assert_eq!(o.get_by_name("foldcolumn"), Some(OptionValue::Int(3)));
1890 }
1891
1892 #[test]
1893 fn options_foldcolumn_rejects_out_of_range() {
1894 let mut o = Options::default();
1895 assert!(matches!(
1896 o.set_by_name("foldcolumn", OptionValue::Int(13)),
1897 Err(EngineError::Ex(_))
1898 ));
1899 assert!(matches!(
1900 o.set_by_name("foldcolumn", OptionValue::Int(-1)),
1901 Err(EngineError::Ex(_))
1902 ));
1903 }
1904
1905 #[test]
1906 fn options_colorcolumn_roundtrip() {
1907 let mut o = Options::default();
1908 assert_eq!(o.colorcolumn, "", "colorcolumn defaults to empty string");
1909 o.set_by_name("cc", OptionValue::String("80,120".into()))
1910 .unwrap();
1911 assert_eq!(
1912 o.get_by_name("colorcolumn"),
1913 Some(OptionValue::String("80,120".into()))
1914 );
1915 o.set_by_name("colorcolumn", OptionValue::String(String::new()))
1916 .unwrap();
1917 assert_eq!(
1918 o.get_by_name("cc"),
1919 Some(OptionValue::String(String::new()))
1920 );
1921 }
1922
1923 #[test]
1924 fn options_cursorline_alias_cul() {
1925 let mut o = Options::default();
1926 o.set_by_name("cul", OptionValue::Bool(true)).unwrap();
1928 assert!(o.cursorline);
1929 o.set_by_name("cul", OptionValue::Bool(false)).unwrap();
1931 assert!(!o.cursorline);
1932 }
1933
1934 #[test]
1935 fn sign_column_mode_default_is_auto() {
1936 assert_eq!(SignColumnMode::default(), SignColumnMode::Auto);
1937 }
1938
1939 #[test]
1940 fn options_scrolloff_default_and_set() {
1941 let mut o = Options::default();
1942 assert_eq!(o.scrolloff, 5, "scrolloff defaults to 5");
1943 o.set_by_name("scrolloff", OptionValue::Int(0)).unwrap();
1944 assert_eq!(o.scrolloff, 0);
1945 o.set_by_name("scrolloff", OptionValue::Int(999)).unwrap();
1946 assert_eq!(o.scrolloff, 999);
1947 assert_eq!(o.get_by_name("scrolloff"), Some(OptionValue::Int(999)));
1948 }
1949
1950 #[test]
1951 fn options_sidescrolloff_default_and_set() {
1952 let mut o = Options::default();
1953 assert_eq!(o.sidescrolloff, 0, "sidescrolloff defaults to 0");
1954 o.set_by_name("sidescrolloff", OptionValue::Int(5)).unwrap();
1955 assert_eq!(o.sidescrolloff, 5);
1956 assert_eq!(o.get_by_name("sidescrolloff"), Some(OptionValue::Int(5)));
1957 }
1958
1959 #[test]
1960 fn options_alias_so_siso() {
1961 let mut o = Options::default();
1962 o.set_by_name("so", OptionValue::Int(3)).unwrap();
1964 assert_eq!(o.scrolloff, 3);
1965 assert_eq!(o.get_by_name("so"), Some(OptionValue::Int(3)));
1966 o.set_by_name("siso", OptionValue::Int(2)).unwrap();
1968 assert_eq!(o.sidescrolloff, 2);
1969 assert_eq!(o.get_by_name("siso"), Some(OptionValue::Int(2)));
1970 }
1971
1972 #[test]
1975 fn options_list_default_false_and_set() {
1976 let mut o = Options::default();
1977 assert!(!o.list, "list default is false");
1978 o.set_by_name("list", OptionValue::Bool(true)).unwrap();
1979 assert!(o.list);
1980 assert_eq!(o.get_by_name("list"), Some(OptionValue::Bool(true)));
1981 o.set_by_name("list", OptionValue::Bool(false)).unwrap();
1982 assert!(!o.list);
1983 }
1984
1985 #[test]
1986 fn options_listchars_default_matches_vim() {
1987 let o = Options::default();
1988 let lc = &o.listchars;
1989 assert_eq!(lc.tab_lead, '^');
1990 assert_eq!(lc.tab_fill, Some('I'));
1991 assert_eq!(lc.eol, Some('$'));
1992 assert_eq!(lc.space, None);
1993 assert_eq!(lc.trail, None);
1994 assert_eq!(lc.nbsp, None);
1995 }
1996
1997 #[test]
1998 fn options_listchars_set_and_get() {
1999 let mut o = Options::default();
2000 o.set_by_name("listchars", OptionValue::String("tab:>-,eol:$".to_string()))
2001 .unwrap();
2002 assert_eq!(o.listchars.tab_lead, '>');
2003 assert_eq!(o.listchars.tab_fill, Some('-'));
2004 assert_eq!(o.listchars.eol, Some('$'));
2005 }
2006
2007 #[test]
2008 fn options_lcs_alias_sets_listchars() {
2009 let mut o = Options::default();
2010 o.set_by_name("lcs", OptionValue::String("tab:>-,trail:~".to_string()))
2011 .unwrap();
2012 assert_eq!(o.listchars.tab_lead, '>');
2013 assert_eq!(o.listchars.trail, Some('~'));
2014 }
2015
2016 #[test]
2017 fn options_listchars_get_by_name_returns_string() {
2018 let o = Options::default();
2019 match o.get_by_name("listchars") {
2020 Some(OptionValue::String(s)) => {
2021 assert!(s.contains("tab:"), "canonical string should contain tab:");
2022 }
2023 other => panic!("expected String, got {other:?}"),
2024 }
2025 }
2026
2027 #[test]
2028 fn options_listchars_invalid_value_returns_err() {
2029 let mut o = Options::default();
2030 assert!(
2031 o.set_by_name("listchars", OptionValue::String("bogus:x".to_string()))
2032 .is_err()
2033 );
2034 }
2035
2036 #[test]
2039 fn indent_guides_default_true() {
2040 assert!(
2041 Options::default().indent_guides,
2042 "indent_guides must default to true"
2043 );
2044 }
2045
2046 #[test]
2047 fn options_indent_guides_set_and_get() {
2048 let mut opts = Options::default();
2049 opts.set_by_name("indent_guides", OptionValue::Bool(false))
2051 .unwrap();
2052 assert!(!opts.indent_guides);
2053 opts.set_by_name("ig", OptionValue::Bool(true)).unwrap();
2055 assert!(opts.indent_guides);
2056 assert_eq!(opts.get_by_name("ig"), Some(OptionValue::Bool(true)));
2058 assert_eq!(
2059 opts.get_by_name("indent_guides"),
2060 Some(OptionValue::Bool(true))
2061 );
2062 }
2063
2064 #[test]
2065 fn options_indent_guide_char_set_and_get() {
2066 let mut opts = Options::default();
2067 opts.set_by_name("indent_guide_char", OptionValue::String(":".to_string()))
2068 .unwrap();
2069 assert_eq!(opts.indent_guide_char, ':');
2070 opts.set_by_name("igc", OptionValue::String("┊".to_string()))
2072 .unwrap();
2073 assert_eq!(opts.indent_guide_char, '┊');
2074 assert_eq!(
2076 opts.get_by_name("igc"),
2077 Some(OptionValue::String("┊".to_string()))
2078 );
2079 assert_eq!(
2080 opts.get_by_name("indent_guide_char"),
2081 Some(OptionValue::String("┊".to_string()))
2082 );
2083 }
2084
2085 #[test]
2086 fn options_indent_guide_char_rejects_multi_char() {
2087 let mut opts = Options::default();
2088 assert!(
2089 opts.set_by_name("indent_guide_char", OptionValue::String("ab".to_string()))
2090 .is_err(),
2091 "multi-char value must be rejected"
2092 );
2093 }
2094
2095 #[test]
2096 fn options_indent_guide_char_rejects_empty() {
2097 let mut opts = Options::default();
2098 assert!(
2099 opts.set_by_name("indent_guide_char", OptionValue::String(String::new()))
2100 .is_err(),
2101 "empty string must be rejected"
2102 );
2103 }
2104
2105 #[test]
2108 fn colorizer_default_true() {
2109 assert!(
2110 Options::default().colorizer,
2111 "colorizer must default to true"
2112 );
2113 }
2114
2115 #[test]
2116 fn colorizer_filetypes_includes_css() {
2117 let o = Options::default();
2118 assert!(
2119 o.colorizer_filetypes.iter().any(|f| f == "css"),
2120 "default colorizer_filetypes must include 'css'"
2121 );
2122 }
2123
2124 #[test]
2125 fn options_colorizer_set_and_get() {
2126 let mut o = Options::default();
2127 o.set_by_name("colorizer", OptionValue::Bool(false))
2128 .unwrap();
2129 assert_eq!(o.get_by_name("colorizer"), Some(OptionValue::Bool(false)));
2130 o.set_by_name("clz", OptionValue::Bool(true)).unwrap();
2131 assert_eq!(o.get_by_name("clz"), Some(OptionValue::Bool(true)));
2132 }
2133
2134 #[test]
2135 fn options_colorizer_filetypes_set_and_get() {
2136 let mut o = Options::default();
2137 o.set_by_name(
2138 "colorizer_filetypes",
2139 OptionValue::String("css,scss,toml".into()),
2140 )
2141 .unwrap();
2142 assert_eq!(o.colorizer_filetypes, vec!["css", "scss", "toml"]);
2143 assert_eq!(
2144 o.get_by_name("clzft"),
2145 Some(OptionValue::String("css,scss,toml".into()))
2146 );
2147 }
2148
2149 #[test]
2152 fn format_on_save_default_true() {
2153 let o = Options::default();
2154 assert!(o.format_on_save, "format_on_save must default to true");
2155 }
2156
2157 #[test]
2158 fn trim_trailing_whitespace_default_false() {
2159 let o = Options::default();
2160 assert!(
2161 !o.trim_trailing_whitespace,
2162 "trim_trailing_whitespace must default to false"
2163 );
2164 }
2165
2166 #[test]
2167 fn options_fos_alias_sets_format_on_save() {
2168 let mut o = Options::default();
2169 o.set_by_name("fos", OptionValue::Bool(true)).unwrap();
2170 assert!(o.format_on_save, "fos alias must set format_on_save");
2171 assert_eq!(
2172 o.get_by_name("fos"),
2173 Some(OptionValue::Bool(true)),
2174 "get_by_name(fos) must reflect the new value"
2175 );
2176 assert_eq!(
2177 o.get_by_name("format_on_save"),
2178 Some(OptionValue::Bool(true)),
2179 "get_by_name(format_on_save) must also reflect the new value"
2180 );
2181 }
2182
2183 #[test]
2184 fn options_tts_alias_sets_trim_trailing_whitespace() {
2185 let mut o = Options::default();
2186 o.set_by_name("tts", OptionValue::Bool(true)).unwrap();
2187 assert!(
2188 o.trim_trailing_whitespace,
2189 "tts alias must set trim_trailing_whitespace"
2190 );
2191 assert_eq!(
2192 o.get_by_name("tts"),
2193 Some(OptionValue::Bool(true)),
2194 "get_by_name(tts) must reflect the new value"
2195 );
2196 assert_eq!(
2197 o.get_by_name("trim_trailing_whitespace"),
2198 Some(OptionValue::Bool(true)),
2199 "get_by_name(trim_trailing_whitespace) must also reflect the new value"
2200 );
2201 }
2202
2203 #[test]
2206 fn rainbow_brackets_default_true() {
2207 let o = Options::default();
2208 assert!(o.rainbow_brackets, "rainbow_brackets must default to true");
2209 }
2210
2211 #[test]
2212 fn options_rb_alias_sets_rainbow_brackets() {
2213 let mut o = Options::default();
2214 o.set_by_name("rb", OptionValue::Bool(false)).unwrap();
2215 assert!(
2216 !o.rainbow_brackets,
2217 "rb alias must set rainbow_brackets to false"
2218 );
2219 assert_eq!(
2220 o.get_by_name("rb"),
2221 Some(OptionValue::Bool(false)),
2222 "get_by_name(rb) must reflect the new value"
2223 );
2224 assert_eq!(
2225 o.get_by_name("rainbow_brackets"),
2226 Some(OptionValue::Bool(false)),
2227 "get_by_name(rainbow_brackets) must also reflect the new value"
2228 );
2229 }
2230
2231 #[test]
2232 fn autoreload_default_true() {
2233 assert!(
2234 Options::default().autoreload,
2235 "autoreload must default true"
2236 );
2237 }
2238
2239 #[test]
2240 fn options_ar_alias_sets_autoreload() {
2241 let mut o = Options::default();
2242 o.set_by_name("ar", OptionValue::Bool(false)).unwrap();
2243 assert!(!o.autoreload, "ar alias must set autoreload");
2244 assert_eq!(o.get_by_name("autoreload"), Some(OptionValue::Bool(false)));
2245 }
2246
2247 #[test]
2250 fn updatetime_default_4000() {
2251 let o = Options::default();
2252 assert_eq!(o.updatetime, 4000, "updatetime must default to 4000 ms");
2253 assert_eq!(
2254 o.get_by_name("updatetime"),
2255 Some(OptionValue::Int(4000)),
2256 "get_by_name(updatetime) must return Int(4000)"
2257 );
2258 }
2259
2260 #[test]
2261 fn options_ut_alias_sets_updatetime() {
2262 let mut o = Options::default();
2263 o.set_by_name("ut", OptionValue::Int(1000)).unwrap();
2264 assert_eq!(o.updatetime, 1000, "ut alias must set updatetime");
2265 assert_eq!(
2266 o.get_by_name("ut"),
2267 Some(OptionValue::Int(1000)),
2268 "get_by_name(ut) must reflect the new value"
2269 );
2270 assert_eq!(
2271 o.get_by_name("updatetime"),
2272 Some(OptionValue::Int(1000)),
2273 "get_by_name(updatetime) must also reflect the new value"
2274 );
2275 }
2276
2277 #[test]
2280 fn matchparen_default_true() {
2281 let o = Options::default();
2282 assert!(o.matchparen, "matchparen must default to true");
2283 assert_eq!(
2284 o.get_by_name("matchparen"),
2285 Some(OptionValue::Bool(true)),
2286 "get_by_name(matchparen) must return Bool(true)"
2287 );
2288 }
2289
2290 #[test]
2291 fn options_matchparen_set_and_get() {
2292 let mut o = Options::default();
2293 o.set_by_name("matchparen", OptionValue::Bool(false))
2294 .unwrap();
2295 assert!(!o.matchparen, "matchparen must be false after set");
2296 assert_eq!(
2297 o.get_by_name("matchparen"),
2298 Some(OptionValue::Bool(false)),
2299 "get_by_name(matchparen) must reflect false"
2300 );
2301 o.set_by_name("mps", OptionValue::Bool(true)).unwrap();
2303 assert!(o.matchparen, "mps alias must set matchparen to true");
2304 assert_eq!(
2305 o.get_by_name("mps"),
2306 Some(OptionValue::Bool(true)),
2307 "get_by_name(mps) must reflect true"
2308 );
2309 }
2310
2311 #[test]
2314 fn foldmethod_default_expr() {
2315 let o = Options::default();
2316 assert_eq!(
2317 o.foldmethod,
2318 FoldMethod::Expr,
2319 "foldmethod must default to Expr (tree-sitter)"
2320 );
2321 assert_eq!(
2322 o.get_by_name("foldmethod"),
2323 Some(OptionValue::String("expr".into())),
2324 "get_by_name(foldmethod) must return \"expr\""
2325 );
2326 }
2327
2328 #[test]
2329 fn foldmethod_fdm_alias_roundtrip() {
2330 let mut o = Options::default();
2331 o.set_by_name("fdm", OptionValue::String("manual".into()))
2332 .unwrap();
2333 assert_eq!(o.foldmethod, FoldMethod::Manual);
2334 assert_eq!(
2335 o.get_by_name("fdm"),
2336 Some(OptionValue::String("manual".into()))
2337 );
2338 o.set_by_name("foldmethod", OptionValue::String("expr".into()))
2339 .unwrap();
2340 assert_eq!(o.foldmethod, FoldMethod::Expr);
2341 o.set_by_name("foldmethod", OptionValue::String("marker".into()))
2342 .unwrap();
2343 assert_eq!(o.foldmethod, FoldMethod::Marker);
2344 o.set_by_name("foldmethod", OptionValue::String("syntax".into()))
2346 .unwrap();
2347 assert_eq!(o.foldmethod, FoldMethod::Expr);
2348 }
2349
2350 #[test]
2351 fn foldmethod_rejects_invalid_value() {
2352 let mut o = Options::default();
2353 let err = o
2354 .set_by_name("foldmethod", OptionValue::String("bogus".into()))
2355 .unwrap_err();
2356 assert!(
2357 err.to_string().contains("must be"),
2358 "expected error about valid values, got: {err}"
2359 );
2360 }
2361
2362 #[test]
2363 fn foldenable_default_true() {
2364 let o = Options::default();
2365 assert!(o.foldenable, "foldenable must default to true");
2366 assert_eq!(
2367 o.get_by_name("foldenable"),
2368 Some(OptionValue::Bool(true)),
2369 "get_by_name(foldenable) must return Bool(true)"
2370 );
2371 }
2372
2373 #[test]
2374 fn foldenable_fen_alias_roundtrip() {
2375 let mut o = Options::default();
2376 o.set_by_name("fen", OptionValue::Bool(false)).unwrap();
2377 assert!(!o.foldenable, "fen alias must disable foldenable");
2378 assert_eq!(o.get_by_name("fen"), Some(OptionValue::Bool(false)));
2379 o.set_by_name("foldenable", OptionValue::Bool(true))
2380 .unwrap();
2381 assert!(o.foldenable);
2382 }
2383
2384 #[test]
2385 fn foldlevelstart_default_99() {
2386 let o = Options::default();
2387 assert_eq!(o.foldlevelstart, 99, "foldlevelstart must default to 99");
2388 assert_eq!(
2389 o.get_by_name("foldlevelstart"),
2390 Some(OptionValue::Int(99)),
2391 "get_by_name(foldlevelstart) must return Int(99)"
2392 );
2393 }
2394
2395 #[test]
2396 fn foldlevelstart_fls_alias_roundtrip() {
2397 let mut o = Options::default();
2398 o.set_by_name("fls", OptionValue::Int(0)).unwrap();
2399 assert_eq!(
2400 o.foldlevelstart, 0,
2401 "fls alias must set foldlevelstart to 0"
2402 );
2403 assert_eq!(o.get_by_name("fls"), Some(OptionValue::Int(0)));
2404 o.set_by_name("foldlevelstart", OptionValue::Int(5))
2405 .unwrap();
2406 assert_eq!(o.foldlevelstart, 5);
2407 }
2408}