use std::ops::Range;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
pub struct Pos {
pub line: u32,
pub col: u32,
}
impl Pos {
pub const ORIGIN: Pos = Pos { line: 0, col: 0 };
pub const fn new(line: u32, col: u32) -> Self {
Pos { line, col }
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum SelectionKind {
#[default]
Char,
Line,
Block,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct Selection {
pub anchor: Pos,
pub head: Pos,
pub kind: SelectionKind,
}
impl Selection {
pub const fn caret(pos: Pos) -> Self {
Selection {
anchor: pos,
head: pos,
kind: SelectionKind::Char,
}
}
pub const fn char_range(anchor: Pos, head: Pos) -> Self {
Selection {
anchor,
head,
kind: SelectionKind::Char,
}
}
pub fn is_empty(&self) -> bool {
self.anchor == self.head
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SelectionSet {
pub items: Vec<Selection>,
pub primary: usize,
}
impl SelectionSet {
pub fn caret(pos: Pos) -> Self {
SelectionSet {
items: vec![Selection::caret(pos)],
primary: 0,
}
}
pub fn primary(&self) -> &Selection {
self.items
.get(self.primary)
.or_else(|| self.items.first())
.expect("SelectionSet must contain at least one selection")
}
}
impl Default for SelectionSet {
fn default() -> Self {
SelectionSet::caret(Pos::ORIGIN)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Edit {
pub range: Range<Pos>,
pub replacement: String,
}
impl Edit {
pub fn insert(at: Pos, text: impl Into<String>) -> Self {
Edit {
range: at..at,
replacement: text.into(),
}
}
pub fn delete(range: Range<Pos>) -> Self {
Edit {
range,
replacement: String::new(),
}
}
pub fn replace(range: Range<Pos>, text: impl Into<String>) -> Self {
Edit {
range,
replacement: text.into(),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Mode {
#[default]
Normal,
Insert,
Visual,
Replace,
Command,
OperatorPending,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum CursorShape {
#[default]
Block,
Bar,
Underline,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct Style {
pub fg: Option<Color>,
pub bg: Option<Color>,
pub attrs: Attrs,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub struct Color(pub u8, pub u8, pub u8);
bitflags::bitflags! {
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Hash)]
pub struct Attrs: u8 {
const BOLD = 1 << 0;
const ITALIC = 1 << 1;
const UNDERLINE = 1 << 2;
const REVERSE = 1 << 3;
const DIM = 1 << 4;
const STRIKE = 1 << 5;
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum HighlightKind {
Selection,
SearchMatch,
IncSearch,
MatchParen,
Syntax(u32),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Highlight {
pub range: Range<Pos>,
pub kind: HighlightKind,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Options {
pub tabstop: u32,
pub shiftwidth: u32,
pub expandtab: bool,
pub iskeyword: String,
pub ignorecase: bool,
pub smartcase: bool,
pub hlsearch: bool,
pub incsearch: bool,
pub wrapscan: bool,
pub autoindent: bool,
pub timeout_len: core::time::Duration,
pub undo_levels: u32,
pub undo_break_on_motion: bool,
pub readonly: bool,
}
impl Default for Options {
fn default() -> Self {
Options {
tabstop: 8,
shiftwidth: 8,
expandtab: false,
iskeyword: "@,48-57,_,192-255".to_string(),
ignorecase: false,
smartcase: false,
hlsearch: true,
incsearch: true,
wrapscan: true,
autoindent: true,
timeout_len: core::time::Duration::from_millis(1000),
undo_levels: 1000,
undo_break_on_motion: true,
readonly: false,
}
}
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
pub struct Viewport {
pub top_line: u32,
pub height: u32,
pub scroll_off: u32,
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
pub struct BufferId(pub u64);
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
pub struct Modifiers {
pub ctrl: bool,
pub shift: bool,
pub alt: bool,
pub super_: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum SpecialKey {
Esc,
Enter,
Backspace,
Tab,
BackTab,
Up,
Down,
Left,
Right,
Home,
End,
PageUp,
PageDown,
Insert,
Delete,
F(u8),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum MouseKind {
Press,
Release,
Drag,
ScrollUp,
ScrollDown,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct MouseEvent {
pub kind: MouseKind,
pub pos: Pos,
pub mods: Modifiers,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum Input {
Char(char, Modifiers),
Key(SpecialKey, Modifiers),
Mouse(MouseEvent),
Paste(String),
FocusGained,
FocusLost,
Resize(u16, u16),
}
pub trait Host: Send {
type Intent;
fn write_clipboard(&mut self, text: String);
fn read_clipboard(&mut self) -> Option<String>;
fn now(&self) -> core::time::Duration;
fn should_cancel(&self) -> bool {
false
}
fn prompt_search(&mut self) -> Option<String>;
fn display_line_for(&self, pos: Pos) -> u32 {
pos.line
}
fn pos_for_display(&self, line: u32, col: u32) -> Pos {
Pos { line, col }
}
fn syntax_highlights(&self, range: Range<Pos>) -> Vec<Highlight> {
let _ = range;
Vec::new()
}
fn emit_cursor_shape(&mut self, shape: CursorShape);
fn emit_intent(&mut self, intent: Self::Intent);
}
#[derive(Debug, thiserror::Error)]
pub enum EngineError {
#[error("regex compile error: {0}")]
Regex(#[from] regex::Error),
#[error("invalid range: {0}")]
InvalidRange(String),
#[error("ex parse: {0}")]
Ex(String),
#[error("buffer is read-only")]
ReadOnly,
#[error("position out of bounds: {0:?}")]
OutOfBounds(Pos),
#[error("snapshot version mismatch: file={0}, expected={1}")]
SnapshotVersion(u32, u32),
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn caret_is_empty() {
let sel = Selection::caret(Pos::new(2, 4));
assert!(sel.is_empty());
assert_eq!(sel.anchor, sel.head);
}
#[test]
fn selection_set_default_has_one_caret() {
let set = SelectionSet::default();
assert_eq!(set.items.len(), 1);
assert_eq!(set.primary, 0);
assert_eq!(set.primary().anchor, Pos::ORIGIN);
}
#[test]
fn edit_constructors() {
let p = Pos::new(0, 5);
assert_eq!(Edit::insert(p, "x").range, p..p);
assert!(Edit::insert(p, "x").replacement == "x");
assert!(Edit::delete(p..p).replacement.is_empty());
}
#[test]
fn attrs_flags() {
let a = Attrs::BOLD | Attrs::UNDERLINE;
assert!(a.contains(Attrs::BOLD));
assert!(!a.contains(Attrs::ITALIC));
}
#[test]
fn options_default_matches_vim() {
let o = Options::default();
assert_eq!(o.tabstop, 8);
assert!(!o.expandtab);
assert!(o.hlsearch);
assert!(o.wrapscan);
assert_eq!(o.timeout_len, core::time::Duration::from_millis(1000));
}
#[test]
fn engine_error_display() {
let e = EngineError::ReadOnly;
assert_eq!(e.to_string(), "buffer is read-only");
let e = EngineError::OutOfBounds(Pos::new(3, 7));
assert!(e.to_string().contains("out of bounds"));
}
}