1use std::ops::Range;
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
17pub struct Pos {
18 pub line: u32,
19 pub col: u32,
20}
21
22impl Pos {
23 pub const ORIGIN: Pos = Pos { line: 0, col: 0 };
24
25 pub const fn new(line: u32, col: u32) -> Self {
26 Pos { line, col }
27 }
28}
29
30#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
41pub enum SelectionKind {
42 #[default]
43 Char,
44 Line,
45 Block,
46}
47
48#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
50pub struct Selection {
51 pub anchor: Pos,
52 pub head: Pos,
53 pub kind: SelectionKind,
54}
55
56impl Selection {
57 pub const fn caret(pos: Pos) -> Self {
59 Selection {
60 anchor: pos,
61 head: pos,
62 kind: SelectionKind::Char,
63 }
64 }
65
66 pub const fn char_range(anchor: Pos, head: Pos) -> Self {
68 Selection {
69 anchor,
70 head,
71 kind: SelectionKind::Char,
72 }
73 }
74
75 pub fn is_empty(&self) -> bool {
77 self.anchor == self.head
78 }
79}
80
81#[derive(Debug, Clone, PartialEq, Eq)]
84pub struct SelectionSet {
85 pub items: Vec<Selection>,
86 pub primary: usize,
87}
88
89impl SelectionSet {
90 pub fn caret(pos: Pos) -> Self {
92 SelectionSet {
93 items: vec![Selection::caret(pos)],
94 primary: 0,
95 }
96 }
97
98 pub fn primary(&self) -> &Selection {
101 self.items
102 .get(self.primary)
103 .or_else(|| self.items.first())
104 .expect("SelectionSet must contain at least one selection")
105 }
106}
107
108impl Default for SelectionSet {
109 fn default() -> Self {
110 SelectionSet::caret(Pos::ORIGIN)
111 }
112}
113
114#[derive(Debug, Clone, PartialEq, Eq)]
118pub struct Edit {
119 pub range: Range<Pos>,
120 pub replacement: String,
121}
122
123#[derive(Debug, Clone, PartialEq, Eq)]
135pub struct ContentEdit {
136 pub start_byte: usize,
137 pub old_end_byte: usize,
138 pub new_end_byte: usize,
139 pub start_position: (u32, u32),
140 pub old_end_position: (u32, u32),
141 pub new_end_position: (u32, u32),
142}
143
144impl Edit {
145 pub fn insert(at: Pos, text: impl Into<String>) -> Self {
146 Edit {
147 range: at..at,
148 replacement: text.into(),
149 }
150 }
151
152 pub fn delete(range: Range<Pos>) -> Self {
153 Edit {
154 range,
155 replacement: String::new(),
156 }
157 }
158
159 pub fn replace(range: Range<Pos>, text: impl Into<String>) -> Self {
160 Edit {
161 range,
162 replacement: text.into(),
163 }
164 }
165}
166
167#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
171pub enum Mode {
172 #[default]
173 Normal,
174 Insert,
175 Visual,
176 Replace,
177 Command,
178 OperatorPending,
179}
180
181#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
184pub enum CursorShape {
185 #[default]
186 Block,
187 Bar,
188 Underline,
189}
190
191#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
194pub struct Style {
195 pub fg: Option<Color>,
196 pub bg: Option<Color>,
197 pub attrs: Attrs,
198}
199
200#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
201pub struct Color(pub u8, pub u8, pub u8);
202
203bitflags::bitflags! {
204 #[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Hash)]
205 pub struct Attrs: u8 {
206 const BOLD = 1 << 0;
207 const ITALIC = 1 << 1;
208 const UNDERLINE = 1 << 2;
209 const REVERSE = 1 << 3;
210 const DIM = 1 << 4;
211 const STRIKE = 1 << 5;
212 }
213}
214
215#[derive(Debug, Clone, Copy, PartialEq, Eq)]
219pub enum HighlightKind {
220 Selection,
221 SearchMatch,
222 IncSearch,
223 MatchParen,
224 Syntax(u32),
225}
226
227#[derive(Debug, Clone, PartialEq, Eq)]
228pub struct Highlight {
229 pub range: Range<Pos>,
230 pub kind: HighlightKind,
231}
232
233#[derive(Debug, Clone, PartialEq, Eq)]
237pub struct Options {
238 pub tabstop: u32,
240 pub shiftwidth: u32,
242 pub expandtab: bool,
244 pub softtabstop: u32,
249 pub iskeyword: String,
253 pub ignorecase: bool,
255 pub smartcase: bool,
258 pub hlsearch: bool,
260 pub incsearch: bool,
262 pub wrapscan: bool,
264 pub autoindent: bool,
266 pub smartindent: bool,
273 pub timeout_len: core::time::Duration,
275 pub undo_levels: u32,
277 pub undo_break_on_motion: bool,
280 pub readonly: bool,
282 pub modifiable: bool,
286 pub wrap: WrapMode,
289 pub textwidth: u32,
291 pub number: bool,
294 pub relativenumber: bool,
297 pub numberwidth: usize,
301 pub cursorline: bool,
305 pub cursorcolumn: bool,
308 pub signcolumn: SignColumnMode,
311 pub foldcolumn: u32,
314 pub foldmethod: FoldMethod,
319 pub foldenable: bool,
323 pub foldlevelstart: u32,
326 pub foldmarker: String,
331 pub colorcolumn: String,
334 pub formatoptions: String,
339 pub filetype: String,
342 pub scrolloff: usize,
347 pub sidescrolloff: usize,
351 pub modeline: bool,
356 pub modelines: u32,
359 pub autoreload: bool,
364 pub motion_sneak: bool,
370 pub list: bool,
373 pub listchars: ListChars,
377 pub indent_guides: bool,
381 pub indent_guide_char: char,
384 pub colorizer: bool,
388 pub colorizer_filetypes: Vec<String>,
392 pub format_on_save: bool,
398 pub trim_trailing_whitespace: bool,
402 pub rainbow_brackets: bool,
405 pub updatetime: u32,
409 pub matchparen: bool,
415}
416
417pub use hjkl_buffer::ListChars;
422
423#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
426#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
427pub enum FoldMethod {
428 Manual,
430 #[default]
435 Expr,
436 Marker,
439}
440
441#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
444#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
445pub enum SignColumnMode {
446 No,
448 Yes,
450 #[default]
452 Auto,
453}
454
455#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
459#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
460pub enum DiagInlineMode {
461 Off,
463 Current,
465 #[default]
467 All,
468}
469
470#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
474#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
475pub enum WrapMode {
476 #[default]
479 None,
480 Char,
483 Word,
487}
488
489#[derive(Debug, Clone, PartialEq, Eq)]
495pub enum OptionValue {
496 Bool(bool),
497 Int(i64),
498 String(String),
499}
500
501impl Default for Options {
502 fn default() -> Self {
503 Options {
504 tabstop: 4,
505 shiftwidth: 4,
506 expandtab: true,
507 softtabstop: 4,
508 iskeyword: "@,48-57,_,192-255".to_string(),
509 ignorecase: true,
510 smartcase: true,
511 hlsearch: true,
512 incsearch: true,
513 wrapscan: true,
514 autoindent: true,
515 smartindent: true,
516 timeout_len: core::time::Duration::from_millis(1000),
517 undo_levels: 1000,
518 undo_break_on_motion: true,
519 readonly: false,
520 modifiable: true,
521 wrap: WrapMode::None,
522 textwidth: 79,
523 number: true,
524 relativenumber: false,
525 numberwidth: 4,
526 cursorline: true,
527 cursorcolumn: false,
528 signcolumn: SignColumnMode::Auto,
529 foldcolumn: 0,
530 foldmethod: FoldMethod::Expr,
531 foldenable: true,
532 foldlevelstart: 99,
533 foldmarker: "{{{,}}}".to_string(),
534 colorcolumn: String::new(),
535 formatoptions: "ro".to_string(),
536 filetype: String::new(),
537 scrolloff: 5,
538 sidescrolloff: 0,
539 modeline: true,
540 modelines: 5,
541 autoreload: true,
542 motion_sneak: true,
543 list: false,
544 listchars: ListChars::default(),
545 indent_guides: true,
546 indent_guide_char: '│',
547 colorizer: true,
548 colorizer_filetypes: vec![
549 "css".to_string(),
550 "scss".to_string(),
551 "sass".to_string(),
552 "less".to_string(),
553 "html".to_string(),
554 "vue".to_string(),
555 "svelte".to_string(),
556 "tailwindcss".to_string(),
557 "toml".to_string(),
558 "lua".to_string(),
559 "vim".to_string(),
560 ],
561 format_on_save: true,
562 trim_trailing_whitespace: false,
563 rainbow_brackets: true,
564 updatetime: 4000,
565 matchparen: true,
566 }
567 }
568}
569
570impl Options {
571 pub fn set_by_name(&mut self, name: &str, val: OptionValue) -> Result<(), EngineError> {
578 macro_rules! set_bool {
579 ($field:ident) => {{
580 self.$field = match val {
581 OptionValue::Bool(b) => b,
582 OptionValue::Int(n) => n != 0,
583 other => {
584 return Err(EngineError::Ex(format!(
585 "option `{name}` expects bool, got {other:?}"
586 )));
587 }
588 };
589 Ok(())
590 }};
591 }
592 macro_rules! set_u32 {
593 ($field:ident) => {{
594 self.$field = match val {
595 OptionValue::Int(n) if n >= 0 && n <= u32::MAX as i64 => n as u32,
596 OptionValue::Int(n) => {
597 return Err(EngineError::Ex(format!(
598 "option `{name}` out of u32 range: {n}"
599 )));
600 }
601 other => {
602 return Err(EngineError::Ex(format!(
603 "option `{name}` expects int, got {other:?}"
604 )));
605 }
606 };
607 Ok(())
608 }};
609 }
610 macro_rules! set_string {
611 ($field:ident) => {{
612 self.$field = match val {
613 OptionValue::String(s) => s,
614 other => {
615 return Err(EngineError::Ex(format!(
616 "option `{name}` expects string, got {other:?}"
617 )));
618 }
619 };
620 Ok(())
621 }};
622 }
623 match name {
624 "tabstop" | "ts" => set_u32!(tabstop),
625 "shiftwidth" | "sw" => set_u32!(shiftwidth),
626 "softtabstop" | "sts" => set_u32!(softtabstop),
627 "textwidth" | "tw" => set_u32!(textwidth),
628 "expandtab" | "et" => set_bool!(expandtab),
629 "iskeyword" | "isk" => set_string!(iskeyword),
630 "ignorecase" | "ic" => set_bool!(ignorecase),
631 "smartcase" | "scs" => set_bool!(smartcase),
632 "hlsearch" | "hls" => set_bool!(hlsearch),
633 "incsearch" | "is" => set_bool!(incsearch),
634 "wrapscan" | "ws" => set_bool!(wrapscan),
635 "autoindent" | "ai" => set_bool!(autoindent),
636 "smartindent" | "si" => set_bool!(smartindent),
637 "timeoutlen" | "tm" => {
638 self.timeout_len = match val {
639 OptionValue::Int(n) if n >= 0 => core::time::Duration::from_millis(n as u64),
640 other => {
641 return Err(EngineError::Ex(format!(
642 "option `{name}` expects non-negative int (millis), got {other:?}"
643 )));
644 }
645 };
646 Ok(())
647 }
648 "undolevels" | "ul" => set_u32!(undo_levels),
649 "undobreak" => set_bool!(undo_break_on_motion),
650 "readonly" | "ro" => set_bool!(readonly),
651 "modifiable" | "ma" => set_bool!(modifiable),
652 "wrap" => {
653 let on = match val {
654 OptionValue::Bool(b) => b,
655 OptionValue::Int(n) => n != 0,
656 other => {
657 return Err(EngineError::Ex(format!(
658 "option `{name}` expects bool, got {other:?}"
659 )));
660 }
661 };
662 self.wrap = match (on, self.wrap) {
663 (false, _) => WrapMode::None,
664 (true, WrapMode::Word) => WrapMode::Word,
665 (true, _) => WrapMode::Char,
666 };
667 Ok(())
668 }
669 "linebreak" | "lbr" => {
670 let on = match val {
671 OptionValue::Bool(b) => b,
672 OptionValue::Int(n) => n != 0,
673 other => {
674 return Err(EngineError::Ex(format!(
675 "option `{name}` expects bool, got {other:?}"
676 )));
677 }
678 };
679 self.wrap = match (on, self.wrap) {
680 (true, _) => WrapMode::Word,
681 (false, WrapMode::Word) => WrapMode::Char,
682 (false, other) => other,
683 };
684 Ok(())
685 }
686 "number" | "nu" => set_bool!(number),
687 "relativenumber" | "rnu" => set_bool!(relativenumber),
688 "numberwidth" | "nuw" => {
689 self.numberwidth = match val {
690 OptionValue::Int(n) if (1..=20).contains(&n) => n as usize,
691 OptionValue::Int(n) => {
692 return Err(EngineError::Ex(format!(
693 "option `{name}` must be in range 1..=20, got {n}"
694 )));
695 }
696 other => {
697 return Err(EngineError::Ex(format!(
698 "option `{name}` expects int, got {other:?}"
699 )));
700 }
701 };
702 Ok(())
703 }
704 "cursorline" | "cul" => set_bool!(cursorline),
705 "cursorcolumn" | "cuc" => set_bool!(cursorcolumn),
706 "signcolumn" | "scl" => {
707 self.signcolumn = match val {
708 OptionValue::String(ref s) => match s.as_str() {
709 "yes" => SignColumnMode::Yes,
710 "no" => SignColumnMode::No,
711 "auto" => SignColumnMode::Auto,
712 other => {
713 return Err(EngineError::Ex(format!(
714 "option `{name}` must be `yes`, `no`, or `auto`, got {other:?}"
715 )));
716 }
717 },
718 other => {
719 return Err(EngineError::Ex(format!(
720 "option `{name}` expects string (yes/no/auto), got {other:?}"
721 )));
722 }
723 };
724 Ok(())
725 }
726 "foldcolumn" | "fdc" => {
727 self.foldcolumn = match val {
728 OptionValue::Int(n) if (0..=12).contains(&n) => n as u32,
729 OptionValue::Int(n) => {
730 return Err(EngineError::Ex(format!(
731 "option `{name}` must be in range 0..=12, got {n}"
732 )));
733 }
734 other => {
735 return Err(EngineError::Ex(format!(
736 "option `{name}` expects int (0-12), got {other:?}"
737 )));
738 }
739 };
740 Ok(())
741 }
742 "foldmethod" | "fdm" => {
743 self.foldmethod = match val {
744 OptionValue::String(ref s) => match s.as_str() {
745 "manual" => FoldMethod::Manual,
746 "expr" | "syntax" => FoldMethod::Expr,
747 "marker" => FoldMethod::Marker,
748 other => {
749 return Err(EngineError::Ex(format!(
750 "option `{name}` must be `manual`, `expr`, `syntax`, or `marker`, got `{other}`"
751 )));
752 }
753 },
754 other => {
755 return Err(EngineError::Ex(format!(
756 "option `{name}` expects string, got {other:?}"
757 )));
758 }
759 };
760 Ok(())
761 }
762 "foldenable" | "fen" => set_bool!(foldenable),
763 "foldlevelstart" | "fls" => set_u32!(foldlevelstart),
764 "colorcolumn" | "cc" => set_string!(colorcolumn),
765 "formatoptions" | "fo" => set_string!(formatoptions),
766 "filetype" | "ft" => set_string!(filetype),
767 "scrolloff" | "so" => {
768 self.scrolloff = match val {
769 OptionValue::Int(n) if n >= 0 => n as usize,
770 OptionValue::Int(n) => {
771 return Err(EngineError::Ex(format!(
772 "option `{name}` must be >= 0, got {n}"
773 )));
774 }
775 other => {
776 return Err(EngineError::Ex(format!(
777 "option `{name}` expects int, got {other:?}"
778 )));
779 }
780 };
781 Ok(())
782 }
783 "sidescrolloff" | "siso" => {
784 self.sidescrolloff = match val {
785 OptionValue::Int(n) if n >= 0 => n as usize,
786 OptionValue::Int(n) => {
787 return Err(EngineError::Ex(format!(
788 "option `{name}` must be >= 0, got {n}"
789 )));
790 }
791 other => {
792 return Err(EngineError::Ex(format!(
793 "option `{name}` expects int, got {other:?}"
794 )));
795 }
796 };
797 Ok(())
798 }
799 "modeline" | "ml" => set_bool!(modeline),
800 "autoreload" | "ar" => set_bool!(autoreload),
801 "modelines" | "mls" => set_u32!(modelines),
802 "motion_sneak" | "snk" => set_bool!(motion_sneak),
803 "list" => set_bool!(list),
804 "listchars" | "lcs" => {
805 let s = match val {
806 OptionValue::String(s) => s,
807 other => {
808 return Err(EngineError::Ex(format!(
809 "option `{name}` expects string, got {other:?}"
810 )));
811 }
812 };
813 self.listchars = ListChars::parse(&s).map_err(EngineError::Ex)?;
814 Ok(())
815 }
816 "indent_guides" | "ig" => set_bool!(indent_guides),
817 "colorizer" | "clz" => set_bool!(colorizer),
818 "colorizer_filetypes" | "clzft" => {
819 let s = match val {
820 OptionValue::String(s) => s,
821 other => {
822 return Err(EngineError::Ex(format!(
823 "option `{name}` expects string, got {other:?}"
824 )));
825 }
826 };
827 self.colorizer_filetypes = s
828 .split(',')
829 .map(|p| p.trim().to_string())
830 .filter(|p| !p.is_empty())
831 .collect();
832 Ok(())
833 }
834 "indent_guide_char" | "igc" => {
835 let s = match val {
836 OptionValue::String(s) => s,
837 other => {
838 return Err(EngineError::Ex(format!(
839 "option `{name}` expects a single-char string, got {other:?}"
840 )));
841 }
842 };
843 let mut chars = s.chars();
844 let ch = match (chars.next(), chars.next()) {
845 (Some(c), None) => c,
846 _ => {
847 return Err(EngineError::Ex(format!(
848 "option `{name}` expects exactly one character, got {s:?}"
849 )));
850 }
851 };
852 self.indent_guide_char = ch;
853 Ok(())
854 }
855 "format_on_save" | "fos" => set_bool!(format_on_save),
856 "trim_trailing_whitespace" | "tts" => set_bool!(trim_trailing_whitespace),
857 "rainbow_brackets" | "rb" => set_bool!(rainbow_brackets),
858 "updatetime" | "ut" => set_u32!(updatetime),
859 "matchparen" | "mps" => set_bool!(matchparen),
860 other => Err(EngineError::Ex(format!("unknown option `{other}`"))),
861 }
862 }
863
864 pub fn get_by_name(&self, name: &str) -> Option<OptionValue> {
866 Some(match name {
867 "tabstop" | "ts" => OptionValue::Int(self.tabstop as i64),
868 "shiftwidth" | "sw" => OptionValue::Int(self.shiftwidth as i64),
869 "softtabstop" | "sts" => OptionValue::Int(self.softtabstop as i64),
870 "textwidth" | "tw" => OptionValue::Int(self.textwidth as i64),
871 "expandtab" | "et" => OptionValue::Bool(self.expandtab),
872 "iskeyword" | "isk" => OptionValue::String(self.iskeyword.clone()),
873 "ignorecase" | "ic" => OptionValue::Bool(self.ignorecase),
874 "smartcase" | "scs" => OptionValue::Bool(self.smartcase),
875 "hlsearch" | "hls" => OptionValue::Bool(self.hlsearch),
876 "incsearch" | "is" => OptionValue::Bool(self.incsearch),
877 "wrapscan" | "ws" => OptionValue::Bool(self.wrapscan),
878 "autoindent" | "ai" => OptionValue::Bool(self.autoindent),
879 "smartindent" | "si" => OptionValue::Bool(self.smartindent),
880 "timeoutlen" | "tm" => OptionValue::Int(self.timeout_len.as_millis() as i64),
881 "undolevels" | "ul" => OptionValue::Int(self.undo_levels as i64),
882 "undobreak" => OptionValue::Bool(self.undo_break_on_motion),
883 "readonly" | "ro" => OptionValue::Bool(self.readonly),
884 "modifiable" | "ma" => OptionValue::Bool(self.modifiable),
885 "wrap" => OptionValue::Bool(!matches!(self.wrap, WrapMode::None)),
886 "linebreak" | "lbr" => OptionValue::Bool(matches!(self.wrap, WrapMode::Word)),
887 "number" | "nu" => OptionValue::Bool(self.number),
888 "relativenumber" | "rnu" => OptionValue::Bool(self.relativenumber),
889 "numberwidth" | "nuw" => OptionValue::Int(self.numberwidth as i64),
890 "cursorline" | "cul" => OptionValue::Bool(self.cursorline),
891 "cursorcolumn" | "cuc" => OptionValue::Bool(self.cursorcolumn),
892 "signcolumn" | "scl" => OptionValue::String(
893 match self.signcolumn {
894 SignColumnMode::Yes => "yes",
895 SignColumnMode::No => "no",
896 SignColumnMode::Auto => "auto",
897 }
898 .to_string(),
899 ),
900 "foldcolumn" | "fdc" => OptionValue::Int(self.foldcolumn as i64),
901 "foldmethod" | "fdm" => OptionValue::String(
902 match self.foldmethod {
903 FoldMethod::Manual => "manual",
904 FoldMethod::Expr => "expr",
905 FoldMethod::Marker => "marker",
906 }
907 .to_string(),
908 ),
909 "foldenable" | "fen" => OptionValue::Bool(self.foldenable),
910 "foldlevelstart" | "fls" => OptionValue::Int(self.foldlevelstart as i64),
911 "colorcolumn" | "cc" => OptionValue::String(self.colorcolumn.clone()),
912 "formatoptions" | "fo" => OptionValue::String(self.formatoptions.clone()),
913 "filetype" | "ft" => OptionValue::String(self.filetype.clone()),
914 "scrolloff" | "so" => OptionValue::Int(self.scrolloff as i64),
915 "sidescrolloff" | "siso" => OptionValue::Int(self.sidescrolloff as i64),
916 "modeline" | "ml" => OptionValue::Bool(self.modeline),
917 "autoreload" | "ar" => OptionValue::Bool(self.autoreload),
918 "modelines" | "mls" => OptionValue::Int(self.modelines as i64),
919 "motion_sneak" | "snk" => OptionValue::Bool(self.motion_sneak),
920 "list" => OptionValue::Bool(self.list),
921 "listchars" | "lcs" => OptionValue::String(self.listchars.to_canonical_string()),
922 "indent_guides" | "ig" => OptionValue::Bool(self.indent_guides),
923 "indent_guide_char" | "igc" => OptionValue::String(self.indent_guide_char.to_string()),
924 "colorizer" | "clz" => OptionValue::Bool(self.colorizer),
925 "colorizer_filetypes" | "clzft" => {
926 OptionValue::String(self.colorizer_filetypes.join(","))
927 }
928 "format_on_save" | "fos" => OptionValue::Bool(self.format_on_save),
929 "trim_trailing_whitespace" | "tts" => OptionValue::Bool(self.trim_trailing_whitespace),
930 "rainbow_brackets" | "rb" => OptionValue::Bool(self.rainbow_brackets),
931 "updatetime" | "ut" => OptionValue::Int(self.updatetime as i64),
932 "matchparen" | "mps" => OptionValue::Bool(self.matchparen),
933 _ => return None,
934 })
935 }
936}
937
938pub use hjkl_buffer::Viewport;
960
961#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
965pub struct BufferId(pub u64);
966
967#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
969pub struct Modifiers {
970 pub ctrl: bool,
971 pub shift: bool,
972 pub alt: bool,
973 pub super_: bool,
974}
975
976#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
978#[non_exhaustive]
979pub enum SpecialKey {
980 Esc,
981 Enter,
982 Backspace,
983 Tab,
984 BackTab,
985 Up,
986 Down,
987 Left,
988 Right,
989 Home,
990 End,
991 PageUp,
992 PageDown,
993 Insert,
994 Delete,
995 F(u8),
996}
997
998#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
999pub enum MouseKind {
1000 Press,
1001 Release,
1002 Drag,
1003 ScrollUp,
1004 ScrollDown,
1005}
1006
1007#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
1008pub struct MouseEvent {
1009 pub kind: MouseKind,
1010 pub pos: Pos,
1011 pub mods: Modifiers,
1012}
1013
1014#[derive(Debug, Clone, PartialEq, Eq)]
1019#[non_exhaustive]
1020pub enum Input {
1021 Char(char, Modifiers),
1022 Key(SpecialKey, Modifiers),
1023 Mouse(MouseEvent),
1024 Paste(String),
1025 FocusGained,
1026 FocusLost,
1027 Resize(u16, u16),
1028}
1029
1030pub trait Host: Send {
1039 type Intent;
1043
1044 fn write_clipboard(&mut self, text: String);
1050
1051 fn read_clipboard(&mut self) -> Option<String>;
1054
1055 fn now(&self) -> core::time::Duration;
1061
1062 fn should_cancel(&self) -> bool {
1065 false
1066 }
1067
1068 fn prompt_search(&mut self) -> Option<String>;
1073
1074 fn display_line_for(&self, pos: Pos) -> u32 {
1079 pos.line
1080 }
1081
1082 fn pos_for_display(&self, line: u32, col: u32) -> Pos {
1084 Pos { line, col }
1085 }
1086
1087 fn syntax_highlights(&self, range: Range<Pos>) -> Vec<Highlight> {
1092 let _ = range;
1093 Vec::new()
1094 }
1095
1096 fn emit_cursor_shape(&mut self, shape: CursorShape);
1101
1102 fn viewport(&self) -> &Viewport;
1109
1110 fn viewport_mut(&mut self) -> &mut Viewport;
1113
1114 fn emit_intent(&mut self, intent: Self::Intent);
1119}
1120
1121#[derive(Debug)]
1135pub struct DefaultHost {
1136 clipboard: Option<String>,
1137 last_cursor_shape: CursorShape,
1138 started: std::time::Instant,
1139 viewport: Viewport,
1140}
1141
1142impl Default for DefaultHost {
1143 fn default() -> Self {
1144 Self::new()
1145 }
1146}
1147
1148impl DefaultHost {
1149 pub const DEFAULT_VIEWPORT: Viewport = Viewport {
1152 top_row: 0,
1153 top_col: 0,
1154 width: 80,
1155 height: 24,
1156 wrap: hjkl_buffer::Wrap::None,
1157 text_width: 80,
1158 tab_width: 0,
1159 };
1160
1161 pub fn new() -> Self {
1162 Self {
1163 clipboard: None,
1164 last_cursor_shape: CursorShape::Block,
1165 started: std::time::Instant::now(),
1166 viewport: Self::DEFAULT_VIEWPORT,
1167 }
1168 }
1169
1170 pub fn with_viewport(viewport: Viewport) -> Self {
1174 Self {
1175 clipboard: None,
1176 last_cursor_shape: CursorShape::Block,
1177 started: std::time::Instant::now(),
1178 viewport,
1179 }
1180 }
1181
1182 pub fn last_cursor_shape(&self) -> CursorShape {
1184 self.last_cursor_shape
1185 }
1186}
1187
1188impl Host for DefaultHost {
1189 type Intent = ();
1190
1191 fn write_clipboard(&mut self, text: String) {
1192 self.clipboard = Some(text);
1193 }
1194
1195 fn read_clipboard(&mut self) -> Option<String> {
1196 self.clipboard.clone()
1197 }
1198
1199 fn now(&self) -> core::time::Duration {
1200 self.started.elapsed()
1201 }
1202
1203 fn prompt_search(&mut self) -> Option<String> {
1204 None
1205 }
1206
1207 fn emit_cursor_shape(&mut self, shape: CursorShape) {
1208 self.last_cursor_shape = shape;
1209 }
1210
1211 fn viewport(&self) -> &Viewport {
1212 &self.viewport
1213 }
1214
1215 fn viewport_mut(&mut self) -> &mut Viewport {
1216 &mut self.viewport
1217 }
1218
1219 fn emit_intent(&mut self, _intent: Self::Intent) {}
1220}
1221
1222#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
1234pub struct RenderFrame {
1235 pub mode: SnapshotMode,
1236 pub cursor_row: u32,
1237 pub cursor_col: u32,
1238 pub cursor_shape: CursorShape,
1239 pub viewport_top: u32,
1240 pub line_count: u32,
1241}
1242
1243#[derive(Debug, Clone)]
1270#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1271pub struct EditorSnapshot {
1272 pub version: u32,
1275 pub mode: SnapshotMode,
1277 pub cursor: (u32, u32),
1279 pub lines: Vec<String>,
1281 pub viewport_top: u32,
1283 pub registers: crate::Registers,
1287 pub marks: std::collections::BTreeMap<char, (u32, u32)>,
1294 pub global_marks: std::collections::BTreeMap<char, (u64, u32, u32)>,
1298}
1299
1300#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
1304#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1305pub enum SnapshotMode {
1306 #[default]
1307 Normal,
1308 Insert,
1309 Visual,
1310 VisualLine,
1311 VisualBlock,
1312}
1313
1314impl EditorSnapshot {
1315 pub const VERSION: u32 = 5;
1336}
1337
1338#[derive(Debug, thiserror::Error)]
1342pub enum EngineError {
1343 #[error("regex compile error: {0}")]
1346 Regex(#[from] regex::Error),
1347
1348 #[error("invalid range: {0}")]
1350 InvalidRange(String),
1351
1352 #[error("ex parse: {0}")]
1354 Ex(String),
1355
1356 #[error("buffer is read-only")]
1358 ReadOnly,
1359
1360 #[error("position out of bounds: {0:?}")]
1362 OutOfBounds(Pos),
1363
1364 #[error("snapshot version mismatch: file={0}, expected={1}")]
1367 SnapshotVersion(u32, u32),
1368}
1369
1370pub(crate) mod sealed {
1371 pub trait Sealed {}
1381}
1382
1383pub trait Cursor: Send {
1389 fn cursor(&self) -> Pos;
1391 fn set_cursor(&mut self, pos: Pos);
1393 fn byte_offset(&self, pos: Pos) -> usize;
1395 fn pos_at_byte(&self, byte: usize) -> Pos;
1397}
1398
1399pub trait Query: Send {
1401 fn line_count(&self) -> u32;
1403 fn line(&self, idx: u32) -> String;
1406 fn len_bytes(&self) -> usize;
1408 fn slice(&self, range: core::ops::Range<Pos>) -> std::borrow::Cow<'_, str>;
1413 fn dirty_gen(&self) -> u64 {
1427 0
1428 }
1429
1430 fn byte_of_row(&self, row: usize) -> usize {
1441 let n = self.line_count() as usize;
1442 let row = row.min(n);
1443 let mut acc = 0usize;
1444 for r in 0..row {
1445 acc += self.line(r as u32).len();
1446 if r + 1 < n {
1451 acc += 1;
1452 }
1453 }
1454 acc
1455 }
1456
1457 fn content_joined(&self) -> std::sync::Arc<String> {
1466 let n = self.line_count() as usize;
1467 let mut acc = String::with_capacity(self.len_bytes());
1468 for r in 0..n {
1469 if r > 0 {
1470 acc.push('\n');
1471 }
1472 acc.push_str(&self.line(r as u32));
1473 }
1474 std::sync::Arc::new(acc)
1475 }
1476
1477 fn line_bytes(&self, row: usize) -> usize {
1485 let n = self.line_count() as usize;
1486 if row >= n {
1487 return 0;
1488 }
1489 self.line(row as u32).len()
1490 }
1491
1492 fn rope(&self) -> ropey::Rope {
1501 ropey::Rope::from_str(&self.content_joined())
1502 }
1503}
1504
1505pub trait BufferEdit: Send {
1509 fn insert_at(&mut self, pos: Pos, text: &str);
1512 fn delete_range(&mut self, range: core::ops::Range<Pos>);
1514 fn replace_range(&mut self, range: core::ops::Range<Pos>, replacement: &str);
1516 fn replace_all(&mut self, text: &str) {
1524 self.replace_range(
1525 Pos::ORIGIN..Pos {
1526 line: u32::MAX,
1527 col: u32::MAX,
1528 },
1529 text,
1530 );
1531 }
1532}
1533
1534pub trait Search: Send {
1537 fn find_next(&self, from: Pos, pat: ®ex::Regex) -> Option<core::ops::Range<Pos>>;
1539 fn find_prev(&self, from: Pos, pat: ®ex::Regex) -> Option<core::ops::Range<Pos>>;
1541}
1542
1543pub trait Buffer: Cursor + Query + BufferEdit + Search + sealed::Sealed + Send {}
1551
1552#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
1570#[non_exhaustive]
1571pub enum FoldOp {
1572 Add {
1576 start_row: usize,
1577 end_row: usize,
1578 closed: bool,
1579 },
1580 RemoveAt(usize),
1582 OpenAt(usize),
1584 CloseAt(usize),
1586 ToggleAt(usize),
1588 OpenAll,
1590 CloseAll,
1592 ClearAll,
1594 Invalidate { start_row: usize, end_row: usize },
1599}
1600
1601pub trait FoldProvider: Send {
1622 fn next_visible_row(&self, row: usize, row_count: usize) -> Option<usize>;
1625 fn prev_visible_row(&self, row: usize) -> Option<usize>;
1627 fn is_row_hidden(&self, row: usize) -> bool;
1629 fn fold_at_row(&self, row: usize) -> Option<(usize, usize, bool)>;
1633
1634 fn apply(&mut self, op: FoldOp) {
1644 let _ = op;
1645 }
1646
1647 fn invalidate_range(&mut self, start_row: usize, end_row: usize) {
1652 self.apply(FoldOp::Invalidate { start_row, end_row });
1653 }
1654}
1655
1656#[derive(Debug, Default, Clone, Copy)]
1659pub struct NoopFoldProvider;
1660
1661impl FoldProvider for NoopFoldProvider {
1662 fn next_visible_row(&self, row: usize, row_count: usize) -> Option<usize> {
1663 let last = row_count.saturating_sub(1);
1664 if last == 0 && row == 0 {
1665 return None;
1666 }
1667 let r = row.checked_add(1)?;
1668 (r <= last).then_some(r)
1669 }
1670
1671 fn prev_visible_row(&self, row: usize) -> Option<usize> {
1672 row.checked_sub(1)
1673 }
1674
1675 fn is_row_hidden(&self, _row: usize) -> bool {
1676 false
1677 }
1678
1679 fn fold_at_row(&self, _row: usize) -> Option<(usize, usize, bool)> {
1680 None
1681 }
1682}
1683
1684#[cfg(test)]
1685mod tests {
1686 use super::*;
1687
1688 #[test]
1689 fn caret_is_empty() {
1690 let sel = Selection::caret(Pos::new(2, 4));
1691 assert!(sel.is_empty());
1692 assert_eq!(sel.anchor, sel.head);
1693 }
1694
1695 #[test]
1696 fn selection_set_default_has_one_caret() {
1697 let set = SelectionSet::default();
1698 assert_eq!(set.items.len(), 1);
1699 assert_eq!(set.primary, 0);
1700 assert_eq!(set.primary().anchor, Pos::ORIGIN);
1701 }
1702
1703 #[test]
1704 fn edit_constructors() {
1705 let p = Pos::new(0, 5);
1706 assert_eq!(Edit::insert(p, "x").range, p..p);
1707 assert!(Edit::insert(p, "x").replacement == "x");
1708 assert!(Edit::delete(p..p).replacement.is_empty());
1709 }
1710
1711 #[test]
1712 fn attrs_flags() {
1713 let a = Attrs::BOLD | Attrs::UNDERLINE;
1714 assert!(a.contains(Attrs::BOLD));
1715 assert!(!a.contains(Attrs::ITALIC));
1716 }
1717
1718 #[test]
1719 fn options_set_get_roundtrip() {
1720 let mut o = Options::default();
1721 o.set_by_name("tabstop", OptionValue::Int(4)).unwrap();
1722 assert!(matches!(o.get_by_name("ts"), Some(OptionValue::Int(4))));
1723 o.set_by_name("expandtab", OptionValue::Bool(true)).unwrap();
1724 assert!(matches!(o.get_by_name("et"), Some(OptionValue::Bool(true))));
1725 o.set_by_name("iskeyword", OptionValue::String("a-z".into()))
1726 .unwrap();
1727 match o.get_by_name("iskeyword") {
1728 Some(OptionValue::String(s)) => assert_eq!(s, "a-z"),
1729 other => panic!("expected String, got {other:?}"),
1730 }
1731 }
1732
1733 #[test]
1734 fn options_unknown_name_errors_on_set() {
1735 let mut o = Options::default();
1736 assert!(matches!(
1737 o.set_by_name("frobnicate", OptionValue::Int(1)),
1738 Err(EngineError::Ex(_))
1739 ));
1740 assert!(o.get_by_name("frobnicate").is_none());
1741 }
1742
1743 #[test]
1744 fn options_type_mismatch_errors() {
1745 let mut o = Options::default();
1746 assert!(matches!(
1747 o.set_by_name("tabstop", OptionValue::String("nope".into())),
1748 Err(EngineError::Ex(_))
1749 ));
1750 assert!(matches!(
1751 o.set_by_name("iskeyword", OptionValue::Int(7)),
1752 Err(EngineError::Ex(_))
1753 ));
1754 }
1755
1756 #[test]
1759 fn default_options_ignorecase_and_smartcase_are_true() {
1760 let o = Options::default();
1761 assert!(o.ignorecase, "ignorecase must default to true");
1762 assert!(o.smartcase, "smartcase must default to true");
1763 }
1764
1765 #[test]
1766 fn options_int_to_bool_coercion() {
1767 let mut o = Options::default();
1770 o.set_by_name("ignorecase", OptionValue::Int(1)).unwrap();
1771 assert!(matches!(o.get_by_name("ic"), Some(OptionValue::Bool(true))));
1772 o.set_by_name("ignorecase", OptionValue::Int(0)).unwrap();
1773 assert!(matches!(
1774 o.get_by_name("ic"),
1775 Some(OptionValue::Bool(false))
1776 ));
1777 }
1778
1779 #[test]
1780 fn options_wrap_linebreak_roundtrip() {
1781 let mut o = Options::default();
1782 assert_eq!(o.wrap, WrapMode::None);
1783 o.set_by_name("wrap", OptionValue::Bool(true)).unwrap();
1784 assert_eq!(o.wrap, WrapMode::Char);
1785 o.set_by_name("linebreak", OptionValue::Bool(true)).unwrap();
1786 assert_eq!(o.wrap, WrapMode::Word);
1787 assert!(matches!(
1788 o.get_by_name("wrap"),
1789 Some(OptionValue::Bool(true))
1790 ));
1791 assert!(matches!(
1792 o.get_by_name("lbr"),
1793 Some(OptionValue::Bool(true))
1794 ));
1795 o.set_by_name("linebreak", OptionValue::Bool(false))
1796 .unwrap();
1797 assert_eq!(o.wrap, WrapMode::Char);
1798 o.set_by_name("wrap", OptionValue::Bool(false)).unwrap();
1799 assert_eq!(o.wrap, WrapMode::None);
1800 }
1801
1802 #[test]
1803 fn options_default_modern() {
1804 let o = Options::default();
1807 assert_eq!(o.tabstop, 4);
1808 assert_eq!(o.shiftwidth, 4);
1809 assert_eq!(o.softtabstop, 4);
1810 assert!(o.expandtab);
1811 assert!(o.hlsearch);
1812 assert!(o.wrapscan);
1813 assert!(o.smartindent);
1814 assert_eq!(o.timeout_len, core::time::Duration::from_millis(1000));
1815 }
1816
1817 #[test]
1818 fn editor_snapshot_version_const() {
1819 assert_eq!(EditorSnapshot::VERSION, 5);
1820 }
1821
1822 #[test]
1823 fn editor_snapshot_default_shape() {
1824 let s = EditorSnapshot {
1825 version: EditorSnapshot::VERSION,
1826 mode: SnapshotMode::Normal,
1827 cursor: (0, 0),
1828 lines: vec!["hello".to_string()],
1829 viewport_top: 0,
1830 registers: crate::Registers::default(),
1831 marks: Default::default(),
1832 global_marks: Default::default(),
1833 };
1834 assert_eq!(s.cursor, (0, 0));
1835 assert_eq!(s.lines.len(), 1);
1836 }
1837
1838 #[cfg(feature = "serde")]
1839 #[test]
1840 fn editor_snapshot_roundtrip() {
1841 let mut marks = std::collections::BTreeMap::new();
1842 marks.insert('a', (1u32, 0u32));
1843 let mut global_marks = std::collections::BTreeMap::new();
1844 global_marks.insert('A', (42u64, 5u32, 2u32));
1845 let s = EditorSnapshot {
1846 version: EditorSnapshot::VERSION,
1847 mode: SnapshotMode::Insert,
1848 cursor: (3, 7),
1849 lines: vec!["alpha".into(), "beta".into()],
1850 viewport_top: 2,
1851 registers: crate::Registers::default(),
1852 marks,
1853 global_marks,
1854 };
1855 let json = serde_json::to_string(&s).unwrap();
1856 let back: EditorSnapshot = serde_json::from_str(&json).unwrap();
1857 assert_eq!(s.cursor, back.cursor);
1858 assert_eq!(s.lines, back.lines);
1859 assert_eq!(s.viewport_top, back.viewport_top);
1860 assert_eq!(s.global_marks, back.global_marks);
1861 }
1862
1863 #[test]
1864 fn engine_error_display() {
1865 let e = EngineError::ReadOnly;
1866 assert_eq!(e.to_string(), "buffer is read-only");
1867 let e = EngineError::OutOfBounds(Pos::new(3, 7));
1868 assert!(e.to_string().contains("out of bounds"));
1869 }
1870
1871 #[test]
1874 fn options_cursorline_roundtrip() {
1875 let mut o = Options::default();
1876 assert!(o.cursorline, "cursorline defaults to true");
1877 o.set_by_name("cursorline", OptionValue::Bool(false))
1878 .unwrap();
1879 assert!(matches!(
1880 o.get_by_name("cul"),
1881 Some(OptionValue::Bool(false))
1882 ));
1883 o.set_by_name("cul", OptionValue::Bool(true)).unwrap();
1884 assert!(matches!(
1885 o.get_by_name("cursorline"),
1886 Some(OptionValue::Bool(true))
1887 ));
1888 }
1889
1890 #[test]
1891 fn options_cursorcolumn_roundtrip() {
1892 let mut o = Options::default();
1893 assert!(!o.cursorcolumn, "cursorcolumn defaults to false");
1894 o.set_by_name("cuc", OptionValue::Bool(true)).unwrap();
1895 assert!(matches!(
1896 o.get_by_name("cursorcolumn"),
1897 Some(OptionValue::Bool(true))
1898 ));
1899 }
1900
1901 #[test]
1902 fn options_signcolumn_roundtrip() {
1903 let mut o = Options::default();
1904 assert_eq!(
1905 o.signcolumn,
1906 SignColumnMode::Auto,
1907 "signcolumn defaults to auto"
1908 );
1909 o.set_by_name("signcolumn", OptionValue::String("yes".into()))
1910 .unwrap();
1911 assert_eq!(o.signcolumn, SignColumnMode::Yes);
1912 assert_eq!(
1913 o.get_by_name("scl"),
1914 Some(OptionValue::String("yes".into()))
1915 );
1916 o.set_by_name("scl", OptionValue::String("no".into()))
1917 .unwrap();
1918 assert_eq!(o.signcolumn, SignColumnMode::No);
1919 o.set_by_name("scl", OptionValue::String("auto".into()))
1920 .unwrap();
1921 assert_eq!(o.signcolumn, SignColumnMode::Auto);
1922 }
1923
1924 #[test]
1925 fn options_signcolumn_rejects_invalid() {
1926 let mut o = Options::default();
1927 assert!(matches!(
1928 o.set_by_name("signcolumn", OptionValue::String("maybe".into())),
1929 Err(EngineError::Ex(_))
1930 ));
1931 assert!(matches!(
1933 o.set_by_name("signcolumn", OptionValue::Bool(true)),
1934 Err(EngineError::Ex(_))
1935 ));
1936 }
1937
1938 #[test]
1939 fn options_foldcolumn_roundtrip() {
1940 let mut o = Options::default();
1941 assert_eq!(o.foldcolumn, 0, "foldcolumn defaults to 0");
1942 o.set_by_name("fdc", OptionValue::Int(3)).unwrap();
1943 assert_eq!(o.foldcolumn, 3);
1944 assert_eq!(o.get_by_name("foldcolumn"), Some(OptionValue::Int(3)));
1945 }
1946
1947 #[test]
1948 fn options_foldcolumn_rejects_out_of_range() {
1949 let mut o = Options::default();
1950 assert!(matches!(
1951 o.set_by_name("foldcolumn", OptionValue::Int(13)),
1952 Err(EngineError::Ex(_))
1953 ));
1954 assert!(matches!(
1955 o.set_by_name("foldcolumn", OptionValue::Int(-1)),
1956 Err(EngineError::Ex(_))
1957 ));
1958 }
1959
1960 #[test]
1961 fn options_colorcolumn_roundtrip() {
1962 let mut o = Options::default();
1963 assert_eq!(o.colorcolumn, "", "colorcolumn defaults to empty string");
1964 o.set_by_name("cc", OptionValue::String("80,120".into()))
1965 .unwrap();
1966 assert_eq!(
1967 o.get_by_name("colorcolumn"),
1968 Some(OptionValue::String("80,120".into()))
1969 );
1970 o.set_by_name("colorcolumn", OptionValue::String(String::new()))
1971 .unwrap();
1972 assert_eq!(
1973 o.get_by_name("cc"),
1974 Some(OptionValue::String(String::new()))
1975 );
1976 }
1977
1978 #[test]
1979 fn options_cursorline_alias_cul() {
1980 let mut o = Options::default();
1981 o.set_by_name("cul", OptionValue::Bool(true)).unwrap();
1983 assert!(o.cursorline);
1984 o.set_by_name("cul", OptionValue::Bool(false)).unwrap();
1986 assert!(!o.cursorline);
1987 }
1988
1989 #[test]
1990 fn sign_column_mode_default_is_auto() {
1991 assert_eq!(SignColumnMode::default(), SignColumnMode::Auto);
1992 }
1993
1994 #[test]
1995 fn options_scrolloff_default_and_set() {
1996 let mut o = Options::default();
1997 assert_eq!(o.scrolloff, 5, "scrolloff defaults to 5");
1998 o.set_by_name("scrolloff", OptionValue::Int(0)).unwrap();
1999 assert_eq!(o.scrolloff, 0);
2000 o.set_by_name("scrolloff", OptionValue::Int(999)).unwrap();
2001 assert_eq!(o.scrolloff, 999);
2002 assert_eq!(o.get_by_name("scrolloff"), Some(OptionValue::Int(999)));
2003 }
2004
2005 #[test]
2006 fn options_sidescrolloff_default_and_set() {
2007 let mut o = Options::default();
2008 assert_eq!(o.sidescrolloff, 0, "sidescrolloff defaults to 0");
2009 o.set_by_name("sidescrolloff", OptionValue::Int(5)).unwrap();
2010 assert_eq!(o.sidescrolloff, 5);
2011 assert_eq!(o.get_by_name("sidescrolloff"), Some(OptionValue::Int(5)));
2012 }
2013
2014 #[test]
2015 fn options_alias_so_siso() {
2016 let mut o = Options::default();
2017 o.set_by_name("so", OptionValue::Int(3)).unwrap();
2019 assert_eq!(o.scrolloff, 3);
2020 assert_eq!(o.get_by_name("so"), Some(OptionValue::Int(3)));
2021 o.set_by_name("siso", OptionValue::Int(2)).unwrap();
2023 assert_eq!(o.sidescrolloff, 2);
2024 assert_eq!(o.get_by_name("siso"), Some(OptionValue::Int(2)));
2025 }
2026
2027 #[test]
2030 fn options_list_default_false_and_set() {
2031 let mut o = Options::default();
2032 assert!(!o.list, "list default is false");
2033 o.set_by_name("list", OptionValue::Bool(true)).unwrap();
2034 assert!(o.list);
2035 assert_eq!(o.get_by_name("list"), Some(OptionValue::Bool(true)));
2036 o.set_by_name("list", OptionValue::Bool(false)).unwrap();
2037 assert!(!o.list);
2038 }
2039
2040 #[test]
2041 fn options_listchars_default_matches_vim() {
2042 let o = Options::default();
2043 let lc = &o.listchars;
2044 assert_eq!(lc.tab_lead, '^');
2045 assert_eq!(lc.tab_fill, Some('I'));
2046 assert_eq!(lc.eol, Some('$'));
2047 assert_eq!(lc.space, None);
2048 assert_eq!(lc.trail, None);
2049 assert_eq!(lc.nbsp, None);
2050 }
2051
2052 #[test]
2053 fn options_listchars_set_and_get() {
2054 let mut o = Options::default();
2055 o.set_by_name("listchars", OptionValue::String("tab:>-,eol:$".to_string()))
2056 .unwrap();
2057 assert_eq!(o.listchars.tab_lead, '>');
2058 assert_eq!(o.listchars.tab_fill, Some('-'));
2059 assert_eq!(o.listchars.eol, Some('$'));
2060 }
2061
2062 #[test]
2063 fn options_lcs_alias_sets_listchars() {
2064 let mut o = Options::default();
2065 o.set_by_name("lcs", OptionValue::String("tab:>-,trail:~".to_string()))
2066 .unwrap();
2067 assert_eq!(o.listchars.tab_lead, '>');
2068 assert_eq!(o.listchars.trail, Some('~'));
2069 }
2070
2071 #[test]
2072 fn options_listchars_get_by_name_returns_string() {
2073 let o = Options::default();
2074 match o.get_by_name("listchars") {
2075 Some(OptionValue::String(s)) => {
2076 assert!(s.contains("tab:"), "canonical string should contain tab:");
2077 }
2078 other => panic!("expected String, got {other:?}"),
2079 }
2080 }
2081
2082 #[test]
2083 fn options_listchars_invalid_value_returns_err() {
2084 let mut o = Options::default();
2085 assert!(
2086 o.set_by_name("listchars", OptionValue::String("bogus:x".to_string()))
2087 .is_err()
2088 );
2089 }
2090
2091 #[test]
2094 fn indent_guides_default_true() {
2095 assert!(
2096 Options::default().indent_guides,
2097 "indent_guides must default to true"
2098 );
2099 }
2100
2101 #[test]
2102 fn options_indent_guides_set_and_get() {
2103 let mut opts = Options::default();
2104 opts.set_by_name("indent_guides", OptionValue::Bool(false))
2106 .unwrap();
2107 assert!(!opts.indent_guides);
2108 opts.set_by_name("ig", OptionValue::Bool(true)).unwrap();
2110 assert!(opts.indent_guides);
2111 assert_eq!(opts.get_by_name("ig"), Some(OptionValue::Bool(true)));
2113 assert_eq!(
2114 opts.get_by_name("indent_guides"),
2115 Some(OptionValue::Bool(true))
2116 );
2117 }
2118
2119 #[test]
2120 fn options_indent_guide_char_set_and_get() {
2121 let mut opts = Options::default();
2122 opts.set_by_name("indent_guide_char", OptionValue::String(":".to_string()))
2123 .unwrap();
2124 assert_eq!(opts.indent_guide_char, ':');
2125 opts.set_by_name("igc", OptionValue::String("┊".to_string()))
2127 .unwrap();
2128 assert_eq!(opts.indent_guide_char, '┊');
2129 assert_eq!(
2131 opts.get_by_name("igc"),
2132 Some(OptionValue::String("┊".to_string()))
2133 );
2134 assert_eq!(
2135 opts.get_by_name("indent_guide_char"),
2136 Some(OptionValue::String("┊".to_string()))
2137 );
2138 }
2139
2140 #[test]
2141 fn options_indent_guide_char_rejects_multi_char() {
2142 let mut opts = Options::default();
2143 assert!(
2144 opts.set_by_name("indent_guide_char", OptionValue::String("ab".to_string()))
2145 .is_err(),
2146 "multi-char value must be rejected"
2147 );
2148 }
2149
2150 #[test]
2151 fn options_indent_guide_char_rejects_empty() {
2152 let mut opts = Options::default();
2153 assert!(
2154 opts.set_by_name("indent_guide_char", OptionValue::String(String::new()))
2155 .is_err(),
2156 "empty string must be rejected"
2157 );
2158 }
2159
2160 #[test]
2163 fn colorizer_default_true() {
2164 assert!(
2165 Options::default().colorizer,
2166 "colorizer must default to true"
2167 );
2168 }
2169
2170 #[test]
2171 fn colorizer_filetypes_includes_css() {
2172 let o = Options::default();
2173 assert!(
2174 o.colorizer_filetypes.iter().any(|f| f == "css"),
2175 "default colorizer_filetypes must include 'css'"
2176 );
2177 }
2178
2179 #[test]
2180 fn options_colorizer_set_and_get() {
2181 let mut o = Options::default();
2182 o.set_by_name("colorizer", OptionValue::Bool(false))
2183 .unwrap();
2184 assert_eq!(o.get_by_name("colorizer"), Some(OptionValue::Bool(false)));
2185 o.set_by_name("clz", OptionValue::Bool(true)).unwrap();
2186 assert_eq!(o.get_by_name("clz"), Some(OptionValue::Bool(true)));
2187 }
2188
2189 #[test]
2190 fn options_colorizer_filetypes_set_and_get() {
2191 let mut o = Options::default();
2192 o.set_by_name(
2193 "colorizer_filetypes",
2194 OptionValue::String("css,scss,toml".into()),
2195 )
2196 .unwrap();
2197 assert_eq!(o.colorizer_filetypes, vec!["css", "scss", "toml"]);
2198 assert_eq!(
2199 o.get_by_name("clzft"),
2200 Some(OptionValue::String("css,scss,toml".into()))
2201 );
2202 }
2203
2204 #[test]
2207 fn format_on_save_default_true() {
2208 let o = Options::default();
2209 assert!(o.format_on_save, "format_on_save must default to true");
2210 }
2211
2212 #[test]
2213 fn trim_trailing_whitespace_default_false() {
2214 let o = Options::default();
2215 assert!(
2216 !o.trim_trailing_whitespace,
2217 "trim_trailing_whitespace must default to false"
2218 );
2219 }
2220
2221 #[test]
2222 fn options_fos_alias_sets_format_on_save() {
2223 let mut o = Options::default();
2224 o.set_by_name("fos", OptionValue::Bool(true)).unwrap();
2225 assert!(o.format_on_save, "fos alias must set format_on_save");
2226 assert_eq!(
2227 o.get_by_name("fos"),
2228 Some(OptionValue::Bool(true)),
2229 "get_by_name(fos) must reflect the new value"
2230 );
2231 assert_eq!(
2232 o.get_by_name("format_on_save"),
2233 Some(OptionValue::Bool(true)),
2234 "get_by_name(format_on_save) must also reflect the new value"
2235 );
2236 }
2237
2238 #[test]
2239 fn options_tts_alias_sets_trim_trailing_whitespace() {
2240 let mut o = Options::default();
2241 o.set_by_name("tts", OptionValue::Bool(true)).unwrap();
2242 assert!(
2243 o.trim_trailing_whitespace,
2244 "tts alias must set trim_trailing_whitespace"
2245 );
2246 assert_eq!(
2247 o.get_by_name("tts"),
2248 Some(OptionValue::Bool(true)),
2249 "get_by_name(tts) must reflect the new value"
2250 );
2251 assert_eq!(
2252 o.get_by_name("trim_trailing_whitespace"),
2253 Some(OptionValue::Bool(true)),
2254 "get_by_name(trim_trailing_whitespace) must also reflect the new value"
2255 );
2256 }
2257
2258 #[test]
2261 fn rainbow_brackets_default_true() {
2262 let o = Options::default();
2263 assert!(o.rainbow_brackets, "rainbow_brackets must default to true");
2264 }
2265
2266 #[test]
2267 fn options_rb_alias_sets_rainbow_brackets() {
2268 let mut o = Options::default();
2269 o.set_by_name("rb", OptionValue::Bool(false)).unwrap();
2270 assert!(
2271 !o.rainbow_brackets,
2272 "rb alias must set rainbow_brackets to false"
2273 );
2274 assert_eq!(
2275 o.get_by_name("rb"),
2276 Some(OptionValue::Bool(false)),
2277 "get_by_name(rb) must reflect the new value"
2278 );
2279 assert_eq!(
2280 o.get_by_name("rainbow_brackets"),
2281 Some(OptionValue::Bool(false)),
2282 "get_by_name(rainbow_brackets) must also reflect the new value"
2283 );
2284 }
2285
2286 #[test]
2287 fn autoreload_default_true() {
2288 assert!(
2289 Options::default().autoreload,
2290 "autoreload must default true"
2291 );
2292 }
2293
2294 #[test]
2295 fn options_ar_alias_sets_autoreload() {
2296 let mut o = Options::default();
2297 o.set_by_name("ar", OptionValue::Bool(false)).unwrap();
2298 assert!(!o.autoreload, "ar alias must set autoreload");
2299 assert_eq!(o.get_by_name("autoreload"), Some(OptionValue::Bool(false)));
2300 }
2301
2302 #[test]
2305 fn updatetime_default_4000() {
2306 let o = Options::default();
2307 assert_eq!(o.updatetime, 4000, "updatetime must default to 4000 ms");
2308 assert_eq!(
2309 o.get_by_name("updatetime"),
2310 Some(OptionValue::Int(4000)),
2311 "get_by_name(updatetime) must return Int(4000)"
2312 );
2313 }
2314
2315 #[test]
2316 fn options_ut_alias_sets_updatetime() {
2317 let mut o = Options::default();
2318 o.set_by_name("ut", OptionValue::Int(1000)).unwrap();
2319 assert_eq!(o.updatetime, 1000, "ut alias must set updatetime");
2320 assert_eq!(
2321 o.get_by_name("ut"),
2322 Some(OptionValue::Int(1000)),
2323 "get_by_name(ut) must reflect the new value"
2324 );
2325 assert_eq!(
2326 o.get_by_name("updatetime"),
2327 Some(OptionValue::Int(1000)),
2328 "get_by_name(updatetime) must also reflect the new value"
2329 );
2330 }
2331
2332 #[test]
2335 fn matchparen_default_true() {
2336 let o = Options::default();
2337 assert!(o.matchparen, "matchparen must default to true");
2338 assert_eq!(
2339 o.get_by_name("matchparen"),
2340 Some(OptionValue::Bool(true)),
2341 "get_by_name(matchparen) must return Bool(true)"
2342 );
2343 }
2344
2345 #[test]
2346 fn options_matchparen_set_and_get() {
2347 let mut o = Options::default();
2348 o.set_by_name("matchparen", OptionValue::Bool(false))
2349 .unwrap();
2350 assert!(!o.matchparen, "matchparen must be false after set");
2351 assert_eq!(
2352 o.get_by_name("matchparen"),
2353 Some(OptionValue::Bool(false)),
2354 "get_by_name(matchparen) must reflect false"
2355 );
2356 o.set_by_name("mps", OptionValue::Bool(true)).unwrap();
2358 assert!(o.matchparen, "mps alias must set matchparen to true");
2359 assert_eq!(
2360 o.get_by_name("mps"),
2361 Some(OptionValue::Bool(true)),
2362 "get_by_name(mps) must reflect true"
2363 );
2364 }
2365
2366 #[test]
2369 fn foldmethod_default_expr() {
2370 let o = Options::default();
2371 assert_eq!(
2372 o.foldmethod,
2373 FoldMethod::Expr,
2374 "foldmethod must default to Expr (tree-sitter)"
2375 );
2376 assert_eq!(
2377 o.get_by_name("foldmethod"),
2378 Some(OptionValue::String("expr".into())),
2379 "get_by_name(foldmethod) must return \"expr\""
2380 );
2381 }
2382
2383 #[test]
2384 fn foldmethod_fdm_alias_roundtrip() {
2385 let mut o = Options::default();
2386 o.set_by_name("fdm", OptionValue::String("manual".into()))
2387 .unwrap();
2388 assert_eq!(o.foldmethod, FoldMethod::Manual);
2389 assert_eq!(
2390 o.get_by_name("fdm"),
2391 Some(OptionValue::String("manual".into()))
2392 );
2393 o.set_by_name("foldmethod", OptionValue::String("expr".into()))
2394 .unwrap();
2395 assert_eq!(o.foldmethod, FoldMethod::Expr);
2396 o.set_by_name("foldmethod", OptionValue::String("marker".into()))
2397 .unwrap();
2398 assert_eq!(o.foldmethod, FoldMethod::Marker);
2399 o.set_by_name("foldmethod", OptionValue::String("syntax".into()))
2401 .unwrap();
2402 assert_eq!(o.foldmethod, FoldMethod::Expr);
2403 }
2404
2405 #[test]
2406 fn foldmethod_rejects_invalid_value() {
2407 let mut o = Options::default();
2408 let err = o
2409 .set_by_name("foldmethod", OptionValue::String("bogus".into()))
2410 .unwrap_err();
2411 assert!(
2412 err.to_string().contains("must be"),
2413 "expected error about valid values, got: {err}"
2414 );
2415 }
2416
2417 #[test]
2418 fn foldenable_default_true() {
2419 let o = Options::default();
2420 assert!(o.foldenable, "foldenable must default to true");
2421 assert_eq!(
2422 o.get_by_name("foldenable"),
2423 Some(OptionValue::Bool(true)),
2424 "get_by_name(foldenable) must return Bool(true)"
2425 );
2426 }
2427
2428 #[test]
2429 fn foldenable_fen_alias_roundtrip() {
2430 let mut o = Options::default();
2431 o.set_by_name("fen", OptionValue::Bool(false)).unwrap();
2432 assert!(!o.foldenable, "fen alias must disable foldenable");
2433 assert_eq!(o.get_by_name("fen"), Some(OptionValue::Bool(false)));
2434 o.set_by_name("foldenable", OptionValue::Bool(true))
2435 .unwrap();
2436 assert!(o.foldenable);
2437 }
2438
2439 #[test]
2440 fn foldlevelstart_default_99() {
2441 let o = Options::default();
2442 assert_eq!(o.foldlevelstart, 99, "foldlevelstart must default to 99");
2443 assert_eq!(
2444 o.get_by_name("foldlevelstart"),
2445 Some(OptionValue::Int(99)),
2446 "get_by_name(foldlevelstart) must return Int(99)"
2447 );
2448 }
2449
2450 #[test]
2451 fn foldlevelstart_fls_alias_roundtrip() {
2452 let mut o = Options::default();
2453 o.set_by_name("fls", OptionValue::Int(0)).unwrap();
2454 assert_eq!(
2455 o.foldlevelstart, 0,
2456 "fls alias must set foldlevelstart to 0"
2457 );
2458 assert_eq!(o.get_by_name("fls"), Some(OptionValue::Int(0)));
2459 o.set_by_name("foldlevelstart", OptionValue::Int(5))
2460 .unwrap();
2461 assert_eq!(o.foldlevelstart, 5);
2462 }
2463}