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
364pub enum FoldMethod {
365 Manual,
367 #[default]
372 Expr,
373 Marker,
376}
377
378#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
381
382pub enum SignColumnMode {
383 No,
385 Yes,
387 #[default]
389 Auto,
390}
391
392#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
396
397pub enum DiagInlineMode {
398 Off,
400 Current,
402 #[default]
404 All,
405}
406
407#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
411
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
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
1242pub enum SnapshotMode {
1243 #[default]
1244 Normal,
1245 Insert,
1246 Visual,
1247 VisualLine,
1248 VisualBlock,
1249}
1250
1251impl EditorSnapshot {
1252 pub const VERSION: u32 = 5;
1273}
1274
1275#[derive(Debug, thiserror::Error)]
1279pub enum EngineError {
1280 #[error("regex compile error: {0}")]
1283 Regex(#[from] regex::Error),
1284
1285 #[error("invalid range: {0}")]
1287 InvalidRange(String),
1288
1289 #[error("ex parse: {0}")]
1291 Ex(String),
1292
1293 #[error("buffer is read-only")]
1295 ReadOnly,
1296
1297 #[error("position out of bounds: {0:?}")]
1299 OutOfBounds(Pos),
1300
1301 #[error("snapshot version mismatch: file={0}, expected={1}")]
1304 SnapshotVersion(u32, u32),
1305}
1306
1307pub(crate) mod sealed {
1308 pub trait Sealed {}
1318}
1319
1320pub trait Cursor: Send {
1326 fn cursor(&self) -> Pos;
1328 fn set_cursor(&mut self, pos: Pos);
1330 fn byte_offset(&self, pos: Pos) -> usize;
1332 fn pos_at_byte(&self, byte: usize) -> Pos;
1334}
1335
1336pub trait Query: Send {
1338 fn line_count(&self) -> u32;
1340 fn line(&self, idx: u32) -> String;
1343 fn len_bytes(&self) -> usize;
1345 fn slice(&self, range: core::ops::Range<Pos>) -> std::borrow::Cow<'_, str>;
1350 fn dirty_gen(&self) -> u64 {
1364 0
1365 }
1366
1367 fn byte_of_row(&self, row: usize) -> usize {
1378 let n = self.line_count() as usize;
1379 let row = row.min(n);
1380 let mut acc = 0usize;
1381 for r in 0..row {
1382 acc += self.line(r as u32).len();
1383 if r + 1 < n {
1388 acc += 1;
1389 }
1390 }
1391 acc
1392 }
1393
1394 fn content_joined(&self) -> std::sync::Arc<String> {
1403 let n = self.line_count() as usize;
1404 let mut acc = String::with_capacity(self.len_bytes());
1405 for r in 0..n {
1406 if r > 0 {
1407 acc.push('\n');
1408 }
1409 acc.push_str(&self.line(r as u32));
1410 }
1411 std::sync::Arc::new(acc)
1412 }
1413
1414 fn line_bytes(&self, row: usize) -> usize {
1422 let n = self.line_count() as usize;
1423 if row >= n {
1424 return 0;
1425 }
1426 self.line(row as u32).len()
1427 }
1428
1429 fn rope(&self) -> ropey::Rope {
1438 ropey::Rope::from_str(&self.content_joined())
1439 }
1440}
1441
1442pub trait BufferEdit: Send {
1446 fn insert_at(&mut self, pos: Pos, text: &str);
1449 fn delete_range(&mut self, range: core::ops::Range<Pos>);
1451 fn replace_range(&mut self, range: core::ops::Range<Pos>, replacement: &str);
1453 fn replace_all(&mut self, text: &str) {
1461 self.replace_range(
1462 Pos::ORIGIN..Pos {
1463 line: u32::MAX,
1464 col: u32::MAX,
1465 },
1466 text,
1467 );
1468 }
1469}
1470
1471pub trait Search: Send {
1474 fn find_next(&self, from: Pos, pat: ®ex::Regex) -> Option<core::ops::Range<Pos>>;
1476 fn find_prev(&self, from: Pos, pat: ®ex::Regex) -> Option<core::ops::Range<Pos>>;
1478}
1479
1480pub trait View: Cursor + Query + BufferEdit + Search + sealed::Sealed + Send {}
1488
1489pub trait FoldProvider: Send {
1510 fn next_visible_row(&self, row: usize, row_count: usize) -> Option<usize>;
1513 fn prev_visible_row(&self, row: usize) -> Option<usize>;
1515 fn is_row_hidden(&self, row: usize) -> bool;
1517 fn fold_at_row(&self, row: usize) -> Option<(usize, usize, bool)>;
1521
1522 fn apply(&mut self, op: FoldOp) {
1532 let _ = op;
1533 }
1534
1535 fn invalidate_range(&mut self, start_row: usize, end_row: usize) {
1540 self.apply(FoldOp::Invalidate { start_row, end_row });
1541 }
1542}
1543
1544#[derive(Debug, Default, Clone, Copy)]
1547pub struct NoopFoldProvider;
1548
1549impl FoldProvider for NoopFoldProvider {
1550 fn next_visible_row(&self, row: usize, row_count: usize) -> Option<usize> {
1551 let last = row_count.saturating_sub(1);
1552 if last == 0 && row == 0 {
1553 return None;
1554 }
1555 let r = row.checked_add(1)?;
1556 (r <= last).then_some(r)
1557 }
1558
1559 fn prev_visible_row(&self, row: usize) -> Option<usize> {
1560 row.checked_sub(1)
1561 }
1562
1563 fn is_row_hidden(&self, _row: usize) -> bool {
1564 false
1565 }
1566
1567 fn fold_at_row(&self, _row: usize) -> Option<(usize, usize, bool)> {
1568 None
1569 }
1570}
1571
1572#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1574pub enum InsertDir {
1575 Left,
1576 Right,
1577 Up,
1578 Down,
1579}
1580
1581#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1584pub enum ScrollDir {
1585 Down,
1587 Up,
1589}
1590
1591pub const SEARCH_HISTORY_MAX: usize = 100;
1592pub const CHANGE_LIST_MAX: usize = 100;
1593
1594pub const JUMPLIST_MAX: usize = 100;
1596
1597#[cfg(test)]
1598mod tests {
1599 use super::*;
1600
1601 #[test]
1602 fn caret_is_empty() {
1603 let sel = Selection::caret(Pos::new(2, 4));
1604 assert!(sel.is_empty());
1605 assert_eq!(sel.anchor, sel.head);
1606 }
1607
1608 #[test]
1609 fn selection_set_default_has_one_caret() {
1610 let set = SelectionSet::default();
1611 assert_eq!(set.items.len(), 1);
1612 assert_eq!(set.primary, 0);
1613 assert_eq!(set.primary().anchor, Pos::ORIGIN);
1614 }
1615
1616 #[test]
1617 fn edit_constructors() {
1618 let p = Pos::new(0, 5);
1619 assert_eq!(Edit::insert(p, "x").range, p..p);
1620 assert!(Edit::insert(p, "x").replacement == "x");
1621 assert!(Edit::delete(p..p).replacement.is_empty());
1622 }
1623
1624 #[test]
1625 fn attrs_flags() {
1626 let a = Attrs::BOLD | Attrs::UNDERLINE;
1627 assert!(a.contains(Attrs::BOLD));
1628 assert!(!a.contains(Attrs::ITALIC));
1629 }
1630
1631 #[test]
1632 fn options_set_get_roundtrip() {
1633 let mut o = Options::default();
1634 o.set_by_name("tabstop", OptionValue::Int(4)).unwrap();
1635 assert!(matches!(o.get_by_name("ts"), Some(OptionValue::Int(4))));
1636 o.set_by_name("expandtab", OptionValue::Bool(true)).unwrap();
1637 assert!(matches!(o.get_by_name("et"), Some(OptionValue::Bool(true))));
1638 o.set_by_name("iskeyword", OptionValue::String("a-z".into()))
1639 .unwrap();
1640 match o.get_by_name("iskeyword") {
1641 Some(OptionValue::String(s)) => assert_eq!(s, "a-z"),
1642 other => panic!("expected String, got {other:?}"),
1643 }
1644 }
1645
1646 #[test]
1647 fn options_unknown_name_errors_on_set() {
1648 let mut o = Options::default();
1649 assert!(matches!(
1650 o.set_by_name("frobnicate", OptionValue::Int(1)),
1651 Err(EngineError::Ex(_))
1652 ));
1653 assert!(o.get_by_name("frobnicate").is_none());
1654 }
1655
1656 #[test]
1666 fn set_by_name_rejects_makeprg_and_errorformat() {
1667 let mut o = Options::default();
1668 assert!(
1669 matches!(
1670 o.set_by_name("makeprg", OptionValue::String("rm -rf /".into())),
1671 Err(EngineError::Ex(_))
1672 ),
1673 "`makeprg` must never be a settable option — a modeline must \
1674 never be able to smuggle an arbitrary shell command into `:make`"
1675 );
1676 assert!(o.get_by_name("makeprg").is_none());
1677 assert!(
1678 matches!(
1679 o.set_by_name("errorformat", OptionValue::String("%f:%l:%m".into())),
1680 Err(EngineError::Ex(_))
1681 ),
1682 "`errorformat` rides the same `:make`/`:grep` shell-out surface \
1683 as `makeprg` and must stay unsettable too"
1684 );
1685 assert!(o.get_by_name("errorformat").is_none());
1686 }
1687
1688 #[test]
1689 fn options_type_mismatch_errors() {
1690 let mut o = Options::default();
1691 assert!(matches!(
1692 o.set_by_name("tabstop", OptionValue::String("nope".into())),
1693 Err(EngineError::Ex(_))
1694 ));
1695 assert!(matches!(
1696 o.set_by_name("iskeyword", OptionValue::Int(7)),
1697 Err(EngineError::Ex(_))
1698 ));
1699 }
1700
1701 #[test]
1704 fn default_options_ignorecase_and_smartcase_are_true() {
1705 let o = Options::default();
1706 assert!(o.ignorecase, "ignorecase must default to true");
1707 assert!(o.smartcase, "smartcase must default to true");
1708 }
1709
1710 #[test]
1711 fn options_int_to_bool_coercion() {
1712 let mut o = Options::default();
1715 o.set_by_name("ignorecase", OptionValue::Int(1)).unwrap();
1716 assert!(matches!(o.get_by_name("ic"), Some(OptionValue::Bool(true))));
1717 o.set_by_name("ignorecase", OptionValue::Int(0)).unwrap();
1718 assert!(matches!(
1719 o.get_by_name("ic"),
1720 Some(OptionValue::Bool(false))
1721 ));
1722 }
1723
1724 #[test]
1725 fn options_wrap_linebreak_roundtrip() {
1726 let mut o = Options::default();
1727 assert_eq!(o.wrap, WrapMode::None);
1728 o.set_by_name("wrap", OptionValue::Bool(true)).unwrap();
1729 assert_eq!(o.wrap, WrapMode::Char);
1730 o.set_by_name("linebreak", OptionValue::Bool(true)).unwrap();
1731 assert_eq!(o.wrap, WrapMode::Word);
1732 assert!(matches!(
1733 o.get_by_name("wrap"),
1734 Some(OptionValue::Bool(true))
1735 ));
1736 assert!(matches!(
1737 o.get_by_name("lbr"),
1738 Some(OptionValue::Bool(true))
1739 ));
1740 o.set_by_name("linebreak", OptionValue::Bool(false))
1741 .unwrap();
1742 assert_eq!(o.wrap, WrapMode::Char);
1743 o.set_by_name("wrap", OptionValue::Bool(false)).unwrap();
1744 assert_eq!(o.wrap, WrapMode::None);
1745 }
1746
1747 #[test]
1748 fn options_default_modern() {
1749 let o = Options::default();
1752 assert_eq!(o.tabstop, 4);
1753 assert_eq!(o.shiftwidth, 4);
1754 assert_eq!(o.softtabstop, 4);
1755 assert!(o.expandtab);
1756 assert!(o.hlsearch);
1757 assert!(o.wrapscan);
1758 assert!(o.smartindent);
1759 assert_eq!(o.timeout_len, core::time::Duration::from_millis(1000));
1760 }
1761
1762 #[test]
1763 fn editor_snapshot_version_const() {
1764 assert_eq!(EditorSnapshot::VERSION, 5);
1765 }
1766
1767 #[test]
1768 fn editor_snapshot_default_shape() {
1769 let s = EditorSnapshot {
1770 version: EditorSnapshot::VERSION,
1771 mode: SnapshotMode::Normal,
1772 cursor: (0, 0),
1773 lines: vec!["hello".to_string()],
1774 viewport_top: 0,
1775 registers: crate::Registers::default(),
1776 marks: Default::default(),
1777 global_marks: Default::default(),
1778 };
1779 assert_eq!(s.cursor, (0, 0));
1780 assert_eq!(s.lines.len(), 1);
1781 }
1782
1783 #[test]
1784 fn engine_error_display() {
1785 let e = EngineError::ReadOnly;
1786 assert_eq!(e.to_string(), "buffer is read-only");
1787 let e = EngineError::OutOfBounds(Pos::new(3, 7));
1788 assert!(e.to_string().contains("out of bounds"));
1789 }
1790
1791 #[test]
1794 fn options_cursorline_roundtrip() {
1795 let mut o = Options::default();
1796 assert!(o.cursorline, "cursorline defaults to true");
1797 o.set_by_name("cursorline", OptionValue::Bool(false))
1798 .unwrap();
1799 assert!(matches!(
1800 o.get_by_name("cul"),
1801 Some(OptionValue::Bool(false))
1802 ));
1803 o.set_by_name("cul", OptionValue::Bool(true)).unwrap();
1804 assert!(matches!(
1805 o.get_by_name("cursorline"),
1806 Some(OptionValue::Bool(true))
1807 ));
1808 }
1809
1810 #[test]
1811 fn options_cursorcolumn_roundtrip() {
1812 let mut o = Options::default();
1813 assert!(!o.cursorcolumn, "cursorcolumn defaults to false");
1814 o.set_by_name("cuc", OptionValue::Bool(true)).unwrap();
1815 assert!(matches!(
1816 o.get_by_name("cursorcolumn"),
1817 Some(OptionValue::Bool(true))
1818 ));
1819 }
1820
1821 #[test]
1822 fn options_signcolumn_roundtrip() {
1823 let mut o = Options::default();
1824 assert_eq!(
1825 o.signcolumn,
1826 SignColumnMode::Auto,
1827 "signcolumn defaults to auto"
1828 );
1829 o.set_by_name("signcolumn", OptionValue::String("yes".into()))
1830 .unwrap();
1831 assert_eq!(o.signcolumn, SignColumnMode::Yes);
1832 assert_eq!(
1833 o.get_by_name("scl"),
1834 Some(OptionValue::String("yes".into()))
1835 );
1836 o.set_by_name("scl", OptionValue::String("no".into()))
1837 .unwrap();
1838 assert_eq!(o.signcolumn, SignColumnMode::No);
1839 o.set_by_name("scl", OptionValue::String("auto".into()))
1840 .unwrap();
1841 assert_eq!(o.signcolumn, SignColumnMode::Auto);
1842 }
1843
1844 #[test]
1845 fn options_signcolumn_rejects_invalid() {
1846 let mut o = Options::default();
1847 assert!(matches!(
1848 o.set_by_name("signcolumn", OptionValue::String("maybe".into())),
1849 Err(EngineError::Ex(_))
1850 ));
1851 assert!(matches!(
1853 o.set_by_name("signcolumn", OptionValue::Bool(true)),
1854 Err(EngineError::Ex(_))
1855 ));
1856 }
1857
1858 #[test]
1859 fn options_foldcolumn_roundtrip() {
1860 let mut o = Options::default();
1861 assert_eq!(o.foldcolumn, 0, "foldcolumn defaults to 0");
1862 o.set_by_name("fdc", OptionValue::Int(3)).unwrap();
1863 assert_eq!(o.foldcolumn, 3);
1864 assert_eq!(o.get_by_name("foldcolumn"), Some(OptionValue::Int(3)));
1865 }
1866
1867 #[test]
1868 fn options_foldcolumn_rejects_out_of_range() {
1869 let mut o = Options::default();
1870 assert!(matches!(
1871 o.set_by_name("foldcolumn", OptionValue::Int(13)),
1872 Err(EngineError::Ex(_))
1873 ));
1874 assert!(matches!(
1875 o.set_by_name("foldcolumn", OptionValue::Int(-1)),
1876 Err(EngineError::Ex(_))
1877 ));
1878 }
1879
1880 #[test]
1881 fn options_colorcolumn_roundtrip() {
1882 let mut o = Options::default();
1883 assert_eq!(o.colorcolumn, "", "colorcolumn defaults to empty string");
1884 o.set_by_name("cc", OptionValue::String("80,120".into()))
1885 .unwrap();
1886 assert_eq!(
1887 o.get_by_name("colorcolumn"),
1888 Some(OptionValue::String("80,120".into()))
1889 );
1890 o.set_by_name("colorcolumn", OptionValue::String(String::new()))
1891 .unwrap();
1892 assert_eq!(
1893 o.get_by_name("cc"),
1894 Some(OptionValue::String(String::new()))
1895 );
1896 }
1897
1898 #[test]
1899 fn options_cursorline_alias_cul() {
1900 let mut o = Options::default();
1901 o.set_by_name("cul", OptionValue::Bool(true)).unwrap();
1903 assert!(o.cursorline);
1904 o.set_by_name("cul", OptionValue::Bool(false)).unwrap();
1906 assert!(!o.cursorline);
1907 }
1908
1909 #[test]
1910 fn sign_column_mode_default_is_auto() {
1911 assert_eq!(SignColumnMode::default(), SignColumnMode::Auto);
1912 }
1913
1914 #[test]
1915 fn options_scrolloff_default_and_set() {
1916 let mut o = Options::default();
1917 assert_eq!(o.scrolloff, 5, "scrolloff defaults to 5");
1918 o.set_by_name("scrolloff", OptionValue::Int(0)).unwrap();
1919 assert_eq!(o.scrolloff, 0);
1920 o.set_by_name("scrolloff", OptionValue::Int(999)).unwrap();
1921 assert_eq!(o.scrolloff, 999);
1922 assert_eq!(o.get_by_name("scrolloff"), Some(OptionValue::Int(999)));
1923 }
1924
1925 #[test]
1926 fn options_sidescrolloff_default_and_set() {
1927 let mut o = Options::default();
1928 assert_eq!(o.sidescrolloff, 0, "sidescrolloff defaults to 0");
1929 o.set_by_name("sidescrolloff", OptionValue::Int(5)).unwrap();
1930 assert_eq!(o.sidescrolloff, 5);
1931 assert_eq!(o.get_by_name("sidescrolloff"), Some(OptionValue::Int(5)));
1932 }
1933
1934 #[test]
1935 fn options_alias_so_siso() {
1936 let mut o = Options::default();
1937 o.set_by_name("so", OptionValue::Int(3)).unwrap();
1939 assert_eq!(o.scrolloff, 3);
1940 assert_eq!(o.get_by_name("so"), Some(OptionValue::Int(3)));
1941 o.set_by_name("siso", OptionValue::Int(2)).unwrap();
1943 assert_eq!(o.sidescrolloff, 2);
1944 assert_eq!(o.get_by_name("siso"), Some(OptionValue::Int(2)));
1945 }
1946
1947 #[test]
1950 fn options_list_default_false_and_set() {
1951 let mut o = Options::default();
1952 assert!(!o.list, "list default is false");
1953 o.set_by_name("list", OptionValue::Bool(true)).unwrap();
1954 assert!(o.list);
1955 assert_eq!(o.get_by_name("list"), Some(OptionValue::Bool(true)));
1956 o.set_by_name("list", OptionValue::Bool(false)).unwrap();
1957 assert!(!o.list);
1958 }
1959
1960 #[test]
1961 fn options_listchars_default_matches_vim() {
1962 let o = Options::default();
1963 let lc = &o.listchars;
1964 assert_eq!(lc.tab_lead, '^');
1965 assert_eq!(lc.tab_fill, Some('I'));
1966 assert_eq!(lc.eol, Some('$'));
1967 assert_eq!(lc.space, None);
1968 assert_eq!(lc.trail, None);
1969 assert_eq!(lc.nbsp, None);
1970 }
1971
1972 #[test]
1973 fn options_listchars_set_and_get() {
1974 let mut o = Options::default();
1975 o.set_by_name("listchars", OptionValue::String("tab:>-,eol:$".to_string()))
1976 .unwrap();
1977 assert_eq!(o.listchars.tab_lead, '>');
1978 assert_eq!(o.listchars.tab_fill, Some('-'));
1979 assert_eq!(o.listchars.eol, Some('$'));
1980 }
1981
1982 #[test]
1983 fn options_lcs_alias_sets_listchars() {
1984 let mut o = Options::default();
1985 o.set_by_name("lcs", OptionValue::String("tab:>-,trail:~".to_string()))
1986 .unwrap();
1987 assert_eq!(o.listchars.tab_lead, '>');
1988 assert_eq!(o.listchars.trail, Some('~'));
1989 }
1990
1991 #[test]
1992 fn options_listchars_get_by_name_returns_string() {
1993 let o = Options::default();
1994 match o.get_by_name("listchars") {
1995 Some(OptionValue::String(s)) => {
1996 assert!(s.contains("tab:"), "canonical string should contain tab:");
1997 }
1998 other => panic!("expected String, got {other:?}"),
1999 }
2000 }
2001
2002 #[test]
2003 fn options_listchars_invalid_value_returns_err() {
2004 let mut o = Options::default();
2005 assert!(
2006 o.set_by_name("listchars", OptionValue::String("bogus:x".to_string()))
2007 .is_err()
2008 );
2009 }
2010
2011 #[test]
2014 fn indent_guides_default_true() {
2015 assert!(
2016 Options::default().indent_guides,
2017 "indent_guides must default to true"
2018 );
2019 }
2020
2021 #[test]
2022 fn options_indent_guides_set_and_get() {
2023 let mut opts = Options::default();
2024 opts.set_by_name("indent_guides", OptionValue::Bool(false))
2026 .unwrap();
2027 assert!(!opts.indent_guides);
2028 opts.set_by_name("ig", OptionValue::Bool(true)).unwrap();
2030 assert!(opts.indent_guides);
2031 assert_eq!(opts.get_by_name("ig"), Some(OptionValue::Bool(true)));
2033 assert_eq!(
2034 opts.get_by_name("indent_guides"),
2035 Some(OptionValue::Bool(true))
2036 );
2037 }
2038
2039 #[test]
2040 fn options_indent_guide_char_set_and_get() {
2041 let mut opts = Options::default();
2042 opts.set_by_name("indent_guide_char", OptionValue::String(":".to_string()))
2043 .unwrap();
2044 assert_eq!(opts.indent_guide_char, ':');
2045 opts.set_by_name("igc", OptionValue::String("┊".to_string()))
2047 .unwrap();
2048 assert_eq!(opts.indent_guide_char, '┊');
2049 assert_eq!(
2051 opts.get_by_name("igc"),
2052 Some(OptionValue::String("┊".to_string()))
2053 );
2054 assert_eq!(
2055 opts.get_by_name("indent_guide_char"),
2056 Some(OptionValue::String("┊".to_string()))
2057 );
2058 }
2059
2060 #[test]
2061 fn options_indent_guide_char_rejects_multi_char() {
2062 let mut opts = Options::default();
2063 assert!(
2064 opts.set_by_name("indent_guide_char", OptionValue::String("ab".to_string()))
2065 .is_err(),
2066 "multi-char value must be rejected"
2067 );
2068 }
2069
2070 #[test]
2071 fn options_indent_guide_char_rejects_empty() {
2072 let mut opts = Options::default();
2073 assert!(
2074 opts.set_by_name("indent_guide_char", OptionValue::String(String::new()))
2075 .is_err(),
2076 "empty string must be rejected"
2077 );
2078 }
2079
2080 #[test]
2083 fn colorizer_default_true() {
2084 assert!(
2085 Options::default().colorizer,
2086 "colorizer must default to true"
2087 );
2088 }
2089
2090 #[test]
2091 fn colorizer_filetypes_includes_css() {
2092 let o = Options::default();
2093 assert!(
2094 o.colorizer_filetypes.iter().any(|f| f == "css"),
2095 "default colorizer_filetypes must include 'css'"
2096 );
2097 }
2098
2099 #[test]
2100 fn options_colorizer_set_and_get() {
2101 let mut o = Options::default();
2102 o.set_by_name("colorizer", OptionValue::Bool(false))
2103 .unwrap();
2104 assert_eq!(o.get_by_name("colorizer"), Some(OptionValue::Bool(false)));
2105 o.set_by_name("clz", OptionValue::Bool(true)).unwrap();
2106 assert_eq!(o.get_by_name("clz"), Some(OptionValue::Bool(true)));
2107 }
2108
2109 #[test]
2110 fn options_colorizer_filetypes_set_and_get() {
2111 let mut o = Options::default();
2112 o.set_by_name(
2113 "colorizer_filetypes",
2114 OptionValue::String("css,scss,toml".into()),
2115 )
2116 .unwrap();
2117 assert_eq!(o.colorizer_filetypes, vec!["css", "scss", "toml"]);
2118 assert_eq!(
2119 o.get_by_name("clzft"),
2120 Some(OptionValue::String("css,scss,toml".into()))
2121 );
2122 }
2123
2124 #[test]
2127 fn format_on_save_default_true() {
2128 let o = Options::default();
2129 assert!(o.format_on_save, "format_on_save must default to true");
2130 }
2131
2132 #[test]
2133 fn trim_trailing_whitespace_default_false() {
2134 let o = Options::default();
2135 assert!(
2136 !o.trim_trailing_whitespace,
2137 "trim_trailing_whitespace must default to false"
2138 );
2139 }
2140
2141 #[test]
2142 fn options_fos_alias_sets_format_on_save() {
2143 let mut o = Options::default();
2144 o.set_by_name("fos", OptionValue::Bool(true)).unwrap();
2145 assert!(o.format_on_save, "fos alias must set format_on_save");
2146 assert_eq!(
2147 o.get_by_name("fos"),
2148 Some(OptionValue::Bool(true)),
2149 "get_by_name(fos) must reflect the new value"
2150 );
2151 assert_eq!(
2152 o.get_by_name("format_on_save"),
2153 Some(OptionValue::Bool(true)),
2154 "get_by_name(format_on_save) must also reflect the new value"
2155 );
2156 }
2157
2158 #[test]
2159 fn options_tts_alias_sets_trim_trailing_whitespace() {
2160 let mut o = Options::default();
2161 o.set_by_name("tts", OptionValue::Bool(true)).unwrap();
2162 assert!(
2163 o.trim_trailing_whitespace,
2164 "tts alias must set trim_trailing_whitespace"
2165 );
2166 assert_eq!(
2167 o.get_by_name("tts"),
2168 Some(OptionValue::Bool(true)),
2169 "get_by_name(tts) must reflect the new value"
2170 );
2171 assert_eq!(
2172 o.get_by_name("trim_trailing_whitespace"),
2173 Some(OptionValue::Bool(true)),
2174 "get_by_name(trim_trailing_whitespace) must also reflect the new value"
2175 );
2176 }
2177
2178 #[test]
2181 fn rainbow_brackets_default_true() {
2182 let o = Options::default();
2183 assert!(o.rainbow_brackets, "rainbow_brackets must default to true");
2184 }
2185
2186 #[test]
2187 fn options_rb_alias_sets_rainbow_brackets() {
2188 let mut o = Options::default();
2189 o.set_by_name("rb", OptionValue::Bool(false)).unwrap();
2190 assert!(
2191 !o.rainbow_brackets,
2192 "rb alias must set rainbow_brackets to false"
2193 );
2194 assert_eq!(
2195 o.get_by_name("rb"),
2196 Some(OptionValue::Bool(false)),
2197 "get_by_name(rb) must reflect the new value"
2198 );
2199 assert_eq!(
2200 o.get_by_name("rainbow_brackets"),
2201 Some(OptionValue::Bool(false)),
2202 "get_by_name(rainbow_brackets) must also reflect the new value"
2203 );
2204 }
2205
2206 #[test]
2207 fn autoreload_default_true() {
2208 assert!(
2209 Options::default().autoreload,
2210 "autoreload must default true"
2211 );
2212 }
2213
2214 #[test]
2215 fn options_ar_alias_sets_autoreload() {
2216 let mut o = Options::default();
2217 o.set_by_name("ar", OptionValue::Bool(false)).unwrap();
2218 assert!(!o.autoreload, "ar alias must set autoreload");
2219 assert_eq!(o.get_by_name("autoreload"), Some(OptionValue::Bool(false)));
2220 }
2221
2222 #[test]
2225 fn updatetime_default_4000() {
2226 let o = Options::default();
2227 assert_eq!(o.updatetime, 4000, "updatetime must default to 4000 ms");
2228 assert_eq!(
2229 o.get_by_name("updatetime"),
2230 Some(OptionValue::Int(4000)),
2231 "get_by_name(updatetime) must return Int(4000)"
2232 );
2233 }
2234
2235 #[test]
2236 fn options_ut_alias_sets_updatetime() {
2237 let mut o = Options::default();
2238 o.set_by_name("ut", OptionValue::Int(1000)).unwrap();
2239 assert_eq!(o.updatetime, 1000, "ut alias must set updatetime");
2240 assert_eq!(
2241 o.get_by_name("ut"),
2242 Some(OptionValue::Int(1000)),
2243 "get_by_name(ut) must reflect the new value"
2244 );
2245 assert_eq!(
2246 o.get_by_name("updatetime"),
2247 Some(OptionValue::Int(1000)),
2248 "get_by_name(updatetime) must also reflect the new value"
2249 );
2250 }
2251
2252 #[test]
2255 fn matchparen_default_true() {
2256 let o = Options::default();
2257 assert!(o.matchparen, "matchparen must default to true");
2258 assert_eq!(
2259 o.get_by_name("matchparen"),
2260 Some(OptionValue::Bool(true)),
2261 "get_by_name(matchparen) must return Bool(true)"
2262 );
2263 }
2264
2265 #[test]
2266 fn options_matchparen_set_and_get() {
2267 let mut o = Options::default();
2268 o.set_by_name("matchparen", OptionValue::Bool(false))
2269 .unwrap();
2270 assert!(!o.matchparen, "matchparen must be false after set");
2271 assert_eq!(
2272 o.get_by_name("matchparen"),
2273 Some(OptionValue::Bool(false)),
2274 "get_by_name(matchparen) must reflect false"
2275 );
2276 o.set_by_name("mps", OptionValue::Bool(true)).unwrap();
2278 assert!(o.matchparen, "mps alias must set matchparen to true");
2279 assert_eq!(
2280 o.get_by_name("mps"),
2281 Some(OptionValue::Bool(true)),
2282 "get_by_name(mps) must reflect true"
2283 );
2284 }
2285
2286 #[test]
2289 fn foldmethod_default_expr() {
2290 let o = Options::default();
2291 assert_eq!(
2292 o.foldmethod,
2293 FoldMethod::Expr,
2294 "foldmethod must default to Expr (tree-sitter)"
2295 );
2296 assert_eq!(
2297 o.get_by_name("foldmethod"),
2298 Some(OptionValue::String("expr".into())),
2299 "get_by_name(foldmethod) must return \"expr\""
2300 );
2301 }
2302
2303 #[test]
2304 fn foldmethod_fdm_alias_roundtrip() {
2305 let mut o = Options::default();
2306 o.set_by_name("fdm", OptionValue::String("manual".into()))
2307 .unwrap();
2308 assert_eq!(o.foldmethod, FoldMethod::Manual);
2309 assert_eq!(
2310 o.get_by_name("fdm"),
2311 Some(OptionValue::String("manual".into()))
2312 );
2313 o.set_by_name("foldmethod", OptionValue::String("expr".into()))
2314 .unwrap();
2315 assert_eq!(o.foldmethod, FoldMethod::Expr);
2316 o.set_by_name("foldmethod", OptionValue::String("marker".into()))
2317 .unwrap();
2318 assert_eq!(o.foldmethod, FoldMethod::Marker);
2319 o.set_by_name("foldmethod", OptionValue::String("syntax".into()))
2321 .unwrap();
2322 assert_eq!(o.foldmethod, FoldMethod::Expr);
2323 }
2324
2325 #[test]
2326 fn foldmethod_rejects_invalid_value() {
2327 let mut o = Options::default();
2328 let err = o
2329 .set_by_name("foldmethod", OptionValue::String("bogus".into()))
2330 .unwrap_err();
2331 assert!(
2332 err.to_string().contains("must be"),
2333 "expected error about valid values, got: {err}"
2334 );
2335 }
2336
2337 #[test]
2338 fn foldenable_default_true() {
2339 let o = Options::default();
2340 assert!(o.foldenable, "foldenable must default to true");
2341 assert_eq!(
2342 o.get_by_name("foldenable"),
2343 Some(OptionValue::Bool(true)),
2344 "get_by_name(foldenable) must return Bool(true)"
2345 );
2346 }
2347
2348 #[test]
2349 fn foldenable_fen_alias_roundtrip() {
2350 let mut o = Options::default();
2351 o.set_by_name("fen", OptionValue::Bool(false)).unwrap();
2352 assert!(!o.foldenable, "fen alias must disable foldenable");
2353 assert_eq!(o.get_by_name("fen"), Some(OptionValue::Bool(false)));
2354 o.set_by_name("foldenable", OptionValue::Bool(true))
2355 .unwrap();
2356 assert!(o.foldenable);
2357 }
2358
2359 #[test]
2360 fn foldlevelstart_default_99() {
2361 let o = Options::default();
2362 assert_eq!(o.foldlevelstart, 99, "foldlevelstart must default to 99");
2363 assert_eq!(
2364 o.get_by_name("foldlevelstart"),
2365 Some(OptionValue::Int(99)),
2366 "get_by_name(foldlevelstart) must return Int(99)"
2367 );
2368 }
2369
2370 #[test]
2371 fn foldlevelstart_fls_alias_roundtrip() {
2372 let mut o = Options::default();
2373 o.set_by_name("fls", OptionValue::Int(0)).unwrap();
2374 assert_eq!(
2375 o.foldlevelstart, 0,
2376 "fls alias must set foldlevelstart to 0"
2377 );
2378 assert_eq!(o.get_by_name("fls"), Some(OptionValue::Int(0)));
2379 o.set_by_name("foldlevelstart", OptionValue::Int(5))
2380 .unwrap();
2381 assert_eq!(o.foldlevelstart, 5);
2382 }
2383}