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 wrap: WrapMode,
285 pub textwidth: u32,
287 pub number: bool,
290 pub relativenumber: bool,
293 pub numberwidth: usize,
297 pub cursorline: bool,
301 pub cursorcolumn: bool,
304 pub signcolumn: SignColumnMode,
307 pub foldcolumn: u32,
310 pub foldmethod: FoldMethod,
315 pub foldenable: bool,
319 pub foldlevelstart: u32,
322 pub foldmarker: String,
327 pub colorcolumn: String,
330 pub formatoptions: String,
335 pub filetype: String,
338 pub scrolloff: usize,
343 pub sidescrolloff: usize,
347 pub modeline: bool,
352 pub modelines: u32,
355 pub autoreload: bool,
360 pub motion_sneak: bool,
366 pub list: bool,
369 pub listchars: ListChars,
373 pub indent_guides: bool,
377 pub indent_guide_char: char,
380 pub colorizer: bool,
384 pub colorizer_filetypes: Vec<String>,
388 pub format_on_save: bool,
394 pub trim_trailing_whitespace: bool,
398 pub rainbow_brackets: bool,
401 pub updatetime: u32,
405 pub matchparen: bool,
411}
412
413pub use hjkl_buffer::ListChars;
418
419#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
422#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
423pub enum FoldMethod {
424 Manual,
426 #[default]
431 Expr,
432 Marker,
435}
436
437#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
440#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
441pub enum SignColumnMode {
442 No,
444 Yes,
446 #[default]
448 Auto,
449}
450
451#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
455#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
456pub enum DiagInlineMode {
457 Off,
459 Current,
461 #[default]
463 All,
464}
465
466#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
470#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
471pub enum WrapMode {
472 #[default]
475 None,
476 Char,
479 Word,
483}
484
485#[derive(Debug, Clone, PartialEq, Eq)]
491pub enum OptionValue {
492 Bool(bool),
493 Int(i64),
494 String(String),
495}
496
497impl Default for Options {
498 fn default() -> Self {
499 Options {
500 tabstop: 4,
501 shiftwidth: 4,
502 expandtab: true,
503 softtabstop: 4,
504 iskeyword: "@,48-57,_,192-255".to_string(),
505 ignorecase: true,
506 smartcase: true,
507 hlsearch: true,
508 incsearch: true,
509 wrapscan: true,
510 autoindent: true,
511 smartindent: true,
512 timeout_len: core::time::Duration::from_millis(1000),
513 undo_levels: 1000,
514 undo_break_on_motion: true,
515 readonly: false,
516 wrap: WrapMode::None,
517 textwidth: 79,
518 number: true,
519 relativenumber: false,
520 numberwidth: 4,
521 cursorline: true,
522 cursorcolumn: false,
523 signcolumn: SignColumnMode::Auto,
524 foldcolumn: 0,
525 foldmethod: FoldMethod::Expr,
526 foldenable: true,
527 foldlevelstart: 99,
528 foldmarker: "{{{,}}}".to_string(),
529 colorcolumn: String::new(),
530 formatoptions: "ro".to_string(),
531 filetype: String::new(),
532 scrolloff: 5,
533 sidescrolloff: 0,
534 modeline: true,
535 modelines: 5,
536 autoreload: true,
537 motion_sneak: true,
538 list: false,
539 listchars: ListChars::default(),
540 indent_guides: true,
541 indent_guide_char: '│',
542 colorizer: true,
543 colorizer_filetypes: vec![
544 "css".to_string(),
545 "scss".to_string(),
546 "sass".to_string(),
547 "less".to_string(),
548 "html".to_string(),
549 "vue".to_string(),
550 "svelte".to_string(),
551 "tailwindcss".to_string(),
552 "toml".to_string(),
553 "lua".to_string(),
554 "vim".to_string(),
555 ],
556 format_on_save: true,
557 trim_trailing_whitespace: false,
558 rainbow_brackets: true,
559 updatetime: 4000,
560 matchparen: true,
561 }
562 }
563}
564
565impl Options {
566 pub fn set_by_name(&mut self, name: &str, val: OptionValue) -> Result<(), EngineError> {
573 macro_rules! set_bool {
574 ($field:ident) => {{
575 self.$field = match val {
576 OptionValue::Bool(b) => b,
577 OptionValue::Int(n) => n != 0,
578 other => {
579 return Err(EngineError::Ex(format!(
580 "option `{name}` expects bool, got {other:?}"
581 )));
582 }
583 };
584 Ok(())
585 }};
586 }
587 macro_rules! set_u32 {
588 ($field:ident) => {{
589 self.$field = match val {
590 OptionValue::Int(n) if n >= 0 && n <= u32::MAX as i64 => n as u32,
591 OptionValue::Int(n) => {
592 return Err(EngineError::Ex(format!(
593 "option `{name}` out of u32 range: {n}"
594 )));
595 }
596 other => {
597 return Err(EngineError::Ex(format!(
598 "option `{name}` expects int, got {other:?}"
599 )));
600 }
601 };
602 Ok(())
603 }};
604 }
605 macro_rules! set_string {
606 ($field:ident) => {{
607 self.$field = match val {
608 OptionValue::String(s) => s,
609 other => {
610 return Err(EngineError::Ex(format!(
611 "option `{name}` expects string, got {other:?}"
612 )));
613 }
614 };
615 Ok(())
616 }};
617 }
618 match name {
619 "tabstop" | "ts" => set_u32!(tabstop),
620 "shiftwidth" | "sw" => set_u32!(shiftwidth),
621 "softtabstop" | "sts" => set_u32!(softtabstop),
622 "textwidth" | "tw" => set_u32!(textwidth),
623 "expandtab" | "et" => set_bool!(expandtab),
624 "iskeyword" | "isk" => set_string!(iskeyword),
625 "ignorecase" | "ic" => set_bool!(ignorecase),
626 "smartcase" | "scs" => set_bool!(smartcase),
627 "hlsearch" | "hls" => set_bool!(hlsearch),
628 "incsearch" | "is" => set_bool!(incsearch),
629 "wrapscan" | "ws" => set_bool!(wrapscan),
630 "autoindent" | "ai" => set_bool!(autoindent),
631 "smartindent" | "si" => set_bool!(smartindent),
632 "timeoutlen" | "tm" => {
633 self.timeout_len = match val {
634 OptionValue::Int(n) if n >= 0 => core::time::Duration::from_millis(n as u64),
635 other => {
636 return Err(EngineError::Ex(format!(
637 "option `{name}` expects non-negative int (millis), got {other:?}"
638 )));
639 }
640 };
641 Ok(())
642 }
643 "undolevels" | "ul" => set_u32!(undo_levels),
644 "undobreak" => set_bool!(undo_break_on_motion),
645 "readonly" | "ro" => set_bool!(readonly),
646 "wrap" => {
647 let on = match val {
648 OptionValue::Bool(b) => b,
649 OptionValue::Int(n) => n != 0,
650 other => {
651 return Err(EngineError::Ex(format!(
652 "option `{name}` expects bool, got {other:?}"
653 )));
654 }
655 };
656 self.wrap = match (on, self.wrap) {
657 (false, _) => WrapMode::None,
658 (true, WrapMode::Word) => WrapMode::Word,
659 (true, _) => WrapMode::Char,
660 };
661 Ok(())
662 }
663 "linebreak" | "lbr" => {
664 let on = match val {
665 OptionValue::Bool(b) => b,
666 OptionValue::Int(n) => n != 0,
667 other => {
668 return Err(EngineError::Ex(format!(
669 "option `{name}` expects bool, got {other:?}"
670 )));
671 }
672 };
673 self.wrap = match (on, self.wrap) {
674 (true, _) => WrapMode::Word,
675 (false, WrapMode::Word) => WrapMode::Char,
676 (false, other) => other,
677 };
678 Ok(())
679 }
680 "number" | "nu" => set_bool!(number),
681 "relativenumber" | "rnu" => set_bool!(relativenumber),
682 "numberwidth" | "nuw" => {
683 self.numberwidth = match val {
684 OptionValue::Int(n) if (1..=20).contains(&n) => n as usize,
685 OptionValue::Int(n) => {
686 return Err(EngineError::Ex(format!(
687 "option `{name}` must be in range 1..=20, got {n}"
688 )));
689 }
690 other => {
691 return Err(EngineError::Ex(format!(
692 "option `{name}` expects int, got {other:?}"
693 )));
694 }
695 };
696 Ok(())
697 }
698 "cursorline" | "cul" => set_bool!(cursorline),
699 "cursorcolumn" | "cuc" => set_bool!(cursorcolumn),
700 "signcolumn" | "scl" => {
701 self.signcolumn = match val {
702 OptionValue::String(ref s) => match s.as_str() {
703 "yes" => SignColumnMode::Yes,
704 "no" => SignColumnMode::No,
705 "auto" => SignColumnMode::Auto,
706 other => {
707 return Err(EngineError::Ex(format!(
708 "option `{name}` must be `yes`, `no`, or `auto`, got {other:?}"
709 )));
710 }
711 },
712 other => {
713 return Err(EngineError::Ex(format!(
714 "option `{name}` expects string (yes/no/auto), got {other:?}"
715 )));
716 }
717 };
718 Ok(())
719 }
720 "foldcolumn" | "fdc" => {
721 self.foldcolumn = match val {
722 OptionValue::Int(n) if (0..=12).contains(&n) => n as u32,
723 OptionValue::Int(n) => {
724 return Err(EngineError::Ex(format!(
725 "option `{name}` must be in range 0..=12, got {n}"
726 )));
727 }
728 other => {
729 return Err(EngineError::Ex(format!(
730 "option `{name}` expects int (0-12), got {other:?}"
731 )));
732 }
733 };
734 Ok(())
735 }
736 "foldmethod" | "fdm" => {
737 self.foldmethod = match val {
738 OptionValue::String(ref s) => match s.as_str() {
739 "manual" => FoldMethod::Manual,
740 "expr" | "syntax" => FoldMethod::Expr,
741 "marker" => FoldMethod::Marker,
742 other => {
743 return Err(EngineError::Ex(format!(
744 "option `{name}` must be `manual`, `expr`, `syntax`, or `marker`, got `{other}`"
745 )));
746 }
747 },
748 other => {
749 return Err(EngineError::Ex(format!(
750 "option `{name}` expects string, got {other:?}"
751 )));
752 }
753 };
754 Ok(())
755 }
756 "foldenable" | "fen" => set_bool!(foldenable),
757 "foldlevelstart" | "fls" => set_u32!(foldlevelstart),
758 "colorcolumn" | "cc" => set_string!(colorcolumn),
759 "formatoptions" | "fo" => set_string!(formatoptions),
760 "filetype" | "ft" => set_string!(filetype),
761 "scrolloff" | "so" => {
762 self.scrolloff = match val {
763 OptionValue::Int(n) if n >= 0 => n as usize,
764 OptionValue::Int(n) => {
765 return Err(EngineError::Ex(format!(
766 "option `{name}` must be >= 0, got {n}"
767 )));
768 }
769 other => {
770 return Err(EngineError::Ex(format!(
771 "option `{name}` expects int, got {other:?}"
772 )));
773 }
774 };
775 Ok(())
776 }
777 "sidescrolloff" | "siso" => {
778 self.sidescrolloff = match val {
779 OptionValue::Int(n) if n >= 0 => n as usize,
780 OptionValue::Int(n) => {
781 return Err(EngineError::Ex(format!(
782 "option `{name}` must be >= 0, got {n}"
783 )));
784 }
785 other => {
786 return Err(EngineError::Ex(format!(
787 "option `{name}` expects int, got {other:?}"
788 )));
789 }
790 };
791 Ok(())
792 }
793 "modeline" | "ml" => set_bool!(modeline),
794 "autoreload" | "ar" => set_bool!(autoreload),
795 "modelines" | "mls" => set_u32!(modelines),
796 "motion_sneak" | "snk" => set_bool!(motion_sneak),
797 "list" => set_bool!(list),
798 "listchars" | "lcs" => {
799 let s = match val {
800 OptionValue::String(s) => s,
801 other => {
802 return Err(EngineError::Ex(format!(
803 "option `{name}` expects string, got {other:?}"
804 )));
805 }
806 };
807 self.listchars = ListChars::parse(&s).map_err(EngineError::Ex)?;
808 Ok(())
809 }
810 "indent_guides" | "ig" => set_bool!(indent_guides),
811 "colorizer" | "clz" => set_bool!(colorizer),
812 "colorizer_filetypes" | "clzft" => {
813 let s = match val {
814 OptionValue::String(s) => s,
815 other => {
816 return Err(EngineError::Ex(format!(
817 "option `{name}` expects string, got {other:?}"
818 )));
819 }
820 };
821 self.colorizer_filetypes = s
822 .split(',')
823 .map(|p| p.trim().to_string())
824 .filter(|p| !p.is_empty())
825 .collect();
826 Ok(())
827 }
828 "indent_guide_char" | "igc" => {
829 let s = match val {
830 OptionValue::String(s) => s,
831 other => {
832 return Err(EngineError::Ex(format!(
833 "option `{name}` expects a single-char string, got {other:?}"
834 )));
835 }
836 };
837 let mut chars = s.chars();
838 let ch = match (chars.next(), chars.next()) {
839 (Some(c), None) => c,
840 _ => {
841 return Err(EngineError::Ex(format!(
842 "option `{name}` expects exactly one character, got {s:?}"
843 )));
844 }
845 };
846 self.indent_guide_char = ch;
847 Ok(())
848 }
849 "format_on_save" | "fos" => set_bool!(format_on_save),
850 "trim_trailing_whitespace" | "tts" => set_bool!(trim_trailing_whitespace),
851 "rainbow_brackets" | "rb" => set_bool!(rainbow_brackets),
852 "updatetime" | "ut" => set_u32!(updatetime),
853 "matchparen" | "mps" => set_bool!(matchparen),
854 other => Err(EngineError::Ex(format!("unknown option `{other}`"))),
855 }
856 }
857
858 pub fn get_by_name(&self, name: &str) -> Option<OptionValue> {
860 Some(match name {
861 "tabstop" | "ts" => OptionValue::Int(self.tabstop as i64),
862 "shiftwidth" | "sw" => OptionValue::Int(self.shiftwidth as i64),
863 "softtabstop" | "sts" => OptionValue::Int(self.softtabstop as i64),
864 "textwidth" | "tw" => OptionValue::Int(self.textwidth as i64),
865 "expandtab" | "et" => OptionValue::Bool(self.expandtab),
866 "iskeyword" | "isk" => OptionValue::String(self.iskeyword.clone()),
867 "ignorecase" | "ic" => OptionValue::Bool(self.ignorecase),
868 "smartcase" | "scs" => OptionValue::Bool(self.smartcase),
869 "hlsearch" | "hls" => OptionValue::Bool(self.hlsearch),
870 "incsearch" | "is" => OptionValue::Bool(self.incsearch),
871 "wrapscan" | "ws" => OptionValue::Bool(self.wrapscan),
872 "autoindent" | "ai" => OptionValue::Bool(self.autoindent),
873 "smartindent" | "si" => OptionValue::Bool(self.smartindent),
874 "timeoutlen" | "tm" => OptionValue::Int(self.timeout_len.as_millis() as i64),
875 "undolevels" | "ul" => OptionValue::Int(self.undo_levels as i64),
876 "undobreak" => OptionValue::Bool(self.undo_break_on_motion),
877 "readonly" | "ro" => OptionValue::Bool(self.readonly),
878 "wrap" => OptionValue::Bool(!matches!(self.wrap, WrapMode::None)),
879 "linebreak" | "lbr" => OptionValue::Bool(matches!(self.wrap, WrapMode::Word)),
880 "number" | "nu" => OptionValue::Bool(self.number),
881 "relativenumber" | "rnu" => OptionValue::Bool(self.relativenumber),
882 "numberwidth" | "nuw" => OptionValue::Int(self.numberwidth as i64),
883 "cursorline" | "cul" => OptionValue::Bool(self.cursorline),
884 "cursorcolumn" | "cuc" => OptionValue::Bool(self.cursorcolumn),
885 "signcolumn" | "scl" => OptionValue::String(
886 match self.signcolumn {
887 SignColumnMode::Yes => "yes",
888 SignColumnMode::No => "no",
889 SignColumnMode::Auto => "auto",
890 }
891 .to_string(),
892 ),
893 "foldcolumn" | "fdc" => OptionValue::Int(self.foldcolumn as i64),
894 "foldmethod" | "fdm" => OptionValue::String(
895 match self.foldmethod {
896 FoldMethod::Manual => "manual",
897 FoldMethod::Expr => "expr",
898 FoldMethod::Marker => "marker",
899 }
900 .to_string(),
901 ),
902 "foldenable" | "fen" => OptionValue::Bool(self.foldenable),
903 "foldlevelstart" | "fls" => OptionValue::Int(self.foldlevelstart as i64),
904 "colorcolumn" | "cc" => OptionValue::String(self.colorcolumn.clone()),
905 "formatoptions" | "fo" => OptionValue::String(self.formatoptions.clone()),
906 "filetype" | "ft" => OptionValue::String(self.filetype.clone()),
907 "scrolloff" | "so" => OptionValue::Int(self.scrolloff as i64),
908 "sidescrolloff" | "siso" => OptionValue::Int(self.sidescrolloff as i64),
909 "modeline" | "ml" => OptionValue::Bool(self.modeline),
910 "autoreload" | "ar" => OptionValue::Bool(self.autoreload),
911 "modelines" | "mls" => OptionValue::Int(self.modelines as i64),
912 "motion_sneak" | "snk" => OptionValue::Bool(self.motion_sneak),
913 "list" => OptionValue::Bool(self.list),
914 "listchars" | "lcs" => OptionValue::String(self.listchars.to_canonical_string()),
915 "indent_guides" | "ig" => OptionValue::Bool(self.indent_guides),
916 "indent_guide_char" | "igc" => OptionValue::String(self.indent_guide_char.to_string()),
917 "colorizer" | "clz" => OptionValue::Bool(self.colorizer),
918 "colorizer_filetypes" | "clzft" => {
919 OptionValue::String(self.colorizer_filetypes.join(","))
920 }
921 "format_on_save" | "fos" => OptionValue::Bool(self.format_on_save),
922 "trim_trailing_whitespace" | "tts" => OptionValue::Bool(self.trim_trailing_whitespace),
923 "rainbow_brackets" | "rb" => OptionValue::Bool(self.rainbow_brackets),
924 "updatetime" | "ut" => OptionValue::Int(self.updatetime as i64),
925 "matchparen" | "mps" => OptionValue::Bool(self.matchparen),
926 _ => return None,
927 })
928 }
929}
930
931pub use hjkl_buffer::Viewport;
953
954#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
958pub struct BufferId(pub u64);
959
960#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
962pub struct Modifiers {
963 pub ctrl: bool,
964 pub shift: bool,
965 pub alt: bool,
966 pub super_: bool,
967}
968
969#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
971#[non_exhaustive]
972pub enum SpecialKey {
973 Esc,
974 Enter,
975 Backspace,
976 Tab,
977 BackTab,
978 Up,
979 Down,
980 Left,
981 Right,
982 Home,
983 End,
984 PageUp,
985 PageDown,
986 Insert,
987 Delete,
988 F(u8),
989}
990
991#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
992pub enum MouseKind {
993 Press,
994 Release,
995 Drag,
996 ScrollUp,
997 ScrollDown,
998}
999
1000#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
1001pub struct MouseEvent {
1002 pub kind: MouseKind,
1003 pub pos: Pos,
1004 pub mods: Modifiers,
1005}
1006
1007#[derive(Debug, Clone, PartialEq, Eq)]
1012#[non_exhaustive]
1013pub enum Input {
1014 Char(char, Modifiers),
1015 Key(SpecialKey, Modifiers),
1016 Mouse(MouseEvent),
1017 Paste(String),
1018 FocusGained,
1019 FocusLost,
1020 Resize(u16, u16),
1021}
1022
1023pub trait Host: Send {
1032 type Intent;
1036
1037 fn write_clipboard(&mut self, text: String);
1043
1044 fn read_clipboard(&mut self) -> Option<String>;
1047
1048 fn now(&self) -> core::time::Duration;
1054
1055 fn should_cancel(&self) -> bool {
1058 false
1059 }
1060
1061 fn prompt_search(&mut self) -> Option<String>;
1066
1067 fn display_line_for(&self, pos: Pos) -> u32 {
1072 pos.line
1073 }
1074
1075 fn pos_for_display(&self, line: u32, col: u32) -> Pos {
1077 Pos { line, col }
1078 }
1079
1080 fn syntax_highlights(&self, range: Range<Pos>) -> Vec<Highlight> {
1085 let _ = range;
1086 Vec::new()
1087 }
1088
1089 fn emit_cursor_shape(&mut self, shape: CursorShape);
1094
1095 fn viewport(&self) -> &Viewport;
1102
1103 fn viewport_mut(&mut self) -> &mut Viewport;
1106
1107 fn emit_intent(&mut self, intent: Self::Intent);
1112}
1113
1114#[derive(Debug)]
1128pub struct DefaultHost {
1129 clipboard: Option<String>,
1130 last_cursor_shape: CursorShape,
1131 started: std::time::Instant,
1132 viewport: Viewport,
1133}
1134
1135impl Default for DefaultHost {
1136 fn default() -> Self {
1137 Self::new()
1138 }
1139}
1140
1141impl DefaultHost {
1142 pub const DEFAULT_VIEWPORT: Viewport = Viewport {
1145 top_row: 0,
1146 top_col: 0,
1147 width: 80,
1148 height: 24,
1149 wrap: hjkl_buffer::Wrap::None,
1150 text_width: 80,
1151 tab_width: 0,
1152 };
1153
1154 pub fn new() -> Self {
1155 Self {
1156 clipboard: None,
1157 last_cursor_shape: CursorShape::Block,
1158 started: std::time::Instant::now(),
1159 viewport: Self::DEFAULT_VIEWPORT,
1160 }
1161 }
1162
1163 pub fn with_viewport(viewport: Viewport) -> Self {
1167 Self {
1168 clipboard: None,
1169 last_cursor_shape: CursorShape::Block,
1170 started: std::time::Instant::now(),
1171 viewport,
1172 }
1173 }
1174
1175 pub fn last_cursor_shape(&self) -> CursorShape {
1177 self.last_cursor_shape
1178 }
1179}
1180
1181impl Host for DefaultHost {
1182 type Intent = ();
1183
1184 fn write_clipboard(&mut self, text: String) {
1185 self.clipboard = Some(text);
1186 }
1187
1188 fn read_clipboard(&mut self) -> Option<String> {
1189 self.clipboard.clone()
1190 }
1191
1192 fn now(&self) -> core::time::Duration {
1193 self.started.elapsed()
1194 }
1195
1196 fn prompt_search(&mut self) -> Option<String> {
1197 None
1198 }
1199
1200 fn emit_cursor_shape(&mut self, shape: CursorShape) {
1201 self.last_cursor_shape = shape;
1202 }
1203
1204 fn viewport(&self) -> &Viewport {
1205 &self.viewport
1206 }
1207
1208 fn viewport_mut(&mut self) -> &mut Viewport {
1209 &mut self.viewport
1210 }
1211
1212 fn emit_intent(&mut self, _intent: Self::Intent) {}
1213}
1214
1215#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
1227pub struct RenderFrame {
1228 pub mode: SnapshotMode,
1229 pub cursor_row: u32,
1230 pub cursor_col: u32,
1231 pub cursor_shape: CursorShape,
1232 pub viewport_top: u32,
1233 pub line_count: u32,
1234}
1235
1236#[derive(Debug, Clone)]
1263#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1264pub struct EditorSnapshot {
1265 pub version: u32,
1268 pub mode: SnapshotMode,
1270 pub cursor: (u32, u32),
1272 pub lines: Vec<String>,
1274 pub viewport_top: u32,
1276 pub registers: crate::Registers,
1280 pub marks: std::collections::BTreeMap<char, (u32, u32)>,
1287 pub global_marks: std::collections::BTreeMap<char, (u64, u32, u32)>,
1291}
1292
1293#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
1297#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1298pub enum SnapshotMode {
1299 #[default]
1300 Normal,
1301 Insert,
1302 Visual,
1303 VisualLine,
1304 VisualBlock,
1305}
1306
1307impl EditorSnapshot {
1308 pub const VERSION: u32 = 5;
1329}
1330
1331#[derive(Debug, thiserror::Error)]
1335pub enum EngineError {
1336 #[error("regex compile error: {0}")]
1339 Regex(#[from] regex::Error),
1340
1341 #[error("invalid range: {0}")]
1343 InvalidRange(String),
1344
1345 #[error("ex parse: {0}")]
1347 Ex(String),
1348
1349 #[error("buffer is read-only")]
1351 ReadOnly,
1352
1353 #[error("position out of bounds: {0:?}")]
1355 OutOfBounds(Pos),
1356
1357 #[error("snapshot version mismatch: file={0}, expected={1}")]
1360 SnapshotVersion(u32, u32),
1361}
1362
1363pub(crate) mod sealed {
1364 pub trait Sealed {}
1374}
1375
1376pub trait Cursor: Send {
1382 fn cursor(&self) -> Pos;
1384 fn set_cursor(&mut self, pos: Pos);
1386 fn byte_offset(&self, pos: Pos) -> usize;
1388 fn pos_at_byte(&self, byte: usize) -> Pos;
1390}
1391
1392pub trait Query: Send {
1394 fn line_count(&self) -> u32;
1396 fn line(&self, idx: u32) -> String;
1399 fn len_bytes(&self) -> usize;
1401 fn slice(&self, range: core::ops::Range<Pos>) -> std::borrow::Cow<'_, str>;
1406 fn dirty_gen(&self) -> u64 {
1420 0
1421 }
1422
1423 fn byte_of_row(&self, row: usize) -> usize {
1434 let n = self.line_count() as usize;
1435 let row = row.min(n);
1436 let mut acc = 0usize;
1437 for r in 0..row {
1438 acc += self.line(r as u32).len();
1439 if r + 1 < n {
1444 acc += 1;
1445 }
1446 }
1447 acc
1448 }
1449
1450 fn content_joined(&self) -> std::sync::Arc<String> {
1459 let n = self.line_count() as usize;
1460 let mut acc = String::with_capacity(self.len_bytes());
1461 for r in 0..n {
1462 if r > 0 {
1463 acc.push('\n');
1464 }
1465 acc.push_str(&self.line(r as u32));
1466 }
1467 std::sync::Arc::new(acc)
1468 }
1469
1470 fn line_bytes(&self, row: usize) -> usize {
1478 let n = self.line_count() as usize;
1479 if row >= n {
1480 return 0;
1481 }
1482 self.line(row as u32).len()
1483 }
1484
1485 fn rope(&self) -> ropey::Rope {
1494 ropey::Rope::from_str(&self.content_joined())
1495 }
1496}
1497
1498pub trait BufferEdit: Send {
1502 fn insert_at(&mut self, pos: Pos, text: &str);
1505 fn delete_range(&mut self, range: core::ops::Range<Pos>);
1507 fn replace_range(&mut self, range: core::ops::Range<Pos>, replacement: &str);
1509 fn replace_all(&mut self, text: &str) {
1517 self.replace_range(
1518 Pos::ORIGIN..Pos {
1519 line: u32::MAX,
1520 col: u32::MAX,
1521 },
1522 text,
1523 );
1524 }
1525}
1526
1527pub trait Search: Send {
1530 fn find_next(&self, from: Pos, pat: ®ex::Regex) -> Option<core::ops::Range<Pos>>;
1532 fn find_prev(&self, from: Pos, pat: ®ex::Regex) -> Option<core::ops::Range<Pos>>;
1534}
1535
1536pub trait Buffer: Cursor + Query + BufferEdit + Search + sealed::Sealed + Send {}
1544
1545#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
1563#[non_exhaustive]
1564pub enum FoldOp {
1565 Add {
1569 start_row: usize,
1570 end_row: usize,
1571 closed: bool,
1572 },
1573 RemoveAt(usize),
1575 OpenAt(usize),
1577 CloseAt(usize),
1579 ToggleAt(usize),
1581 OpenAll,
1583 CloseAll,
1585 ClearAll,
1587 Invalidate { start_row: usize, end_row: usize },
1592}
1593
1594pub trait FoldProvider: Send {
1615 fn next_visible_row(&self, row: usize, row_count: usize) -> Option<usize>;
1618 fn prev_visible_row(&self, row: usize) -> Option<usize>;
1620 fn is_row_hidden(&self, row: usize) -> bool;
1622 fn fold_at_row(&self, row: usize) -> Option<(usize, usize, bool)>;
1626
1627 fn apply(&mut self, op: FoldOp) {
1637 let _ = op;
1638 }
1639
1640 fn invalidate_range(&mut self, start_row: usize, end_row: usize) {
1645 self.apply(FoldOp::Invalidate { start_row, end_row });
1646 }
1647}
1648
1649#[derive(Debug, Default, Clone, Copy)]
1652pub struct NoopFoldProvider;
1653
1654impl FoldProvider for NoopFoldProvider {
1655 fn next_visible_row(&self, row: usize, row_count: usize) -> Option<usize> {
1656 let last = row_count.saturating_sub(1);
1657 if last == 0 && row == 0 {
1658 return None;
1659 }
1660 let r = row.checked_add(1)?;
1661 (r <= last).then_some(r)
1662 }
1663
1664 fn prev_visible_row(&self, row: usize) -> Option<usize> {
1665 row.checked_sub(1)
1666 }
1667
1668 fn is_row_hidden(&self, _row: usize) -> bool {
1669 false
1670 }
1671
1672 fn fold_at_row(&self, _row: usize) -> Option<(usize, usize, bool)> {
1673 None
1674 }
1675}
1676
1677#[cfg(test)]
1678mod tests {
1679 use super::*;
1680
1681 #[test]
1682 fn caret_is_empty() {
1683 let sel = Selection::caret(Pos::new(2, 4));
1684 assert!(sel.is_empty());
1685 assert_eq!(sel.anchor, sel.head);
1686 }
1687
1688 #[test]
1689 fn selection_set_default_has_one_caret() {
1690 let set = SelectionSet::default();
1691 assert_eq!(set.items.len(), 1);
1692 assert_eq!(set.primary, 0);
1693 assert_eq!(set.primary().anchor, Pos::ORIGIN);
1694 }
1695
1696 #[test]
1697 fn edit_constructors() {
1698 let p = Pos::new(0, 5);
1699 assert_eq!(Edit::insert(p, "x").range, p..p);
1700 assert!(Edit::insert(p, "x").replacement == "x");
1701 assert!(Edit::delete(p..p).replacement.is_empty());
1702 }
1703
1704 #[test]
1705 fn attrs_flags() {
1706 let a = Attrs::BOLD | Attrs::UNDERLINE;
1707 assert!(a.contains(Attrs::BOLD));
1708 assert!(!a.contains(Attrs::ITALIC));
1709 }
1710
1711 #[test]
1712 fn options_set_get_roundtrip() {
1713 let mut o = Options::default();
1714 o.set_by_name("tabstop", OptionValue::Int(4)).unwrap();
1715 assert!(matches!(o.get_by_name("ts"), Some(OptionValue::Int(4))));
1716 o.set_by_name("expandtab", OptionValue::Bool(true)).unwrap();
1717 assert!(matches!(o.get_by_name("et"), Some(OptionValue::Bool(true))));
1718 o.set_by_name("iskeyword", OptionValue::String("a-z".into()))
1719 .unwrap();
1720 match o.get_by_name("iskeyword") {
1721 Some(OptionValue::String(s)) => assert_eq!(s, "a-z"),
1722 other => panic!("expected String, got {other:?}"),
1723 }
1724 }
1725
1726 #[test]
1727 fn options_unknown_name_errors_on_set() {
1728 let mut o = Options::default();
1729 assert!(matches!(
1730 o.set_by_name("frobnicate", OptionValue::Int(1)),
1731 Err(EngineError::Ex(_))
1732 ));
1733 assert!(o.get_by_name("frobnicate").is_none());
1734 }
1735
1736 #[test]
1737 fn options_type_mismatch_errors() {
1738 let mut o = Options::default();
1739 assert!(matches!(
1740 o.set_by_name("tabstop", OptionValue::String("nope".into())),
1741 Err(EngineError::Ex(_))
1742 ));
1743 assert!(matches!(
1744 o.set_by_name("iskeyword", OptionValue::Int(7)),
1745 Err(EngineError::Ex(_))
1746 ));
1747 }
1748
1749 #[test]
1752 fn default_options_ignorecase_and_smartcase_are_true() {
1753 let o = Options::default();
1754 assert!(o.ignorecase, "ignorecase must default to true");
1755 assert!(o.smartcase, "smartcase must default to true");
1756 }
1757
1758 #[test]
1759 fn options_int_to_bool_coercion() {
1760 let mut o = Options::default();
1763 o.set_by_name("ignorecase", OptionValue::Int(1)).unwrap();
1764 assert!(matches!(o.get_by_name("ic"), Some(OptionValue::Bool(true))));
1765 o.set_by_name("ignorecase", OptionValue::Int(0)).unwrap();
1766 assert!(matches!(
1767 o.get_by_name("ic"),
1768 Some(OptionValue::Bool(false))
1769 ));
1770 }
1771
1772 #[test]
1773 fn options_wrap_linebreak_roundtrip() {
1774 let mut o = Options::default();
1775 assert_eq!(o.wrap, WrapMode::None);
1776 o.set_by_name("wrap", OptionValue::Bool(true)).unwrap();
1777 assert_eq!(o.wrap, WrapMode::Char);
1778 o.set_by_name("linebreak", OptionValue::Bool(true)).unwrap();
1779 assert_eq!(o.wrap, WrapMode::Word);
1780 assert!(matches!(
1781 o.get_by_name("wrap"),
1782 Some(OptionValue::Bool(true))
1783 ));
1784 assert!(matches!(
1785 o.get_by_name("lbr"),
1786 Some(OptionValue::Bool(true))
1787 ));
1788 o.set_by_name("linebreak", OptionValue::Bool(false))
1789 .unwrap();
1790 assert_eq!(o.wrap, WrapMode::Char);
1791 o.set_by_name("wrap", OptionValue::Bool(false)).unwrap();
1792 assert_eq!(o.wrap, WrapMode::None);
1793 }
1794
1795 #[test]
1796 fn options_default_modern() {
1797 let o = Options::default();
1800 assert_eq!(o.tabstop, 4);
1801 assert_eq!(o.shiftwidth, 4);
1802 assert_eq!(o.softtabstop, 4);
1803 assert!(o.expandtab);
1804 assert!(o.hlsearch);
1805 assert!(o.wrapscan);
1806 assert!(o.smartindent);
1807 assert_eq!(o.timeout_len, core::time::Duration::from_millis(1000));
1808 }
1809
1810 #[test]
1811 fn editor_snapshot_version_const() {
1812 assert_eq!(EditorSnapshot::VERSION, 5);
1813 }
1814
1815 #[test]
1816 fn editor_snapshot_default_shape() {
1817 let s = EditorSnapshot {
1818 version: EditorSnapshot::VERSION,
1819 mode: SnapshotMode::Normal,
1820 cursor: (0, 0),
1821 lines: vec!["hello".to_string()],
1822 viewport_top: 0,
1823 registers: crate::Registers::default(),
1824 marks: Default::default(),
1825 global_marks: Default::default(),
1826 };
1827 assert_eq!(s.cursor, (0, 0));
1828 assert_eq!(s.lines.len(), 1);
1829 }
1830
1831 #[cfg(feature = "serde")]
1832 #[test]
1833 fn editor_snapshot_roundtrip() {
1834 let mut marks = std::collections::BTreeMap::new();
1835 marks.insert('a', (1u32, 0u32));
1836 let mut global_marks = std::collections::BTreeMap::new();
1837 global_marks.insert('A', (42u64, 5u32, 2u32));
1838 let s = EditorSnapshot {
1839 version: EditorSnapshot::VERSION,
1840 mode: SnapshotMode::Insert,
1841 cursor: (3, 7),
1842 lines: vec!["alpha".into(), "beta".into()],
1843 viewport_top: 2,
1844 registers: crate::Registers::default(),
1845 marks,
1846 global_marks,
1847 };
1848 let json = serde_json::to_string(&s).unwrap();
1849 let back: EditorSnapshot = serde_json::from_str(&json).unwrap();
1850 assert_eq!(s.cursor, back.cursor);
1851 assert_eq!(s.lines, back.lines);
1852 assert_eq!(s.viewport_top, back.viewport_top);
1853 assert_eq!(s.global_marks, back.global_marks);
1854 }
1855
1856 #[test]
1857 fn engine_error_display() {
1858 let e = EngineError::ReadOnly;
1859 assert_eq!(e.to_string(), "buffer is read-only");
1860 let e = EngineError::OutOfBounds(Pos::new(3, 7));
1861 assert!(e.to_string().contains("out of bounds"));
1862 }
1863
1864 #[test]
1867 fn options_cursorline_roundtrip() {
1868 let mut o = Options::default();
1869 assert!(o.cursorline, "cursorline defaults to true");
1870 o.set_by_name("cursorline", OptionValue::Bool(false))
1871 .unwrap();
1872 assert!(matches!(
1873 o.get_by_name("cul"),
1874 Some(OptionValue::Bool(false))
1875 ));
1876 o.set_by_name("cul", OptionValue::Bool(true)).unwrap();
1877 assert!(matches!(
1878 o.get_by_name("cursorline"),
1879 Some(OptionValue::Bool(true))
1880 ));
1881 }
1882
1883 #[test]
1884 fn options_cursorcolumn_roundtrip() {
1885 let mut o = Options::default();
1886 assert!(!o.cursorcolumn, "cursorcolumn defaults to false");
1887 o.set_by_name("cuc", OptionValue::Bool(true)).unwrap();
1888 assert!(matches!(
1889 o.get_by_name("cursorcolumn"),
1890 Some(OptionValue::Bool(true))
1891 ));
1892 }
1893
1894 #[test]
1895 fn options_signcolumn_roundtrip() {
1896 let mut o = Options::default();
1897 assert_eq!(
1898 o.signcolumn,
1899 SignColumnMode::Auto,
1900 "signcolumn defaults to auto"
1901 );
1902 o.set_by_name("signcolumn", OptionValue::String("yes".into()))
1903 .unwrap();
1904 assert_eq!(o.signcolumn, SignColumnMode::Yes);
1905 assert_eq!(
1906 o.get_by_name("scl"),
1907 Some(OptionValue::String("yes".into()))
1908 );
1909 o.set_by_name("scl", OptionValue::String("no".into()))
1910 .unwrap();
1911 assert_eq!(o.signcolumn, SignColumnMode::No);
1912 o.set_by_name("scl", OptionValue::String("auto".into()))
1913 .unwrap();
1914 assert_eq!(o.signcolumn, SignColumnMode::Auto);
1915 }
1916
1917 #[test]
1918 fn options_signcolumn_rejects_invalid() {
1919 let mut o = Options::default();
1920 assert!(matches!(
1921 o.set_by_name("signcolumn", OptionValue::String("maybe".into())),
1922 Err(EngineError::Ex(_))
1923 ));
1924 assert!(matches!(
1926 o.set_by_name("signcolumn", OptionValue::Bool(true)),
1927 Err(EngineError::Ex(_))
1928 ));
1929 }
1930
1931 #[test]
1932 fn options_foldcolumn_roundtrip() {
1933 let mut o = Options::default();
1934 assert_eq!(o.foldcolumn, 0, "foldcolumn defaults to 0");
1935 o.set_by_name("fdc", OptionValue::Int(3)).unwrap();
1936 assert_eq!(o.foldcolumn, 3);
1937 assert_eq!(o.get_by_name("foldcolumn"), Some(OptionValue::Int(3)));
1938 }
1939
1940 #[test]
1941 fn options_foldcolumn_rejects_out_of_range() {
1942 let mut o = Options::default();
1943 assert!(matches!(
1944 o.set_by_name("foldcolumn", OptionValue::Int(13)),
1945 Err(EngineError::Ex(_))
1946 ));
1947 assert!(matches!(
1948 o.set_by_name("foldcolumn", OptionValue::Int(-1)),
1949 Err(EngineError::Ex(_))
1950 ));
1951 }
1952
1953 #[test]
1954 fn options_colorcolumn_roundtrip() {
1955 let mut o = Options::default();
1956 assert_eq!(o.colorcolumn, "", "colorcolumn defaults to empty string");
1957 o.set_by_name("cc", OptionValue::String("80,120".into()))
1958 .unwrap();
1959 assert_eq!(
1960 o.get_by_name("colorcolumn"),
1961 Some(OptionValue::String("80,120".into()))
1962 );
1963 o.set_by_name("colorcolumn", OptionValue::String(String::new()))
1964 .unwrap();
1965 assert_eq!(
1966 o.get_by_name("cc"),
1967 Some(OptionValue::String(String::new()))
1968 );
1969 }
1970
1971 #[test]
1972 fn options_cursorline_alias_cul() {
1973 let mut o = Options::default();
1974 o.set_by_name("cul", OptionValue::Bool(true)).unwrap();
1976 assert!(o.cursorline);
1977 o.set_by_name("cul", OptionValue::Bool(false)).unwrap();
1979 assert!(!o.cursorline);
1980 }
1981
1982 #[test]
1983 fn sign_column_mode_default_is_auto() {
1984 assert_eq!(SignColumnMode::default(), SignColumnMode::Auto);
1985 }
1986
1987 #[test]
1988 fn options_scrolloff_default_and_set() {
1989 let mut o = Options::default();
1990 assert_eq!(o.scrolloff, 5, "scrolloff defaults to 5");
1991 o.set_by_name("scrolloff", OptionValue::Int(0)).unwrap();
1992 assert_eq!(o.scrolloff, 0);
1993 o.set_by_name("scrolloff", OptionValue::Int(999)).unwrap();
1994 assert_eq!(o.scrolloff, 999);
1995 assert_eq!(o.get_by_name("scrolloff"), Some(OptionValue::Int(999)));
1996 }
1997
1998 #[test]
1999 fn options_sidescrolloff_default_and_set() {
2000 let mut o = Options::default();
2001 assert_eq!(o.sidescrolloff, 0, "sidescrolloff defaults to 0");
2002 o.set_by_name("sidescrolloff", OptionValue::Int(5)).unwrap();
2003 assert_eq!(o.sidescrolloff, 5);
2004 assert_eq!(o.get_by_name("sidescrolloff"), Some(OptionValue::Int(5)));
2005 }
2006
2007 #[test]
2008 fn options_alias_so_siso() {
2009 let mut o = Options::default();
2010 o.set_by_name("so", OptionValue::Int(3)).unwrap();
2012 assert_eq!(o.scrolloff, 3);
2013 assert_eq!(o.get_by_name("so"), Some(OptionValue::Int(3)));
2014 o.set_by_name("siso", OptionValue::Int(2)).unwrap();
2016 assert_eq!(o.sidescrolloff, 2);
2017 assert_eq!(o.get_by_name("siso"), Some(OptionValue::Int(2)));
2018 }
2019
2020 #[test]
2023 fn options_list_default_false_and_set() {
2024 let mut o = Options::default();
2025 assert!(!o.list, "list default is false");
2026 o.set_by_name("list", OptionValue::Bool(true)).unwrap();
2027 assert!(o.list);
2028 assert_eq!(o.get_by_name("list"), Some(OptionValue::Bool(true)));
2029 o.set_by_name("list", OptionValue::Bool(false)).unwrap();
2030 assert!(!o.list);
2031 }
2032
2033 #[test]
2034 fn options_listchars_default_matches_vim() {
2035 let o = Options::default();
2036 let lc = &o.listchars;
2037 assert_eq!(lc.tab_lead, '^');
2038 assert_eq!(lc.tab_fill, Some('I'));
2039 assert_eq!(lc.eol, Some('$'));
2040 assert_eq!(lc.space, None);
2041 assert_eq!(lc.trail, None);
2042 assert_eq!(lc.nbsp, None);
2043 }
2044
2045 #[test]
2046 fn options_listchars_set_and_get() {
2047 let mut o = Options::default();
2048 o.set_by_name("listchars", OptionValue::String("tab:>-,eol:$".to_string()))
2049 .unwrap();
2050 assert_eq!(o.listchars.tab_lead, '>');
2051 assert_eq!(o.listchars.tab_fill, Some('-'));
2052 assert_eq!(o.listchars.eol, Some('$'));
2053 }
2054
2055 #[test]
2056 fn options_lcs_alias_sets_listchars() {
2057 let mut o = Options::default();
2058 o.set_by_name("lcs", OptionValue::String("tab:>-,trail:~".to_string()))
2059 .unwrap();
2060 assert_eq!(o.listchars.tab_lead, '>');
2061 assert_eq!(o.listchars.trail, Some('~'));
2062 }
2063
2064 #[test]
2065 fn options_listchars_get_by_name_returns_string() {
2066 let o = Options::default();
2067 match o.get_by_name("listchars") {
2068 Some(OptionValue::String(s)) => {
2069 assert!(s.contains("tab:"), "canonical string should contain tab:");
2070 }
2071 other => panic!("expected String, got {other:?}"),
2072 }
2073 }
2074
2075 #[test]
2076 fn options_listchars_invalid_value_returns_err() {
2077 let mut o = Options::default();
2078 assert!(
2079 o.set_by_name("listchars", OptionValue::String("bogus:x".to_string()))
2080 .is_err()
2081 );
2082 }
2083
2084 #[test]
2087 fn indent_guides_default_true() {
2088 assert!(
2089 Options::default().indent_guides,
2090 "indent_guides must default to true"
2091 );
2092 }
2093
2094 #[test]
2095 fn options_indent_guides_set_and_get() {
2096 let mut opts = Options::default();
2097 opts.set_by_name("indent_guides", OptionValue::Bool(false))
2099 .unwrap();
2100 assert!(!opts.indent_guides);
2101 opts.set_by_name("ig", OptionValue::Bool(true)).unwrap();
2103 assert!(opts.indent_guides);
2104 assert_eq!(opts.get_by_name("ig"), Some(OptionValue::Bool(true)));
2106 assert_eq!(
2107 opts.get_by_name("indent_guides"),
2108 Some(OptionValue::Bool(true))
2109 );
2110 }
2111
2112 #[test]
2113 fn options_indent_guide_char_set_and_get() {
2114 let mut opts = Options::default();
2115 opts.set_by_name("indent_guide_char", OptionValue::String(":".to_string()))
2116 .unwrap();
2117 assert_eq!(opts.indent_guide_char, ':');
2118 opts.set_by_name("igc", OptionValue::String("┊".to_string()))
2120 .unwrap();
2121 assert_eq!(opts.indent_guide_char, '┊');
2122 assert_eq!(
2124 opts.get_by_name("igc"),
2125 Some(OptionValue::String("┊".to_string()))
2126 );
2127 assert_eq!(
2128 opts.get_by_name("indent_guide_char"),
2129 Some(OptionValue::String("┊".to_string()))
2130 );
2131 }
2132
2133 #[test]
2134 fn options_indent_guide_char_rejects_multi_char() {
2135 let mut opts = Options::default();
2136 assert!(
2137 opts.set_by_name("indent_guide_char", OptionValue::String("ab".to_string()))
2138 .is_err(),
2139 "multi-char value must be rejected"
2140 );
2141 }
2142
2143 #[test]
2144 fn options_indent_guide_char_rejects_empty() {
2145 let mut opts = Options::default();
2146 assert!(
2147 opts.set_by_name("indent_guide_char", OptionValue::String(String::new()))
2148 .is_err(),
2149 "empty string must be rejected"
2150 );
2151 }
2152
2153 #[test]
2156 fn colorizer_default_true() {
2157 assert!(
2158 Options::default().colorizer,
2159 "colorizer must default to true"
2160 );
2161 }
2162
2163 #[test]
2164 fn colorizer_filetypes_includes_css() {
2165 let o = Options::default();
2166 assert!(
2167 o.colorizer_filetypes.iter().any(|f| f == "css"),
2168 "default colorizer_filetypes must include 'css'"
2169 );
2170 }
2171
2172 #[test]
2173 fn options_colorizer_set_and_get() {
2174 let mut o = Options::default();
2175 o.set_by_name("colorizer", OptionValue::Bool(false))
2176 .unwrap();
2177 assert_eq!(o.get_by_name("colorizer"), Some(OptionValue::Bool(false)));
2178 o.set_by_name("clz", OptionValue::Bool(true)).unwrap();
2179 assert_eq!(o.get_by_name("clz"), Some(OptionValue::Bool(true)));
2180 }
2181
2182 #[test]
2183 fn options_colorizer_filetypes_set_and_get() {
2184 let mut o = Options::default();
2185 o.set_by_name(
2186 "colorizer_filetypes",
2187 OptionValue::String("css,scss,toml".into()),
2188 )
2189 .unwrap();
2190 assert_eq!(o.colorizer_filetypes, vec!["css", "scss", "toml"]);
2191 assert_eq!(
2192 o.get_by_name("clzft"),
2193 Some(OptionValue::String("css,scss,toml".into()))
2194 );
2195 }
2196
2197 #[test]
2200 fn format_on_save_default_true() {
2201 let o = Options::default();
2202 assert!(o.format_on_save, "format_on_save must default to true");
2203 }
2204
2205 #[test]
2206 fn trim_trailing_whitespace_default_false() {
2207 let o = Options::default();
2208 assert!(
2209 !o.trim_trailing_whitespace,
2210 "trim_trailing_whitespace must default to false"
2211 );
2212 }
2213
2214 #[test]
2215 fn options_fos_alias_sets_format_on_save() {
2216 let mut o = Options::default();
2217 o.set_by_name("fos", OptionValue::Bool(true)).unwrap();
2218 assert!(o.format_on_save, "fos alias must set format_on_save");
2219 assert_eq!(
2220 o.get_by_name("fos"),
2221 Some(OptionValue::Bool(true)),
2222 "get_by_name(fos) must reflect the new value"
2223 );
2224 assert_eq!(
2225 o.get_by_name("format_on_save"),
2226 Some(OptionValue::Bool(true)),
2227 "get_by_name(format_on_save) must also reflect the new value"
2228 );
2229 }
2230
2231 #[test]
2232 fn options_tts_alias_sets_trim_trailing_whitespace() {
2233 let mut o = Options::default();
2234 o.set_by_name("tts", OptionValue::Bool(true)).unwrap();
2235 assert!(
2236 o.trim_trailing_whitespace,
2237 "tts alias must set trim_trailing_whitespace"
2238 );
2239 assert_eq!(
2240 o.get_by_name("tts"),
2241 Some(OptionValue::Bool(true)),
2242 "get_by_name(tts) must reflect the new value"
2243 );
2244 assert_eq!(
2245 o.get_by_name("trim_trailing_whitespace"),
2246 Some(OptionValue::Bool(true)),
2247 "get_by_name(trim_trailing_whitespace) must also reflect the new value"
2248 );
2249 }
2250
2251 #[test]
2254 fn rainbow_brackets_default_true() {
2255 let o = Options::default();
2256 assert!(o.rainbow_brackets, "rainbow_brackets must default to true");
2257 }
2258
2259 #[test]
2260 fn options_rb_alias_sets_rainbow_brackets() {
2261 let mut o = Options::default();
2262 o.set_by_name("rb", OptionValue::Bool(false)).unwrap();
2263 assert!(
2264 !o.rainbow_brackets,
2265 "rb alias must set rainbow_brackets to false"
2266 );
2267 assert_eq!(
2268 o.get_by_name("rb"),
2269 Some(OptionValue::Bool(false)),
2270 "get_by_name(rb) must reflect the new value"
2271 );
2272 assert_eq!(
2273 o.get_by_name("rainbow_brackets"),
2274 Some(OptionValue::Bool(false)),
2275 "get_by_name(rainbow_brackets) must also reflect the new value"
2276 );
2277 }
2278
2279 #[test]
2280 fn autoreload_default_true() {
2281 assert!(
2282 Options::default().autoreload,
2283 "autoreload must default true"
2284 );
2285 }
2286
2287 #[test]
2288 fn options_ar_alias_sets_autoreload() {
2289 let mut o = Options::default();
2290 o.set_by_name("ar", OptionValue::Bool(false)).unwrap();
2291 assert!(!o.autoreload, "ar alias must set autoreload");
2292 assert_eq!(o.get_by_name("autoreload"), Some(OptionValue::Bool(false)));
2293 }
2294
2295 #[test]
2298 fn updatetime_default_4000() {
2299 let o = Options::default();
2300 assert_eq!(o.updatetime, 4000, "updatetime must default to 4000 ms");
2301 assert_eq!(
2302 o.get_by_name("updatetime"),
2303 Some(OptionValue::Int(4000)),
2304 "get_by_name(updatetime) must return Int(4000)"
2305 );
2306 }
2307
2308 #[test]
2309 fn options_ut_alias_sets_updatetime() {
2310 let mut o = Options::default();
2311 o.set_by_name("ut", OptionValue::Int(1000)).unwrap();
2312 assert_eq!(o.updatetime, 1000, "ut alias must set updatetime");
2313 assert_eq!(
2314 o.get_by_name("ut"),
2315 Some(OptionValue::Int(1000)),
2316 "get_by_name(ut) must reflect the new value"
2317 );
2318 assert_eq!(
2319 o.get_by_name("updatetime"),
2320 Some(OptionValue::Int(1000)),
2321 "get_by_name(updatetime) must also reflect the new value"
2322 );
2323 }
2324
2325 #[test]
2328 fn matchparen_default_true() {
2329 let o = Options::default();
2330 assert!(o.matchparen, "matchparen must default to true");
2331 assert_eq!(
2332 o.get_by_name("matchparen"),
2333 Some(OptionValue::Bool(true)),
2334 "get_by_name(matchparen) must return Bool(true)"
2335 );
2336 }
2337
2338 #[test]
2339 fn options_matchparen_set_and_get() {
2340 let mut o = Options::default();
2341 o.set_by_name("matchparen", OptionValue::Bool(false))
2342 .unwrap();
2343 assert!(!o.matchparen, "matchparen must be false after set");
2344 assert_eq!(
2345 o.get_by_name("matchparen"),
2346 Some(OptionValue::Bool(false)),
2347 "get_by_name(matchparen) must reflect false"
2348 );
2349 o.set_by_name("mps", OptionValue::Bool(true)).unwrap();
2351 assert!(o.matchparen, "mps alias must set matchparen to true");
2352 assert_eq!(
2353 o.get_by_name("mps"),
2354 Some(OptionValue::Bool(true)),
2355 "get_by_name(mps) must reflect true"
2356 );
2357 }
2358
2359 #[test]
2362 fn foldmethod_default_expr() {
2363 let o = Options::default();
2364 assert_eq!(
2365 o.foldmethod,
2366 FoldMethod::Expr,
2367 "foldmethod must default to Expr (tree-sitter)"
2368 );
2369 assert_eq!(
2370 o.get_by_name("foldmethod"),
2371 Some(OptionValue::String("expr".into())),
2372 "get_by_name(foldmethod) must return \"expr\""
2373 );
2374 }
2375
2376 #[test]
2377 fn foldmethod_fdm_alias_roundtrip() {
2378 let mut o = Options::default();
2379 o.set_by_name("fdm", OptionValue::String("manual".into()))
2380 .unwrap();
2381 assert_eq!(o.foldmethod, FoldMethod::Manual);
2382 assert_eq!(
2383 o.get_by_name("fdm"),
2384 Some(OptionValue::String("manual".into()))
2385 );
2386 o.set_by_name("foldmethod", OptionValue::String("expr".into()))
2387 .unwrap();
2388 assert_eq!(o.foldmethod, FoldMethod::Expr);
2389 o.set_by_name("foldmethod", OptionValue::String("marker".into()))
2390 .unwrap();
2391 assert_eq!(o.foldmethod, FoldMethod::Marker);
2392 o.set_by_name("foldmethod", OptionValue::String("syntax".into()))
2394 .unwrap();
2395 assert_eq!(o.foldmethod, FoldMethod::Expr);
2396 }
2397
2398 #[test]
2399 fn foldmethod_rejects_invalid_value() {
2400 let mut o = Options::default();
2401 let err = o
2402 .set_by_name("foldmethod", OptionValue::String("bogus".into()))
2403 .unwrap_err();
2404 assert!(
2405 err.to_string().contains("must be"),
2406 "expected error about valid values, got: {err}"
2407 );
2408 }
2409
2410 #[test]
2411 fn foldenable_default_true() {
2412 let o = Options::default();
2413 assert!(o.foldenable, "foldenable must default to true");
2414 assert_eq!(
2415 o.get_by_name("foldenable"),
2416 Some(OptionValue::Bool(true)),
2417 "get_by_name(foldenable) must return Bool(true)"
2418 );
2419 }
2420
2421 #[test]
2422 fn foldenable_fen_alias_roundtrip() {
2423 let mut o = Options::default();
2424 o.set_by_name("fen", OptionValue::Bool(false)).unwrap();
2425 assert!(!o.foldenable, "fen alias must disable foldenable");
2426 assert_eq!(o.get_by_name("fen"), Some(OptionValue::Bool(false)));
2427 o.set_by_name("foldenable", OptionValue::Bool(true))
2428 .unwrap();
2429 assert!(o.foldenable);
2430 }
2431
2432 #[test]
2433 fn foldlevelstart_default_99() {
2434 let o = Options::default();
2435 assert_eq!(o.foldlevelstart, 99, "foldlevelstart must default to 99");
2436 assert_eq!(
2437 o.get_by_name("foldlevelstart"),
2438 Some(OptionValue::Int(99)),
2439 "get_by_name(foldlevelstart) must return Int(99)"
2440 );
2441 }
2442
2443 #[test]
2444 fn foldlevelstart_fls_alias_roundtrip() {
2445 let mut o = Options::default();
2446 o.set_by_name("fls", OptionValue::Int(0)).unwrap();
2447 assert_eq!(
2448 o.foldlevelstart, 0,
2449 "fls alias must set foldlevelstart to 0"
2450 );
2451 assert_eq!(o.get_by_name("fls"), Some(OptionValue::Int(0)));
2452 o.set_by_name("foldlevelstart", OptionValue::Int(5))
2453 .unwrap();
2454 assert_eq!(o.foldlevelstart, 5);
2455 }
2456}