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