1use std::ops::Range;
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
17pub struct Pos {
18 pub line: u32,
19 pub col: u32,
20}
21
22impl Pos {
23 pub const ORIGIN: Pos = Pos { line: 0, col: 0 };
24
25 pub const fn new(line: u32, col: u32) -> Self {
26 Pos { line, col }
27 }
28}
29
30#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
41pub enum SelectionKind {
42 #[default]
43 Char,
44 Line,
45 Block,
46}
47
48#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
50pub struct Selection {
51 pub anchor: Pos,
52 pub head: Pos,
53 pub kind: SelectionKind,
54}
55
56impl Selection {
57 pub const fn caret(pos: Pos) -> Self {
59 Selection {
60 anchor: pos,
61 head: pos,
62 kind: SelectionKind::Char,
63 }
64 }
65
66 pub const fn char_range(anchor: Pos, head: Pos) -> Self {
68 Selection {
69 anchor,
70 head,
71 kind: SelectionKind::Char,
72 }
73 }
74
75 pub fn is_empty(&self) -> bool {
77 self.anchor == self.head
78 }
79}
80
81#[derive(Debug, Clone, PartialEq, Eq)]
84pub struct SelectionSet {
85 pub items: Vec<Selection>,
86 pub primary: usize,
87}
88
89impl SelectionSet {
90 pub fn caret(pos: Pos) -> Self {
92 SelectionSet {
93 items: vec![Selection::caret(pos)],
94 primary: 0,
95 }
96 }
97
98 pub fn primary(&self) -> &Selection {
101 self.items
102 .get(self.primary)
103 .or_else(|| self.items.first())
104 .expect("SelectionSet must contain at least one selection")
105 }
106}
107
108impl Default for SelectionSet {
109 fn default() -> Self {
110 SelectionSet::caret(Pos::ORIGIN)
111 }
112}
113
114#[derive(Debug, Clone, PartialEq, Eq)]
118pub struct Edit {
119 pub range: Range<Pos>,
120 pub replacement: String,
121}
122
123impl Edit {
124 pub fn insert(at: Pos, text: impl Into<String>) -> Self {
125 Edit {
126 range: at..at,
127 replacement: text.into(),
128 }
129 }
130
131 pub fn delete(range: Range<Pos>) -> Self {
132 Edit {
133 range,
134 replacement: String::new(),
135 }
136 }
137
138 pub fn replace(range: Range<Pos>, text: impl Into<String>) -> Self {
139 Edit {
140 range,
141 replacement: text.into(),
142 }
143 }
144}
145
146#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
150pub enum Mode {
151 #[default]
152 Normal,
153 Insert,
154 Visual,
155 Replace,
156 Command,
157 OperatorPending,
158}
159
160#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
163pub enum CursorShape {
164 #[default]
165 Block,
166 Bar,
167 Underline,
168}
169
170#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
173pub struct Style {
174 pub fg: Option<Color>,
175 pub bg: Option<Color>,
176 pub attrs: Attrs,
177}
178
179#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
180pub struct Color(pub u8, pub u8, pub u8);
181
182bitflags::bitflags! {
183 #[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Hash)]
184 pub struct Attrs: u8 {
185 const BOLD = 1 << 0;
186 const ITALIC = 1 << 1;
187 const UNDERLINE = 1 << 2;
188 const REVERSE = 1 << 3;
189 const DIM = 1 << 4;
190 const STRIKE = 1 << 5;
191 }
192}
193
194#[derive(Debug, Clone, Copy, PartialEq, Eq)]
198pub enum HighlightKind {
199 Selection,
200 SearchMatch,
201 IncSearch,
202 MatchParen,
203 Syntax(u32),
204}
205
206#[derive(Debug, Clone, PartialEq, Eq)]
207pub struct Highlight {
208 pub range: Range<Pos>,
209 pub kind: HighlightKind,
210}
211
212#[derive(Debug, Clone, PartialEq, Eq)]
216pub struct Options {
217 pub tabstop: u32,
219 pub shiftwidth: u32,
221 pub expandtab: bool,
223 pub iskeyword: String,
227 pub ignorecase: bool,
229 pub smartcase: bool,
232 pub hlsearch: bool,
234 pub incsearch: bool,
236 pub wrapscan: bool,
238 pub autoindent: bool,
240 pub timeout_len: core::time::Duration,
242 pub undo_levels: u32,
244 pub undo_break_on_motion: bool,
247 pub readonly: bool,
249 pub wrap: WrapMode,
252}
253
254#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
258#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
259pub enum WrapMode {
260 #[default]
263 None,
264 Char,
267 Word,
271}
272
273#[derive(Debug, Clone, PartialEq, Eq)]
279pub enum OptionValue {
280 Bool(bool),
281 Int(i64),
282 String(String),
283}
284
285impl Default for Options {
286 fn default() -> Self {
287 Options {
288 tabstop: 8,
289 shiftwidth: 8,
290 expandtab: false,
291 iskeyword: "@,48-57,_,192-255".to_string(),
292 ignorecase: false,
293 smartcase: false,
294 hlsearch: true,
295 incsearch: true,
296 wrapscan: true,
297 autoindent: true,
298 timeout_len: core::time::Duration::from_millis(1000),
299 undo_levels: 1000,
300 undo_break_on_motion: true,
301 readonly: false,
302 wrap: WrapMode::None,
303 }
304 }
305}
306
307impl Options {
308 pub fn set_by_name(&mut self, name: &str, val: OptionValue) -> Result<(), EngineError> {
315 macro_rules! set_bool {
316 ($field:ident) => {{
317 self.$field = match val {
318 OptionValue::Bool(b) => b,
319 OptionValue::Int(n) => n != 0,
320 other => {
321 return Err(EngineError::Ex(format!(
322 "option `{name}` expects bool, got {other:?}"
323 )));
324 }
325 };
326 Ok(())
327 }};
328 }
329 macro_rules! set_u32 {
330 ($field:ident) => {{
331 self.$field = match val {
332 OptionValue::Int(n) if n >= 0 && n <= u32::MAX as i64 => n as u32,
333 OptionValue::Int(n) => {
334 return Err(EngineError::Ex(format!(
335 "option `{name}` out of u32 range: {n}"
336 )));
337 }
338 other => {
339 return Err(EngineError::Ex(format!(
340 "option `{name}` expects int, got {other:?}"
341 )));
342 }
343 };
344 Ok(())
345 }};
346 }
347 macro_rules! set_string {
348 ($field:ident) => {{
349 self.$field = match val {
350 OptionValue::String(s) => s,
351 other => {
352 return Err(EngineError::Ex(format!(
353 "option `{name}` expects string, got {other:?}"
354 )));
355 }
356 };
357 Ok(())
358 }};
359 }
360 match name {
361 "tabstop" | "ts" => set_u32!(tabstop),
362 "shiftwidth" | "sw" => set_u32!(shiftwidth),
363 "expandtab" | "et" => set_bool!(expandtab),
364 "iskeyword" | "isk" => set_string!(iskeyword),
365 "ignorecase" | "ic" => set_bool!(ignorecase),
366 "smartcase" | "scs" => set_bool!(smartcase),
367 "hlsearch" | "hls" => set_bool!(hlsearch),
368 "incsearch" | "is" => set_bool!(incsearch),
369 "wrapscan" | "ws" => set_bool!(wrapscan),
370 "autoindent" | "ai" => set_bool!(autoindent),
371 "timeoutlen" | "tm" => {
372 self.timeout_len = match val {
373 OptionValue::Int(n) if n >= 0 => core::time::Duration::from_millis(n as u64),
374 other => {
375 return Err(EngineError::Ex(format!(
376 "option `{name}` expects non-negative int (millis), got {other:?}"
377 )));
378 }
379 };
380 Ok(())
381 }
382 "undolevels" | "ul" => set_u32!(undo_levels),
383 "undobreak" => set_bool!(undo_break_on_motion),
384 "readonly" | "ro" => set_bool!(readonly),
385 "wrap" => {
386 let on = match val {
387 OptionValue::Bool(b) => b,
388 OptionValue::Int(n) => n != 0,
389 other => {
390 return Err(EngineError::Ex(format!(
391 "option `{name}` expects bool, got {other:?}"
392 )));
393 }
394 };
395 self.wrap = match (on, self.wrap) {
396 (false, _) => WrapMode::None,
397 (true, WrapMode::Word) => WrapMode::Word,
398 (true, _) => WrapMode::Char,
399 };
400 Ok(())
401 }
402 "linebreak" | "lbr" => {
403 let on = match val {
404 OptionValue::Bool(b) => b,
405 OptionValue::Int(n) => n != 0,
406 other => {
407 return Err(EngineError::Ex(format!(
408 "option `{name}` expects bool, got {other:?}"
409 )));
410 }
411 };
412 self.wrap = match (on, self.wrap) {
413 (true, _) => WrapMode::Word,
414 (false, WrapMode::Word) => WrapMode::Char,
415 (false, other) => other,
416 };
417 Ok(())
418 }
419 other => Err(EngineError::Ex(format!("unknown option `{other}`"))),
420 }
421 }
422
423 pub fn get_by_name(&self, name: &str) -> Option<OptionValue> {
425 Some(match name {
426 "tabstop" | "ts" => OptionValue::Int(self.tabstop as i64),
427 "shiftwidth" | "sw" => OptionValue::Int(self.shiftwidth as i64),
428 "expandtab" | "et" => OptionValue::Bool(self.expandtab),
429 "iskeyword" | "isk" => OptionValue::String(self.iskeyword.clone()),
430 "ignorecase" | "ic" => OptionValue::Bool(self.ignorecase),
431 "smartcase" | "scs" => OptionValue::Bool(self.smartcase),
432 "hlsearch" | "hls" => OptionValue::Bool(self.hlsearch),
433 "incsearch" | "is" => OptionValue::Bool(self.incsearch),
434 "wrapscan" | "ws" => OptionValue::Bool(self.wrapscan),
435 "autoindent" | "ai" => OptionValue::Bool(self.autoindent),
436 "timeoutlen" | "tm" => OptionValue::Int(self.timeout_len.as_millis() as i64),
437 "undolevels" | "ul" => OptionValue::Int(self.undo_levels as i64),
438 "undobreak" => OptionValue::Bool(self.undo_break_on_motion),
439 "readonly" | "ro" => OptionValue::Bool(self.readonly),
440 "wrap" => OptionValue::Bool(!matches!(self.wrap, WrapMode::None)),
441 "linebreak" | "lbr" => OptionValue::Bool(matches!(self.wrap, WrapMode::Word)),
442 _ => return None,
443 })
444 }
445}
446
447#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
451pub struct Viewport {
452 pub top_line: u32,
453 pub height: u32,
454 pub scroll_off: u32,
455}
456
457#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
461pub struct BufferId(pub u64);
462
463#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
465pub struct Modifiers {
466 pub ctrl: bool,
467 pub shift: bool,
468 pub alt: bool,
469 pub super_: bool,
470}
471
472#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
474#[non_exhaustive]
475pub enum SpecialKey {
476 Esc,
477 Enter,
478 Backspace,
479 Tab,
480 BackTab,
481 Up,
482 Down,
483 Left,
484 Right,
485 Home,
486 End,
487 PageUp,
488 PageDown,
489 Insert,
490 Delete,
491 F(u8),
492}
493
494#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
495pub enum MouseKind {
496 Press,
497 Release,
498 Drag,
499 ScrollUp,
500 ScrollDown,
501}
502
503#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
504pub struct MouseEvent {
505 pub kind: MouseKind,
506 pub pos: Pos,
507 pub mods: Modifiers,
508}
509
510#[derive(Debug, Clone, PartialEq, Eq)]
515#[non_exhaustive]
516pub enum Input {
517 Char(char, Modifiers),
518 Key(SpecialKey, Modifiers),
519 Mouse(MouseEvent),
520 Paste(String),
521 FocusGained,
522 FocusLost,
523 Resize(u16, u16),
524}
525
526pub trait Host: Send {
535 type Intent;
539
540 fn write_clipboard(&mut self, text: String);
546
547 fn read_clipboard(&mut self) -> Option<String>;
550
551 fn now(&self) -> core::time::Duration;
557
558 fn should_cancel(&self) -> bool {
561 false
562 }
563
564 fn prompt_search(&mut self) -> Option<String>;
569
570 fn display_line_for(&self, pos: Pos) -> u32 {
575 pos.line
576 }
577
578 fn pos_for_display(&self, line: u32, col: u32) -> Pos {
580 Pos { line, col }
581 }
582
583 fn syntax_highlights(&self, range: Range<Pos>) -> Vec<Highlight> {
588 let _ = range;
589 Vec::new()
590 }
591
592 fn emit_cursor_shape(&mut self, shape: CursorShape);
597
598 fn emit_intent(&mut self, intent: Self::Intent);
603}
604
605#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
617pub struct RenderFrame {
618 pub mode: SnapshotMode,
619 pub cursor_row: u32,
620 pub cursor_col: u32,
621 pub cursor_shape: CursorShape,
622 pub viewport_top: u32,
623 pub line_count: u32,
624}
625
626#[derive(Debug, Clone)]
642#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
643pub struct EditorSnapshot {
644 pub version: u32,
647 pub mode: SnapshotMode,
649 pub cursor: (u32, u32),
651 pub lines: Vec<String>,
653 pub viewport_top: u32,
655 pub registers: crate::Registers,
659 pub file_marks: std::collections::HashMap<char, (u32, u32)>,
663}
664
665#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
669#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
670pub enum SnapshotMode {
671 #[default]
672 Normal,
673 Insert,
674 Visual,
675 VisualLine,
676 VisualBlock,
677}
678
679impl EditorSnapshot {
680 pub const VERSION: u32 = 3;
685}
686
687#[derive(Debug, thiserror::Error)]
691pub enum EngineError {
692 #[error("regex compile error: {0}")]
695 Regex(#[from] regex::Error),
696
697 #[error("invalid range: {0}")]
699 InvalidRange(String),
700
701 #[error("ex parse: {0}")]
703 Ex(String),
704
705 #[error("buffer is read-only")]
707 ReadOnly,
708
709 #[error("position out of bounds: {0:?}")]
711 OutOfBounds(Pos),
712
713 #[error("snapshot version mismatch: file={0}, expected={1}")]
716 SnapshotVersion(u32, u32),
717}
718
719#[cfg(test)]
720mod tests {
721 use super::*;
722
723 #[test]
724 fn caret_is_empty() {
725 let sel = Selection::caret(Pos::new(2, 4));
726 assert!(sel.is_empty());
727 assert_eq!(sel.anchor, sel.head);
728 }
729
730 #[test]
731 fn selection_set_default_has_one_caret() {
732 let set = SelectionSet::default();
733 assert_eq!(set.items.len(), 1);
734 assert_eq!(set.primary, 0);
735 assert_eq!(set.primary().anchor, Pos::ORIGIN);
736 }
737
738 #[test]
739 fn edit_constructors() {
740 let p = Pos::new(0, 5);
741 assert_eq!(Edit::insert(p, "x").range, p..p);
742 assert!(Edit::insert(p, "x").replacement == "x");
743 assert!(Edit::delete(p..p).replacement.is_empty());
744 }
745
746 #[test]
747 fn attrs_flags() {
748 let a = Attrs::BOLD | Attrs::UNDERLINE;
749 assert!(a.contains(Attrs::BOLD));
750 assert!(!a.contains(Attrs::ITALIC));
751 }
752
753 #[test]
754 fn options_set_get_roundtrip() {
755 let mut o = Options::default();
756 o.set_by_name("tabstop", OptionValue::Int(4)).unwrap();
757 assert!(matches!(o.get_by_name("ts"), Some(OptionValue::Int(4))));
758 o.set_by_name("expandtab", OptionValue::Bool(true)).unwrap();
759 assert!(matches!(o.get_by_name("et"), Some(OptionValue::Bool(true))));
760 o.set_by_name("iskeyword", OptionValue::String("a-z".into()))
761 .unwrap();
762 match o.get_by_name("iskeyword") {
763 Some(OptionValue::String(s)) => assert_eq!(s, "a-z"),
764 other => panic!("expected String, got {other:?}"),
765 }
766 }
767
768 #[test]
769 fn options_unknown_name_errors_on_set() {
770 let mut o = Options::default();
771 assert!(matches!(
772 o.set_by_name("frobnicate", OptionValue::Int(1)),
773 Err(EngineError::Ex(_))
774 ));
775 assert!(o.get_by_name("frobnicate").is_none());
776 }
777
778 #[test]
779 fn options_type_mismatch_errors() {
780 let mut o = Options::default();
781 assert!(matches!(
782 o.set_by_name("tabstop", OptionValue::String("nope".into())),
783 Err(EngineError::Ex(_))
784 ));
785 assert!(matches!(
786 o.set_by_name("iskeyword", OptionValue::Int(7)),
787 Err(EngineError::Ex(_))
788 ));
789 }
790
791 #[test]
792 fn options_int_to_bool_coercion() {
793 let mut o = Options::default();
796 o.set_by_name("ignorecase", OptionValue::Int(1)).unwrap();
797 assert!(matches!(o.get_by_name("ic"), Some(OptionValue::Bool(true))));
798 o.set_by_name("ignorecase", OptionValue::Int(0)).unwrap();
799 assert!(matches!(
800 o.get_by_name("ic"),
801 Some(OptionValue::Bool(false))
802 ));
803 }
804
805 #[test]
806 fn options_wrap_linebreak_roundtrip() {
807 let mut o = Options::default();
808 assert_eq!(o.wrap, WrapMode::None);
809 o.set_by_name("wrap", OptionValue::Bool(true)).unwrap();
810 assert_eq!(o.wrap, WrapMode::Char);
811 o.set_by_name("linebreak", OptionValue::Bool(true)).unwrap();
812 assert_eq!(o.wrap, WrapMode::Word);
813 assert!(matches!(
814 o.get_by_name("wrap"),
815 Some(OptionValue::Bool(true))
816 ));
817 assert!(matches!(
818 o.get_by_name("lbr"),
819 Some(OptionValue::Bool(true))
820 ));
821 o.set_by_name("linebreak", OptionValue::Bool(false))
822 .unwrap();
823 assert_eq!(o.wrap, WrapMode::Char);
824 o.set_by_name("wrap", OptionValue::Bool(false)).unwrap();
825 assert_eq!(o.wrap, WrapMode::None);
826 }
827
828 #[test]
829 fn options_default_matches_vim() {
830 let o = Options::default();
831 assert_eq!(o.tabstop, 8);
832 assert!(!o.expandtab);
833 assert!(o.hlsearch);
834 assert!(o.wrapscan);
835 assert_eq!(o.timeout_len, core::time::Duration::from_millis(1000));
836 }
837
838 #[test]
839 fn editor_snapshot_version_const() {
840 assert_eq!(EditorSnapshot::VERSION, 3);
841 }
842
843 #[test]
844 fn editor_snapshot_default_shape() {
845 let s = EditorSnapshot {
846 version: EditorSnapshot::VERSION,
847 mode: SnapshotMode::Normal,
848 cursor: (0, 0),
849 lines: vec!["hello".to_string()],
850 viewport_top: 0,
851 registers: crate::Registers::default(),
852 file_marks: Default::default(),
853 };
854 assert_eq!(s.cursor, (0, 0));
855 assert_eq!(s.lines.len(), 1);
856 }
857
858 #[cfg(feature = "serde")]
859 #[test]
860 fn editor_snapshot_roundtrip() {
861 let mut file_marks = std::collections::HashMap::new();
862 file_marks.insert('A', (5u32, 2u32));
863 let s = EditorSnapshot {
864 version: EditorSnapshot::VERSION,
865 mode: SnapshotMode::Insert,
866 cursor: (3, 7),
867 lines: vec!["alpha".into(), "beta".into()],
868 viewport_top: 2,
869 registers: crate::Registers::default(),
870 file_marks,
871 };
872 let json = serde_json::to_string(&s).unwrap();
873 let back: EditorSnapshot = serde_json::from_str(&json).unwrap();
874 assert_eq!(s.cursor, back.cursor);
875 assert_eq!(s.lines, back.lines);
876 assert_eq!(s.viewport_top, back.viewport_top);
877 }
878
879 #[test]
880 fn engine_error_display() {
881 let e = EngineError::ReadOnly;
882 assert_eq!(e.to_string(), "buffer is read-only");
883 let e = EngineError::OutOfBounds(Pos::new(3, 7));
884 assert!(e.to_string().contains("out of bounds"));
885 }
886}