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 Buffer: 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#[cfg(test)]
1573mod tests {
1574 use super::*;
1575
1576 #[test]
1577 fn caret_is_empty() {
1578 let sel = Selection::caret(Pos::new(2, 4));
1579 assert!(sel.is_empty());
1580 assert_eq!(sel.anchor, sel.head);
1581 }
1582
1583 #[test]
1584 fn selection_set_default_has_one_caret() {
1585 let set = SelectionSet::default();
1586 assert_eq!(set.items.len(), 1);
1587 assert_eq!(set.primary, 0);
1588 assert_eq!(set.primary().anchor, Pos::ORIGIN);
1589 }
1590
1591 #[test]
1592 fn edit_constructors() {
1593 let p = Pos::new(0, 5);
1594 assert_eq!(Edit::insert(p, "x").range, p..p);
1595 assert!(Edit::insert(p, "x").replacement == "x");
1596 assert!(Edit::delete(p..p).replacement.is_empty());
1597 }
1598
1599 #[test]
1600 fn attrs_flags() {
1601 let a = Attrs::BOLD | Attrs::UNDERLINE;
1602 assert!(a.contains(Attrs::BOLD));
1603 assert!(!a.contains(Attrs::ITALIC));
1604 }
1605
1606 #[test]
1607 fn options_set_get_roundtrip() {
1608 let mut o = Options::default();
1609 o.set_by_name("tabstop", OptionValue::Int(4)).unwrap();
1610 assert!(matches!(o.get_by_name("ts"), Some(OptionValue::Int(4))));
1611 o.set_by_name("expandtab", OptionValue::Bool(true)).unwrap();
1612 assert!(matches!(o.get_by_name("et"), Some(OptionValue::Bool(true))));
1613 o.set_by_name("iskeyword", OptionValue::String("a-z".into()))
1614 .unwrap();
1615 match o.get_by_name("iskeyword") {
1616 Some(OptionValue::String(s)) => assert_eq!(s, "a-z"),
1617 other => panic!("expected String, got {other:?}"),
1618 }
1619 }
1620
1621 #[test]
1622 fn options_unknown_name_errors_on_set() {
1623 let mut o = Options::default();
1624 assert!(matches!(
1625 o.set_by_name("frobnicate", OptionValue::Int(1)),
1626 Err(EngineError::Ex(_))
1627 ));
1628 assert!(o.get_by_name("frobnicate").is_none());
1629 }
1630
1631 #[test]
1632 fn options_type_mismatch_errors() {
1633 let mut o = Options::default();
1634 assert!(matches!(
1635 o.set_by_name("tabstop", OptionValue::String("nope".into())),
1636 Err(EngineError::Ex(_))
1637 ));
1638 assert!(matches!(
1639 o.set_by_name("iskeyword", OptionValue::Int(7)),
1640 Err(EngineError::Ex(_))
1641 ));
1642 }
1643
1644 #[test]
1647 fn default_options_ignorecase_and_smartcase_are_true() {
1648 let o = Options::default();
1649 assert!(o.ignorecase, "ignorecase must default to true");
1650 assert!(o.smartcase, "smartcase must default to true");
1651 }
1652
1653 #[test]
1654 fn options_int_to_bool_coercion() {
1655 let mut o = Options::default();
1658 o.set_by_name("ignorecase", OptionValue::Int(1)).unwrap();
1659 assert!(matches!(o.get_by_name("ic"), Some(OptionValue::Bool(true))));
1660 o.set_by_name("ignorecase", OptionValue::Int(0)).unwrap();
1661 assert!(matches!(
1662 o.get_by_name("ic"),
1663 Some(OptionValue::Bool(false))
1664 ));
1665 }
1666
1667 #[test]
1668 fn options_wrap_linebreak_roundtrip() {
1669 let mut o = Options::default();
1670 assert_eq!(o.wrap, WrapMode::None);
1671 o.set_by_name("wrap", OptionValue::Bool(true)).unwrap();
1672 assert_eq!(o.wrap, WrapMode::Char);
1673 o.set_by_name("linebreak", OptionValue::Bool(true)).unwrap();
1674 assert_eq!(o.wrap, WrapMode::Word);
1675 assert!(matches!(
1676 o.get_by_name("wrap"),
1677 Some(OptionValue::Bool(true))
1678 ));
1679 assert!(matches!(
1680 o.get_by_name("lbr"),
1681 Some(OptionValue::Bool(true))
1682 ));
1683 o.set_by_name("linebreak", OptionValue::Bool(false))
1684 .unwrap();
1685 assert_eq!(o.wrap, WrapMode::Char);
1686 o.set_by_name("wrap", OptionValue::Bool(false)).unwrap();
1687 assert_eq!(o.wrap, WrapMode::None);
1688 }
1689
1690 #[test]
1691 fn options_default_modern() {
1692 let o = Options::default();
1695 assert_eq!(o.tabstop, 4);
1696 assert_eq!(o.shiftwidth, 4);
1697 assert_eq!(o.softtabstop, 4);
1698 assert!(o.expandtab);
1699 assert!(o.hlsearch);
1700 assert!(o.wrapscan);
1701 assert!(o.smartindent);
1702 assert_eq!(o.timeout_len, core::time::Duration::from_millis(1000));
1703 }
1704
1705 #[test]
1706 fn editor_snapshot_version_const() {
1707 assert_eq!(EditorSnapshot::VERSION, 5);
1708 }
1709
1710 #[test]
1711 fn editor_snapshot_default_shape() {
1712 let s = EditorSnapshot {
1713 version: EditorSnapshot::VERSION,
1714 mode: SnapshotMode::Normal,
1715 cursor: (0, 0),
1716 lines: vec!["hello".to_string()],
1717 viewport_top: 0,
1718 registers: crate::Registers::default(),
1719 marks: Default::default(),
1720 global_marks: Default::default(),
1721 };
1722 assert_eq!(s.cursor, (0, 0));
1723 assert_eq!(s.lines.len(), 1);
1724 }
1725
1726 #[cfg(feature = "serde")]
1727 #[test]
1728 fn editor_snapshot_roundtrip() {
1729 let mut marks = std::collections::BTreeMap::new();
1730 marks.insert('a', (1u32, 0u32));
1731 let mut global_marks = std::collections::BTreeMap::new();
1732 global_marks.insert('A', (42u64, 5u32, 2u32));
1733 let s = EditorSnapshot {
1734 version: EditorSnapshot::VERSION,
1735 mode: SnapshotMode::Insert,
1736 cursor: (3, 7),
1737 lines: vec!["alpha".into(), "beta".into()],
1738 viewport_top: 2,
1739 registers: crate::Registers::default(),
1740 marks,
1741 global_marks,
1742 };
1743 let json = serde_json::to_string(&s).unwrap();
1744 let back: EditorSnapshot = serde_json::from_str(&json).unwrap();
1745 assert_eq!(s.cursor, back.cursor);
1746 assert_eq!(s.lines, back.lines);
1747 assert_eq!(s.viewport_top, back.viewport_top);
1748 assert_eq!(s.global_marks, back.global_marks);
1749 }
1750
1751 #[test]
1752 fn engine_error_display() {
1753 let e = EngineError::ReadOnly;
1754 assert_eq!(e.to_string(), "buffer is read-only");
1755 let e = EngineError::OutOfBounds(Pos::new(3, 7));
1756 assert!(e.to_string().contains("out of bounds"));
1757 }
1758
1759 #[test]
1762 fn options_cursorline_roundtrip() {
1763 let mut o = Options::default();
1764 assert!(o.cursorline, "cursorline defaults to true");
1765 o.set_by_name("cursorline", OptionValue::Bool(false))
1766 .unwrap();
1767 assert!(matches!(
1768 o.get_by_name("cul"),
1769 Some(OptionValue::Bool(false))
1770 ));
1771 o.set_by_name("cul", OptionValue::Bool(true)).unwrap();
1772 assert!(matches!(
1773 o.get_by_name("cursorline"),
1774 Some(OptionValue::Bool(true))
1775 ));
1776 }
1777
1778 #[test]
1779 fn options_cursorcolumn_roundtrip() {
1780 let mut o = Options::default();
1781 assert!(!o.cursorcolumn, "cursorcolumn defaults to false");
1782 o.set_by_name("cuc", OptionValue::Bool(true)).unwrap();
1783 assert!(matches!(
1784 o.get_by_name("cursorcolumn"),
1785 Some(OptionValue::Bool(true))
1786 ));
1787 }
1788
1789 #[test]
1790 fn options_signcolumn_roundtrip() {
1791 let mut o = Options::default();
1792 assert_eq!(
1793 o.signcolumn,
1794 SignColumnMode::Auto,
1795 "signcolumn defaults to auto"
1796 );
1797 o.set_by_name("signcolumn", OptionValue::String("yes".into()))
1798 .unwrap();
1799 assert_eq!(o.signcolumn, SignColumnMode::Yes);
1800 assert_eq!(
1801 o.get_by_name("scl"),
1802 Some(OptionValue::String("yes".into()))
1803 );
1804 o.set_by_name("scl", OptionValue::String("no".into()))
1805 .unwrap();
1806 assert_eq!(o.signcolumn, SignColumnMode::No);
1807 o.set_by_name("scl", OptionValue::String("auto".into()))
1808 .unwrap();
1809 assert_eq!(o.signcolumn, SignColumnMode::Auto);
1810 }
1811
1812 #[test]
1813 fn options_signcolumn_rejects_invalid() {
1814 let mut o = Options::default();
1815 assert!(matches!(
1816 o.set_by_name("signcolumn", OptionValue::String("maybe".into())),
1817 Err(EngineError::Ex(_))
1818 ));
1819 assert!(matches!(
1821 o.set_by_name("signcolumn", OptionValue::Bool(true)),
1822 Err(EngineError::Ex(_))
1823 ));
1824 }
1825
1826 #[test]
1827 fn options_foldcolumn_roundtrip() {
1828 let mut o = Options::default();
1829 assert_eq!(o.foldcolumn, 0, "foldcolumn defaults to 0");
1830 o.set_by_name("fdc", OptionValue::Int(3)).unwrap();
1831 assert_eq!(o.foldcolumn, 3);
1832 assert_eq!(o.get_by_name("foldcolumn"), Some(OptionValue::Int(3)));
1833 }
1834
1835 #[test]
1836 fn options_foldcolumn_rejects_out_of_range() {
1837 let mut o = Options::default();
1838 assert!(matches!(
1839 o.set_by_name("foldcolumn", OptionValue::Int(13)),
1840 Err(EngineError::Ex(_))
1841 ));
1842 assert!(matches!(
1843 o.set_by_name("foldcolumn", OptionValue::Int(-1)),
1844 Err(EngineError::Ex(_))
1845 ));
1846 }
1847
1848 #[test]
1849 fn options_colorcolumn_roundtrip() {
1850 let mut o = Options::default();
1851 assert_eq!(o.colorcolumn, "", "colorcolumn defaults to empty string");
1852 o.set_by_name("cc", OptionValue::String("80,120".into()))
1853 .unwrap();
1854 assert_eq!(
1855 o.get_by_name("colorcolumn"),
1856 Some(OptionValue::String("80,120".into()))
1857 );
1858 o.set_by_name("colorcolumn", OptionValue::String(String::new()))
1859 .unwrap();
1860 assert_eq!(
1861 o.get_by_name("cc"),
1862 Some(OptionValue::String(String::new()))
1863 );
1864 }
1865
1866 #[test]
1867 fn options_cursorline_alias_cul() {
1868 let mut o = Options::default();
1869 o.set_by_name("cul", OptionValue::Bool(true)).unwrap();
1871 assert!(o.cursorline);
1872 o.set_by_name("cul", OptionValue::Bool(false)).unwrap();
1874 assert!(!o.cursorline);
1875 }
1876
1877 #[test]
1878 fn sign_column_mode_default_is_auto() {
1879 assert_eq!(SignColumnMode::default(), SignColumnMode::Auto);
1880 }
1881
1882 #[test]
1883 fn options_scrolloff_default_and_set() {
1884 let mut o = Options::default();
1885 assert_eq!(o.scrolloff, 5, "scrolloff defaults to 5");
1886 o.set_by_name("scrolloff", OptionValue::Int(0)).unwrap();
1887 assert_eq!(o.scrolloff, 0);
1888 o.set_by_name("scrolloff", OptionValue::Int(999)).unwrap();
1889 assert_eq!(o.scrolloff, 999);
1890 assert_eq!(o.get_by_name("scrolloff"), Some(OptionValue::Int(999)));
1891 }
1892
1893 #[test]
1894 fn options_sidescrolloff_default_and_set() {
1895 let mut o = Options::default();
1896 assert_eq!(o.sidescrolloff, 0, "sidescrolloff defaults to 0");
1897 o.set_by_name("sidescrolloff", OptionValue::Int(5)).unwrap();
1898 assert_eq!(o.sidescrolloff, 5);
1899 assert_eq!(o.get_by_name("sidescrolloff"), Some(OptionValue::Int(5)));
1900 }
1901
1902 #[test]
1903 fn options_alias_so_siso() {
1904 let mut o = Options::default();
1905 o.set_by_name("so", OptionValue::Int(3)).unwrap();
1907 assert_eq!(o.scrolloff, 3);
1908 assert_eq!(o.get_by_name("so"), Some(OptionValue::Int(3)));
1909 o.set_by_name("siso", OptionValue::Int(2)).unwrap();
1911 assert_eq!(o.sidescrolloff, 2);
1912 assert_eq!(o.get_by_name("siso"), Some(OptionValue::Int(2)));
1913 }
1914
1915 #[test]
1918 fn options_list_default_false_and_set() {
1919 let mut o = Options::default();
1920 assert!(!o.list, "list default is false");
1921 o.set_by_name("list", OptionValue::Bool(true)).unwrap();
1922 assert!(o.list);
1923 assert_eq!(o.get_by_name("list"), Some(OptionValue::Bool(true)));
1924 o.set_by_name("list", OptionValue::Bool(false)).unwrap();
1925 assert!(!o.list);
1926 }
1927
1928 #[test]
1929 fn options_listchars_default_matches_vim() {
1930 let o = Options::default();
1931 let lc = &o.listchars;
1932 assert_eq!(lc.tab_lead, '^');
1933 assert_eq!(lc.tab_fill, Some('I'));
1934 assert_eq!(lc.eol, Some('$'));
1935 assert_eq!(lc.space, None);
1936 assert_eq!(lc.trail, None);
1937 assert_eq!(lc.nbsp, None);
1938 }
1939
1940 #[test]
1941 fn options_listchars_set_and_get() {
1942 let mut o = Options::default();
1943 o.set_by_name("listchars", OptionValue::String("tab:>-,eol:$".to_string()))
1944 .unwrap();
1945 assert_eq!(o.listchars.tab_lead, '>');
1946 assert_eq!(o.listchars.tab_fill, Some('-'));
1947 assert_eq!(o.listchars.eol, Some('$'));
1948 }
1949
1950 #[test]
1951 fn options_lcs_alias_sets_listchars() {
1952 let mut o = Options::default();
1953 o.set_by_name("lcs", OptionValue::String("tab:>-,trail:~".to_string()))
1954 .unwrap();
1955 assert_eq!(o.listchars.tab_lead, '>');
1956 assert_eq!(o.listchars.trail, Some('~'));
1957 }
1958
1959 #[test]
1960 fn options_listchars_get_by_name_returns_string() {
1961 let o = Options::default();
1962 match o.get_by_name("listchars") {
1963 Some(OptionValue::String(s)) => {
1964 assert!(s.contains("tab:"), "canonical string should contain tab:");
1965 }
1966 other => panic!("expected String, got {other:?}"),
1967 }
1968 }
1969
1970 #[test]
1971 fn options_listchars_invalid_value_returns_err() {
1972 let mut o = Options::default();
1973 assert!(
1974 o.set_by_name("listchars", OptionValue::String("bogus:x".to_string()))
1975 .is_err()
1976 );
1977 }
1978
1979 #[test]
1982 fn indent_guides_default_true() {
1983 assert!(
1984 Options::default().indent_guides,
1985 "indent_guides must default to true"
1986 );
1987 }
1988
1989 #[test]
1990 fn options_indent_guides_set_and_get() {
1991 let mut opts = Options::default();
1992 opts.set_by_name("indent_guides", OptionValue::Bool(false))
1994 .unwrap();
1995 assert!(!opts.indent_guides);
1996 opts.set_by_name("ig", OptionValue::Bool(true)).unwrap();
1998 assert!(opts.indent_guides);
1999 assert_eq!(opts.get_by_name("ig"), Some(OptionValue::Bool(true)));
2001 assert_eq!(
2002 opts.get_by_name("indent_guides"),
2003 Some(OptionValue::Bool(true))
2004 );
2005 }
2006
2007 #[test]
2008 fn options_indent_guide_char_set_and_get() {
2009 let mut opts = Options::default();
2010 opts.set_by_name("indent_guide_char", OptionValue::String(":".to_string()))
2011 .unwrap();
2012 assert_eq!(opts.indent_guide_char, ':');
2013 opts.set_by_name("igc", OptionValue::String("┊".to_string()))
2015 .unwrap();
2016 assert_eq!(opts.indent_guide_char, '┊');
2017 assert_eq!(
2019 opts.get_by_name("igc"),
2020 Some(OptionValue::String("┊".to_string()))
2021 );
2022 assert_eq!(
2023 opts.get_by_name("indent_guide_char"),
2024 Some(OptionValue::String("┊".to_string()))
2025 );
2026 }
2027
2028 #[test]
2029 fn options_indent_guide_char_rejects_multi_char() {
2030 let mut opts = Options::default();
2031 assert!(
2032 opts.set_by_name("indent_guide_char", OptionValue::String("ab".to_string()))
2033 .is_err(),
2034 "multi-char value must be rejected"
2035 );
2036 }
2037
2038 #[test]
2039 fn options_indent_guide_char_rejects_empty() {
2040 let mut opts = Options::default();
2041 assert!(
2042 opts.set_by_name("indent_guide_char", OptionValue::String(String::new()))
2043 .is_err(),
2044 "empty string must be rejected"
2045 );
2046 }
2047
2048 #[test]
2051 fn colorizer_default_true() {
2052 assert!(
2053 Options::default().colorizer,
2054 "colorizer must default to true"
2055 );
2056 }
2057
2058 #[test]
2059 fn colorizer_filetypes_includes_css() {
2060 let o = Options::default();
2061 assert!(
2062 o.colorizer_filetypes.iter().any(|f| f == "css"),
2063 "default colorizer_filetypes must include 'css'"
2064 );
2065 }
2066
2067 #[test]
2068 fn options_colorizer_set_and_get() {
2069 let mut o = Options::default();
2070 o.set_by_name("colorizer", OptionValue::Bool(false))
2071 .unwrap();
2072 assert_eq!(o.get_by_name("colorizer"), Some(OptionValue::Bool(false)));
2073 o.set_by_name("clz", OptionValue::Bool(true)).unwrap();
2074 assert_eq!(o.get_by_name("clz"), Some(OptionValue::Bool(true)));
2075 }
2076
2077 #[test]
2078 fn options_colorizer_filetypes_set_and_get() {
2079 let mut o = Options::default();
2080 o.set_by_name(
2081 "colorizer_filetypes",
2082 OptionValue::String("css,scss,toml".into()),
2083 )
2084 .unwrap();
2085 assert_eq!(o.colorizer_filetypes, vec!["css", "scss", "toml"]);
2086 assert_eq!(
2087 o.get_by_name("clzft"),
2088 Some(OptionValue::String("css,scss,toml".into()))
2089 );
2090 }
2091
2092 #[test]
2095 fn format_on_save_default_true() {
2096 let o = Options::default();
2097 assert!(o.format_on_save, "format_on_save must default to true");
2098 }
2099
2100 #[test]
2101 fn trim_trailing_whitespace_default_false() {
2102 let o = Options::default();
2103 assert!(
2104 !o.trim_trailing_whitespace,
2105 "trim_trailing_whitespace must default to false"
2106 );
2107 }
2108
2109 #[test]
2110 fn options_fos_alias_sets_format_on_save() {
2111 let mut o = Options::default();
2112 o.set_by_name("fos", OptionValue::Bool(true)).unwrap();
2113 assert!(o.format_on_save, "fos alias must set format_on_save");
2114 assert_eq!(
2115 o.get_by_name("fos"),
2116 Some(OptionValue::Bool(true)),
2117 "get_by_name(fos) must reflect the new value"
2118 );
2119 assert_eq!(
2120 o.get_by_name("format_on_save"),
2121 Some(OptionValue::Bool(true)),
2122 "get_by_name(format_on_save) must also reflect the new value"
2123 );
2124 }
2125
2126 #[test]
2127 fn options_tts_alias_sets_trim_trailing_whitespace() {
2128 let mut o = Options::default();
2129 o.set_by_name("tts", OptionValue::Bool(true)).unwrap();
2130 assert!(
2131 o.trim_trailing_whitespace,
2132 "tts alias must set trim_trailing_whitespace"
2133 );
2134 assert_eq!(
2135 o.get_by_name("tts"),
2136 Some(OptionValue::Bool(true)),
2137 "get_by_name(tts) must reflect the new value"
2138 );
2139 assert_eq!(
2140 o.get_by_name("trim_trailing_whitespace"),
2141 Some(OptionValue::Bool(true)),
2142 "get_by_name(trim_trailing_whitespace) must also reflect the new value"
2143 );
2144 }
2145
2146 #[test]
2149 fn rainbow_brackets_default_true() {
2150 let o = Options::default();
2151 assert!(o.rainbow_brackets, "rainbow_brackets must default to true");
2152 }
2153
2154 #[test]
2155 fn options_rb_alias_sets_rainbow_brackets() {
2156 let mut o = Options::default();
2157 o.set_by_name("rb", OptionValue::Bool(false)).unwrap();
2158 assert!(
2159 !o.rainbow_brackets,
2160 "rb alias must set rainbow_brackets to false"
2161 );
2162 assert_eq!(
2163 o.get_by_name("rb"),
2164 Some(OptionValue::Bool(false)),
2165 "get_by_name(rb) must reflect the new value"
2166 );
2167 assert_eq!(
2168 o.get_by_name("rainbow_brackets"),
2169 Some(OptionValue::Bool(false)),
2170 "get_by_name(rainbow_brackets) must also reflect the new value"
2171 );
2172 }
2173
2174 #[test]
2175 fn autoreload_default_true() {
2176 assert!(
2177 Options::default().autoreload,
2178 "autoreload must default true"
2179 );
2180 }
2181
2182 #[test]
2183 fn options_ar_alias_sets_autoreload() {
2184 let mut o = Options::default();
2185 o.set_by_name("ar", OptionValue::Bool(false)).unwrap();
2186 assert!(!o.autoreload, "ar alias must set autoreload");
2187 assert_eq!(o.get_by_name("autoreload"), Some(OptionValue::Bool(false)));
2188 }
2189
2190 #[test]
2193 fn updatetime_default_4000() {
2194 let o = Options::default();
2195 assert_eq!(o.updatetime, 4000, "updatetime must default to 4000 ms");
2196 assert_eq!(
2197 o.get_by_name("updatetime"),
2198 Some(OptionValue::Int(4000)),
2199 "get_by_name(updatetime) must return Int(4000)"
2200 );
2201 }
2202
2203 #[test]
2204 fn options_ut_alias_sets_updatetime() {
2205 let mut o = Options::default();
2206 o.set_by_name("ut", OptionValue::Int(1000)).unwrap();
2207 assert_eq!(o.updatetime, 1000, "ut alias must set updatetime");
2208 assert_eq!(
2209 o.get_by_name("ut"),
2210 Some(OptionValue::Int(1000)),
2211 "get_by_name(ut) must reflect the new value"
2212 );
2213 assert_eq!(
2214 o.get_by_name("updatetime"),
2215 Some(OptionValue::Int(1000)),
2216 "get_by_name(updatetime) must also reflect the new value"
2217 );
2218 }
2219
2220 #[test]
2223 fn matchparen_default_true() {
2224 let o = Options::default();
2225 assert!(o.matchparen, "matchparen must default to true");
2226 assert_eq!(
2227 o.get_by_name("matchparen"),
2228 Some(OptionValue::Bool(true)),
2229 "get_by_name(matchparen) must return Bool(true)"
2230 );
2231 }
2232
2233 #[test]
2234 fn options_matchparen_set_and_get() {
2235 let mut o = Options::default();
2236 o.set_by_name("matchparen", OptionValue::Bool(false))
2237 .unwrap();
2238 assert!(!o.matchparen, "matchparen must be false after set");
2239 assert_eq!(
2240 o.get_by_name("matchparen"),
2241 Some(OptionValue::Bool(false)),
2242 "get_by_name(matchparen) must reflect false"
2243 );
2244 o.set_by_name("mps", OptionValue::Bool(true)).unwrap();
2246 assert!(o.matchparen, "mps alias must set matchparen to true");
2247 assert_eq!(
2248 o.get_by_name("mps"),
2249 Some(OptionValue::Bool(true)),
2250 "get_by_name(mps) must reflect true"
2251 );
2252 }
2253
2254 #[test]
2257 fn foldmethod_default_expr() {
2258 let o = Options::default();
2259 assert_eq!(
2260 o.foldmethod,
2261 FoldMethod::Expr,
2262 "foldmethod must default to Expr (tree-sitter)"
2263 );
2264 assert_eq!(
2265 o.get_by_name("foldmethod"),
2266 Some(OptionValue::String("expr".into())),
2267 "get_by_name(foldmethod) must return \"expr\""
2268 );
2269 }
2270
2271 #[test]
2272 fn foldmethod_fdm_alias_roundtrip() {
2273 let mut o = Options::default();
2274 o.set_by_name("fdm", OptionValue::String("manual".into()))
2275 .unwrap();
2276 assert_eq!(o.foldmethod, FoldMethod::Manual);
2277 assert_eq!(
2278 o.get_by_name("fdm"),
2279 Some(OptionValue::String("manual".into()))
2280 );
2281 o.set_by_name("foldmethod", OptionValue::String("expr".into()))
2282 .unwrap();
2283 assert_eq!(o.foldmethod, FoldMethod::Expr);
2284 o.set_by_name("foldmethod", OptionValue::String("marker".into()))
2285 .unwrap();
2286 assert_eq!(o.foldmethod, FoldMethod::Marker);
2287 o.set_by_name("foldmethod", OptionValue::String("syntax".into()))
2289 .unwrap();
2290 assert_eq!(o.foldmethod, FoldMethod::Expr);
2291 }
2292
2293 #[test]
2294 fn foldmethod_rejects_invalid_value() {
2295 let mut o = Options::default();
2296 let err = o
2297 .set_by_name("foldmethod", OptionValue::String("bogus".into()))
2298 .unwrap_err();
2299 assert!(
2300 err.to_string().contains("must be"),
2301 "expected error about valid values, got: {err}"
2302 );
2303 }
2304
2305 #[test]
2306 fn foldenable_default_true() {
2307 let o = Options::default();
2308 assert!(o.foldenable, "foldenable must default to true");
2309 assert_eq!(
2310 o.get_by_name("foldenable"),
2311 Some(OptionValue::Bool(true)),
2312 "get_by_name(foldenable) must return Bool(true)"
2313 );
2314 }
2315
2316 #[test]
2317 fn foldenable_fen_alias_roundtrip() {
2318 let mut o = Options::default();
2319 o.set_by_name("fen", OptionValue::Bool(false)).unwrap();
2320 assert!(!o.foldenable, "fen alias must disable foldenable");
2321 assert_eq!(o.get_by_name("fen"), Some(OptionValue::Bool(false)));
2322 o.set_by_name("foldenable", OptionValue::Bool(true))
2323 .unwrap();
2324 assert!(o.foldenable);
2325 }
2326
2327 #[test]
2328 fn foldlevelstart_default_99() {
2329 let o = Options::default();
2330 assert_eq!(o.foldlevelstart, 99, "foldlevelstart must default to 99");
2331 assert_eq!(
2332 o.get_by_name("foldlevelstart"),
2333 Some(OptionValue::Int(99)),
2334 "get_by_name(foldlevelstart) must return Int(99)"
2335 );
2336 }
2337
2338 #[test]
2339 fn foldlevelstart_fls_alias_roundtrip() {
2340 let mut o = Options::default();
2341 o.set_by_name("fls", OptionValue::Int(0)).unwrap();
2342 assert_eq!(
2343 o.foldlevelstart, 0,
2344 "fls alias must set foldlevelstart to 0"
2345 );
2346 assert_eq!(o.get_by_name("fls"), Some(OptionValue::Int(0)));
2347 o.set_by_name("foldlevelstart", OptionValue::Int(5))
2348 .unwrap();
2349 assert_eq!(o.foldlevelstart, 5);
2350 }
2351}