use std::sync::Arc;
use hjkl_engine::types::{CursorShape, Host, Viewport};
use hjkl_engine::{DefaultHost, Input, Key as HKey, Options, Slot, VimMode};
use hjkl_vim::VimEditorExt;
use ratatui::crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use super::clipboard::{Clipboard, MemoryClipboard};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Mode {
Normal,
Insert,
Visual,
VisualLine,
VisualBlock,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Selection {
Chars {
start: (usize, usize),
end: (usize, usize),
},
Lines { top: usize, bottom: usize },
Block {
top: usize,
bottom: usize,
left: usize,
right: usize,
to_eol: bool,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RowSelection {
Chars { first: usize, last: usize },
Whole,
}
impl Selection {
pub fn on_row(&self, row: usize) -> Option<RowSelection> {
match *self {
Self::Chars { start, end } => {
if row < start.0 || row > end.0 {
return None;
}
let first = if row == start.0 { start.1 } else { 0 };
let last = if row == end.0 { end.1 } else { usize::MAX };
Some(RowSelection::Chars { first, last })
}
Self::Lines { top, bottom } => {
(top..=bottom).contains(&row).then_some(RowSelection::Whole)
}
Self::Block {
top,
bottom,
left,
right,
to_eol,
} => (top..=bottom)
.contains(&row)
.then_some(RowSelection::Chars {
first: left,
last: if to_eol { usize::MAX } else { right },
}),
}
}
}
pub struct Editor {
inner: hjkl_engine::Editor<hjkl_buffer::View, NmHost>,
session_start: Option<(Arc<String>, usize)>,
}
struct NmHost {
clipboard: Box<dyn Clipboard>,
stored: bool,
started: std::time::Instant,
viewport: Viewport,
}
impl Host for NmHost {
type Intent = ();
fn write_clipboard(&mut self, _text: String) {
self.stored = true;
}
fn read_clipboard(&mut self) -> Option<String> {
self.clipboard.get()
}
fn now(&self) -> std::time::Duration {
self.started.elapsed()
}
fn prompt_search(&mut self) -> Option<String> {
None
}
fn emit_cursor_shape(&mut self, _shape: CursorShape) {}
fn viewport(&self) -> &Viewport {
&self.viewport
}
fn viewport_mut(&mut self) -> &mut Viewport {
&mut self.viewport
}
fn emit_intent(&mut self, _intent: ()) {}
}
fn options() -> Options {
Options {
motion_sneak: false,
autoindent: false,
smartindent: false,
formatoptions: String::new(),
textwidth: 0,
modeline: false,
undo_levels: 200,
..Options::default()
}
}
impl Editor {
pub fn new(lines: &[String]) -> Self {
Self::with_clipboard(lines, Box::new(MemoryClipboard::default()))
}
pub fn with_clipboard(lines: &[String], clipboard: Box<dyn Clipboard>) -> Self {
let host = NmHost {
clipboard,
stored: false,
started: std::time::Instant::now(),
viewport: DefaultHost::DEFAULT_VIEWPORT,
};
let mut inner = hjkl_vim::vim_editor(hjkl_buffer::View::new(), host, options());
inner.settings_mut().autopair = false;
inner.settings_mut().autoclose_tag = false;
let mut editor = Self {
inner,
session_start: None,
};
editor.set_lines(lines);
editor
}
pub fn set_lines(&mut self, lines: &[String]) {
let mut text = lines.join("\n");
text.push('\n');
self.inner.set_content(&text);
self.inner.jump_cursor(0, 0);
self.session_start = None;
}
fn rows(&self) -> usize {
self.inner.row_count()
}
fn terminated(&self) -> bool {
let rows = self.rows();
rows > 1 && self.inner.line_char_count(rows - 1) == 0
}
pub fn lines(&self) -> Vec<String> {
(0..self.line_count())
.map(|row| self.inner.line(row).unwrap_or_default())
.collect()
}
pub fn generation(&self) -> u64 {
self.inner.buffer().dirty_gen()
}
pub fn line_count(&self) -> usize {
let rows = self.rows();
if self.terminated() {
rows - 1
} else {
rows.max(1)
}
}
fn keep_terminated(&mut self) {
let (row, col) = self.cursor();
if !self.terminated() {
let last = self.rows() - 1;
let at = hjkl_buffer::Position::new(last, self.inner.line_char_count(last));
self.inner.mutate_edit(hjkl_buffer::Edit::InsertStr {
at,
text: "\n".to_string(),
});
self.inner.set_cursor_quiet(row, col);
}
let last = self.rows().saturating_sub(2);
if row > last {
let len = self.inner.line_char_count(last);
let max = if self.mode() == Mode::Insert {
len
} else {
len.saturating_sub(1)
};
self.inner.set_cursor_quiet(last, col.min(max));
}
}
pub fn cursor(&self) -> (usize, usize) {
self.inner.cursor()
}
pub fn set_cursor(&mut self, row: usize, col: usize) {
let row = row.min(self.line_count() - 1);
let len = self.inner.line_char_count(row);
let max = if self.mode() == Mode::Insert {
len
} else {
len.saturating_sub(1)
};
self.inner.jump_cursor(row, col.min(max));
}
pub fn line_len(&self, row: usize) -> usize {
if row < self.rows() {
self.inner.line_char_count(row)
} else {
0
}
}
pub fn mode(&self) -> Mode {
match self.inner.vim_mode() {
VimMode::Normal => Mode::Normal,
VimMode::Insert => Mode::Insert,
VimMode::Visual => Mode::Visual,
VimMode::VisualLine => Mode::VisualLine,
VimMode::VisualBlock => Mode::VisualBlock,
}
}
pub fn selection(&self) -> Option<Selection> {
if let Some((start, end)) = self.inner.char_highlight() {
return Some(Selection::Chars { start, end });
}
if let Some((top, bottom)) = self.inner.line_highlight() {
return Some(Selection::Lines { top, bottom });
}
self.inner
.block_highlight()
.map(|(top, bottom, left, right)| Selection::Block {
top,
bottom,
left,
right,
to_eol: self.inner.block_to_eol(),
})
}
pub fn is_chord_pending(&self) -> bool {
self.inner.is_chord_pending()
}
pub fn clipboard(&mut self) -> &mut dyn Clipboard {
self.inner.host_mut().clipboard.as_mut()
}
pub fn set_clipboard(&mut self, clipboard: Box<dyn Clipboard>) {
self.inner.host_mut().clipboard = clipboard;
}
pub fn pending_op(&self) -> Option<char> {
self.inner.pending_op()
}
pub fn register(&self) -> (String, bool) {
self.inner
.with_registers(|r| (r.unnamed.text.clone(), r.unnamed.linewise))
}
pub fn set_register(&mut self, text: String, linewise: bool) {
self.inner.record_yank(text, linewise, None);
}
pub fn import_register(&mut self, text: String, linewise: bool) {
self.inner.set_yank_linewise(linewise);
self.inner.with_registers_mut(|r| {
r.unnamed = Slot {
text,
linewise,
..Slot::default()
};
});
}
pub fn stick_to_line_end(&mut self) {
self.inner.set_sticky_col(Some(usize::MAX));
}
pub fn can_undo(&self) -> bool {
!self.inner.buffer().undo_stack_is_empty()
}
pub fn can_redo(&self) -> bool {
!self.inner.buffer().redo_stack_is_empty()
}
pub fn send(&mut self, key: KeyEvent) -> bool {
self.inner.host_mut().stored = false;
let before = (self.mode(), self.inner.content_arc(), self.undo_depth());
self.dispatch(key);
self.clamp_normal_cursor();
self.drop_empty_undo_steps(before);
std::mem::take(&mut self.inner.host_mut().stored)
}
fn undo_depth(&self) -> usize {
self.inner.buffer().undo_stack_len()
}
fn drop_empty_undo_steps(&mut self, (mode, text, depth): (Mode, Arc<String>, usize)) {
if self.mode() == Mode::Insert {
if mode != Mode::Insert {
self.session_start = Some((text, depth));
}
return;
}
let (text, depth, keep_one) = if mode == Mode::Insert {
match self.session_start.take() {
Some((text, depth)) => (text, depth, true),
None => return,
}
} else {
(text, depth, false)
};
if self.undo_depth() <= depth {
return;
}
let changed = *self.inner.content_arc() != *text;
if changed && !keep_one {
return;
}
let keep = depth + usize::from(changed);
while self.undo_depth() > keep && self.inner.pop_last_undo() {}
}
fn clamp_normal_cursor(&mut self) {
if self.mode() != Mode::Normal {
return;
}
let (row, col) = self.cursor();
let last = self.line_len(row).saturating_sub(1);
if col > last {
self.inner.set_cursor_quiet(row, last);
}
}
fn dispatch(&mut self, key: KeyEvent) {
let Some(input) = to_input(key) else {
return;
};
let inserting = self.mode() == Mode::Insert;
let (row, col) = self.cursor();
if inserting
&& key.code == KeyCode::Delete
&& row + 1 == self.line_count()
&& col >= self.line_len(row)
{
return;
}
hjkl_vim::dispatch_input(&mut self.inner, input);
self.keep_terminated();
if inserting && self.mode() == Mode::Insert {
let new_row = self.cursor().0;
let len = self.line_len(new_row);
match key.code {
KeyCode::End => self.inner.set_cursor_doc(new_row, len),
KeyCode::Up | KeyCode::Down => self.inner.set_cursor_doc(new_row, col.min(len)),
_ => {}
}
}
}
}
fn to_input(key: KeyEvent) -> Option<Input> {
let ctrl = key.modifiers.contains(KeyModifiers::CONTROL);
let alt = key.modifiers.contains(KeyModifiers::ALT);
let mut shift = key.modifiers.contains(KeyModifiers::SHIFT);
let k = match key.code {
KeyCode::Char(c) => {
shift |= c.is_uppercase();
HKey::Char(c)
}
KeyCode::Esc => HKey::Esc,
KeyCode::Enter => HKey::Enter,
KeyCode::Backspace => HKey::Backspace,
KeyCode::Delete => HKey::Delete,
KeyCode::Tab => HKey::Tab,
KeyCode::Left => HKey::Left,
KeyCode::Right => HKey::Right,
KeyCode::Up => HKey::Up,
KeyCode::Down => HKey::Down,
KeyCode::Home => HKey::Home,
KeyCode::End => HKey::End,
KeyCode::PageUp => HKey::PageUp,
KeyCode::PageDown => HKey::PageDown,
_ => return None,
};
Some(Input {
key: k,
ctrl,
alt,
shift,
})
}