pub use crate::input::keybindings::Action;
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct Caret {
pub position: usize,
pub anchor: Option<usize>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct ModalSnapshot {
pub top_popup: Option<PopupView>,
pub depth: usize,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct PopupView {
pub kind: String,
pub title: Option<String>,
pub items: Vec<String>,
pub selected_index: Option<usize>,
}
impl Caret {
pub fn at(position: usize) -> Self {
Self {
position,
anchor: None,
}
}
pub fn range(anchor: usize, position: usize) -> Self {
Self {
position,
anchor: Some(anchor),
}
}
pub fn selection_range(&self) -> Option<std::ops::Range<usize>> {
self.anchor.map(|a| {
if a <= self.position {
a..self.position
} else {
self.position..a
}
})
}
}
pub trait EditorTestApi {
fn dispatch(&mut self, action: Action);
fn dispatch_seq(&mut self, actions: &[Action]);
fn buffer_text(&self) -> String;
fn primary_caret(&self) -> Caret;
fn carets(&self) -> Vec<Caret>;
fn selection_text(&mut self) -> String;
fn viewport_top_byte(&self) -> usize;
fn terminal_width(&self) -> u16;
fn terminal_height(&self) -> u16;
fn gutter_width(&self) -> u16;
fn hardware_cursor_position(&mut self) -> Option<(u16, u16)>;
fn visible_byte_range(&self) -> Option<(usize, usize)>;
fn modal_snapshot(&self) -> ModalSnapshot;
fn buffer_count(&self) -> usize;
fn active_buffer_path(&self) -> Option<String>;
fn buffer_paths(&self) -> Vec<String>;
fn dispatch_mouse_click(&mut self, col: u16, row: u16) -> bool;
fn is_modified(&self) -> bool;
}
impl EditorTestApi for crate::app::Editor {
fn dispatch(&mut self, action: Action) {
self.dispatch_action_for_tests(action);
let _ = self.process_async_messages();
}
fn dispatch_seq(&mut self, actions: &[Action]) {
for a in actions {
self.dispatch_action_for_tests(a.clone());
}
let _ = self.process_async_messages();
}
fn buffer_text(&self) -> String {
self.active_state()
.buffer
.to_string()
.expect("buffer_text(): buffer has unloaded regions; semantic tests do not support large-file mode")
}
fn primary_caret(&self) -> Caret {
let c = self.active_cursors().primary();
Caret {
position: c.position,
anchor: c.anchor,
}
}
fn carets(&self) -> Vec<Caret> {
let mut out: Vec<Caret> = self
.active_cursors()
.iter()
.map(|(_, c)| Caret {
position: c.position,
anchor: c.anchor,
})
.collect();
out.sort_by_key(|c| c.position);
out
}
fn selection_text(&mut self) -> String {
let mut ranges: Vec<std::ops::Range<usize>> = self
.active_cursors()
.iter()
.filter_map(|(_, c)| c.selection_range())
.collect();
if ranges.is_empty() {
return String::new();
}
ranges.sort_by_key(|r| r.start);
let state = self.active_state_mut();
let parts: Vec<String> = ranges
.into_iter()
.map(|r| state.get_text_range(r.start, r.end))
.collect();
parts.join("\n")
}
fn viewport_top_byte(&self) -> usize {
self.active_viewport().top_byte
}
fn terminal_width(&self) -> u16 {
self.active_viewport().width
}
fn terminal_height(&self) -> u16 {
self.active_viewport().height
}
fn gutter_width(&self) -> u16 {
let buffer = &self.active_state().buffer;
u16::try_from(self.active_viewport().gutter_width(buffer)).unwrap_or(u16::MAX)
}
fn hardware_cursor_position(&mut self) -> Option<(u16, u16)> {
let cursor = *self.active_cursors().primary();
let viewport = self.active_viewport().clone();
let viewport_height = viewport.height;
let viewport_width = viewport.width;
let buffer = &mut self.active_state_mut().buffer;
let (col, row) = viewport.cursor_screen_position(buffer, &cursor);
if row >= viewport_height || col >= viewport_width {
None
} else {
Some((col, row))
}
}
fn visible_byte_range(&self) -> Option<(usize, usize)> {
None
}
fn is_modified(&self) -> bool {
self.active_state().buffer.is_modified()
}
fn modal_snapshot(&self) -> ModalSnapshot {
use crate::view::popup::{Popup, PopupContent, PopupKind};
fn kind_name(kind: PopupKind) -> &'static str {
match kind {
PopupKind::Completion => "completion",
PopupKind::Hover => "hover",
PopupKind::Action => "action",
PopupKind::List => "list",
PopupKind::Text => "text",
}
}
fn project(p: &Popup) -> PopupView {
let (items, selected_index) = match &p.content {
PopupContent::List { items, selected } => (
items.iter().map(|i| i.text.clone()).collect(),
Some(*selected),
),
_ => (Vec::new(), None),
};
PopupView {
kind: kind_name(p.kind).to_string(),
title: p.title.clone(),
items,
selected_index,
}
}
let global = self.global_popups.all();
let local = &self.active_state().popups;
let depth = global.len() + local.all().len();
let top = self
.global_popups
.top()
.or_else(|| local.top())
.map(project);
ModalSnapshot {
top_popup: top,
depth,
}
}
fn buffer_count(&self) -> usize {
self.buffer_count_for_tests()
}
fn active_buffer_path(&self) -> Option<String> {
let id = self.active_buffer();
let name = self.get_buffer_display_name(id);
if name.is_empty() {
None
} else {
Some(name)
}
}
fn buffer_paths(&self) -> Vec<String> {
self.all_buffer_ids_for_tests()
.into_iter()
.map(|id| {
let name = self.get_buffer_display_name(id);
if name.is_empty() {
format!("<unnamed:{}>", id.0)
} else {
name
}
})
.collect()
}
fn dispatch_mouse_click(&mut self, col: u16, row: u16) -> bool {
use crossterm::event::{KeyModifiers, MouseButton, MouseEvent, MouseEventKind};
let down = MouseEvent {
kind: MouseEventKind::Down(MouseButton::Left),
column: col,
row,
modifiers: KeyModifiers::NONE,
};
if let Err(e) = self.handle_mouse(down) {
tracing::trace!("mouse down errored in test dispatch: {e}");
}
let up = MouseEvent {
kind: MouseEventKind::Up(MouseButton::Left),
column: col,
row,
modifiers: KeyModifiers::NONE,
};
self.handle_mouse(up).unwrap_or(false)
}
}