use hjkl_engine::abbrev::AbbrevTrigger;
use hjkl_engine::types::InsertDir;
use hjkl_vim_types::{InsertReason, Mode};
use super::*;
use crate::vim_state::{vim, vim_mut};
use hjkl_engine::Editor;
use hjkl_engine::buf_helpers::{
buf_cursor_pos, buf_line, buf_line_bytes, buf_line_chars, buf_row_count, buf_set_cursor_pos,
buf_set_cursor_rc,
};
use hjkl_engine::tag::{is_html_filetype, scan_tag_opener, sync_paired_tag_on_exit};
pub(crate) fn insert_char_bridge<H: hjkl_engine::types::Host>(
ed: &mut Editor<hjkl_buffer::View, H>,
ch: char,
) -> bool {
use hjkl_buffer::{Edit, MotionKind, Position};
ed.sync_buffer_content_from_textarea();
let in_replace = matches!(
vim(ed).insert_session.as_ref().map(|s| &s.reason),
Some(InsertReason::Replace)
);
if !in_replace && !ed.abbrevs_is_empty() {
let iskeyword = ed.settings().iskeyword.clone();
if !is_keyword_char(ch, &iskeyword) {
check_and_apply_abbrev(ed, AbbrevTrigger::NonKeyword(ch));
}
}
maybe_word_undo_break(ed, ch);
let cursor = buf_cursor_pos(ed.buffer());
let line_chars = buf_line_chars(ed.buffer(), cursor.row);
if !in_replace
&& !ed.pending_closes().is_empty()
&& let Some(&(pr, _pc, pch)) = ed.pending_closes().last()
&& ch == pch
&& cursor.row == pr
{
let char_at_cursor =
buf_line(ed.buffer(), cursor.row).and_then(|l| l.chars().nth(cursor.col));
if char_at_cursor == Some(ch) {
ed.pending_closes_mut().pop();
let filetype = ed.settings().filetype.clone();
let autoclose_tag = ed.settings().autoclose_tag;
if ch == '>' && autoclose_tag && is_html_filetype(&filetype) {
let new_col = cursor.col + 1;
buf_set_cursor_rc(ed.buffer_mut(), cursor.row, new_col);
if let Some(line) = buf_line(ed.buffer(), cursor.row) {
let char_col = new_col.saturating_sub(1);
let byte_col = line
.char_indices()
.nth(char_col)
.map(|(b, _)| b)
.unwrap_or(line.len());
if let Some(tag) = scan_tag_opener(&line, byte_col) {
let close_tag = format!("</{tag}>");
let insert_pos = Position::new(cursor.row, new_col);
ed.mutate_edit(Edit::InsertStr {
at: insert_pos,
text: close_tag,
});
buf_set_cursor_rc(ed.buffer_mut(), cursor.row, new_col);
}
}
} else {
buf_set_cursor_rc(ed.buffer_mut(), cursor.row, cursor.col + 1);
}
return true;
}
}
if in_replace && cursor.col < line_chars {
ed.pending_closes_mut().clear();
ed.mutate_edit(Edit::DeleteRange {
start: cursor,
end: Position::new(cursor.row, cursor.col + 1),
kind: MotionKind::Char,
});
ed.mutate_edit(Edit::InsertChar { at: cursor, ch });
} else if !try_dedent_close_bracket(ed, cursor, ch) {
let autopair = ed.settings().autopair;
let filetype = ed.settings().filetype.clone();
let autoclose_tag = ed.settings().autoclose_tag;
let (prev_char, prev2_char) = {
let line = buf_line(ed.buffer(), cursor.row).unwrap_or_default();
let chars: Vec<char> = line.chars().collect();
let p1 = if cursor.col > 0 {
chars.get(cursor.col - 1).copied()
} else {
None
};
let p2 = if cursor.col > 1 {
chars.get(cursor.col - 2).copied()
} else {
None
};
(p1, p2)
};
if autopair {
if let Some(close) = autopair_close_for(ch, &filetype, prev_char, prev2_char) {
ed.mutate_edit(Edit::InsertChar { at: cursor, ch });
let after = Position::new(cursor.row, cursor.col + 1);
ed.mutate_edit(Edit::InsertChar {
at: after,
ch: close,
});
let between_col = cursor.col + 1;
buf_set_cursor_rc(ed.buffer_mut(), cursor.row, between_col);
ed.pending_closes_mut()
.push((cursor.row, between_col, close));
return true;
}
if ch == '>' && autoclose_tag && is_html_filetype(&filetype) {
ed.mutate_edit(Edit::InsertChar { at: cursor, ch });
let new_col = cursor.col + 1;
if let Some(line) = buf_line(ed.buffer(), cursor.row) {
let char_col = new_col.saturating_sub(1);
let byte_col = line
.char_indices()
.nth(char_col)
.map(|(b, _)| b)
.unwrap_or(line.len());
if let Some(tag) = scan_tag_opener(&line, byte_col) {
let close_tag = format!("</{tag}>");
let insert_pos = Position::new(cursor.row, new_col);
ed.mutate_edit(Edit::InsertStr {
at: insert_pos,
text: close_tag,
});
buf_set_cursor_rc(ed.buffer_mut(), cursor.row, new_col);
}
}
return true;
}
}
ed.mutate_edit(Edit::InsertChar { at: cursor, ch });
}
true
}
pub(crate) fn insert_newline_bridge<H: hjkl_engine::types::Host>(
ed: &mut Editor<hjkl_buffer::View, H>,
) -> bool {
use hjkl_buffer::Edit;
ed.sync_buffer_content_from_textarea();
if !ed.abbrevs_is_empty() {
check_and_apply_abbrev(ed, AbbrevTrigger::Cr);
}
maybe_word_undo_break(ed, '\n');
let cursor = buf_cursor_pos(ed.buffer());
let prev_line = buf_line(ed.buffer(), cursor.row)
.unwrap_or_default()
.to_string();
if ed.settings().autopair && !ed.pending_closes().is_empty() {
let prev_char = if cursor.col > 0 {
prev_line.chars().nth(cursor.col - 1)
} else {
None
};
let next_char = prev_line.chars().nth(cursor.col);
let is_open_pair = matches!(
(prev_char, next_char),
(Some('{'), Some('}')) | (Some('('), Some(')')) | (Some('['), Some(']'))
);
if is_open_pair {
ed.pending_closes_mut().clear();
let base_indent: String = prev_line
.chars()
.take_while(|c| *c == ' ' || *c == '\t')
.collect();
let inner_indent = if ed.settings().expandtab {
let unit = if ed.settings().softtabstop > 0 {
ed.settings().softtabstop
} else {
ed.settings().shiftwidth
};
format!("{base_indent}{}", " ".repeat(unit))
} else {
format!("{base_indent}\t")
};
let text = format!("\n{inner_indent}\n{base_indent}");
ed.mutate_edit(Edit::InsertStr { at: cursor, text });
let new_row = cursor.row + 1;
let new_col = inner_indent.len();
buf_set_cursor_rc(ed.buffer_mut(), new_row, new_col);
return true;
}
}
if ed.settings().autopair
&& let Some(fence) = detect_code_fence_opener(&prev_line, cursor.col)
{
ed.pending_closes_mut().clear();
let base_indent: String = prev_line
.chars()
.take_while(|c| *c == ' ' || *c == '\t')
.collect();
let text = format!("\n{base_indent}\n{base_indent}{fence}");
ed.mutate_edit(Edit::InsertStr { at: cursor, text });
let new_row = cursor.row + 1;
let new_col = base_indent.chars().count();
buf_set_cursor_rc(ed.buffer_mut(), new_row, new_col);
return true;
}
let comment_cont = if ed.settings().formatoptions.contains('r') {
continue_comment(ed.buffer(), ed.settings(), cursor.row)
} else {
None
};
ed.pending_closes_mut().clear();
let text = if let Some(cont) = comment_cont {
format!("\n{cont}")
} else {
let indent = compute_enter_indent(ed.settings(), &prev_line);
format!("\n{indent}")
};
ed.mutate_edit(Edit::InsertStr { at: cursor, text });
true
}
pub(crate) fn insert_tab_bridge<H: hjkl_engine::types::Host>(
ed: &mut Editor<hjkl_buffer::View, H>,
) -> bool {
use hjkl_buffer::Edit;
ed.sync_buffer_content_from_textarea();
let cursor = buf_cursor_pos(ed.buffer());
if ed.settings().expandtab {
let sts = ed.settings().softtabstop;
let n = if sts > 0 {
sts - (cursor.col % sts)
} else {
ed.settings().tabstop.max(1)
};
ed.mutate_edit(Edit::InsertStr {
at: cursor,
text: " ".repeat(n),
});
} else {
ed.mutate_edit(Edit::InsertChar {
at: cursor,
ch: '\t',
});
}
true
}
pub(crate) fn insert_backspace_bridge<H: hjkl_engine::types::Host>(
ed: &mut Editor<hjkl_buffer::View, H>,
) -> bool {
ed.sync_buffer_content_from_textarea();
if matches!(
vim(ed).insert_session.as_ref().map(|s| &s.reason),
Some(InsertReason::Replace)
) {
return replace_backspace(ed);
}
generic_backspace(ed)
}
fn generic_backspace<H: hjkl_engine::types::Host>(ed: &mut Editor<hjkl_buffer::View, H>) -> bool {
use hjkl_buffer::{Edit, MotionKind, Position};
let cursor = buf_cursor_pos(ed.buffer());
if cursor.col > 0 {
let line = buf_line(ed.buffer(), cursor.row).unwrap_or_default();
if let Some((indent, prefix)) = detect_comment_on_line(&ed.settings().filetype, &line) {
let full_prefix = format!("{indent}{prefix}");
let line_trimmed = line.trim_end_matches(' ');
let prefix_trimmed = full_prefix.trim_end_matches(' ');
if line_trimmed == prefix_trimmed && cursor.col == full_prefix.chars().count() {
ed.mutate_edit(Edit::DeleteRange {
start: Position::new(cursor.row, 0),
end: cursor,
kind: MotionKind::Char,
});
return true;
}
}
}
let sts = ed.settings().softtabstop;
if sts > 0 && cursor.col >= sts && cursor.col.is_multiple_of(sts) {
let line = buf_line(ed.buffer(), cursor.row).unwrap_or_default();
let chars: Vec<char> = line.chars().collect();
let run_start = cursor.col - sts;
if (run_start..cursor.col).all(|i| chars.get(i).copied() == Some(' ')) {
ed.mutate_edit(Edit::DeleteRange {
start: Position::new(cursor.row, run_start),
end: cursor,
kind: MotionKind::Char,
});
return true;
}
}
if cursor.col > 0 {
ed.mutate_edit(Edit::DeleteRange {
start: Position::new(cursor.row, cursor.col - 1),
end: cursor,
kind: MotionKind::Char,
});
true
} else if cursor.row > 0 {
let prev_row = cursor.row - 1;
let prev_chars = buf_line_chars(ed.buffer(), prev_row);
ed.mutate_edit(Edit::JoinLines {
row: prev_row,
count: 1,
with_space: false,
});
buf_set_cursor_rc(ed.buffer_mut(), prev_row, prev_chars);
true
} else {
false
}
}
fn replace_backspace<H: hjkl_engine::types::Host>(ed: &mut Editor<hjkl_buffer::View, H>) -> bool {
use hjkl_buffer::{Edit, MotionKind, Position};
let cursor = buf_cursor_pos(ed.buffer());
if cursor.col == 0 {
return generic_backspace(ed);
}
let new_col = cursor.col - 1;
let Some(session) = vim(ed).insert_session.as_ref() else {
return generic_backspace(ed);
};
let start_row = session.start_row;
let start_col = session.start_col;
let before_rope = session.before_rope.clone();
if cursor.row != start_row || new_col < start_col {
buf_set_cursor_rc(ed.buffer_mut(), cursor.row, new_col);
return false;
}
let before_line = hjkl_buffer::rope_line_str(&before_rope, start_row);
let orig_ch = before_line.chars().nth(new_col);
ed.mutate_edit(Edit::DeleteRange {
start: Position::new(cursor.row, new_col),
end: cursor,
kind: MotionKind::Char,
});
if let Some(ch) = orig_ch {
ed.mutate_edit(Edit::InsertChar {
at: Position::new(cursor.row, new_col),
ch,
});
}
buf_set_cursor_rc(ed.buffer_mut(), cursor.row, new_col);
true
}
pub(crate) fn insert_delete_bridge<H: hjkl_engine::types::Host>(
ed: &mut Editor<hjkl_buffer::View, H>,
) -> bool {
use hjkl_buffer::{Edit, MotionKind, Position};
ed.sync_buffer_content_from_textarea();
let cursor = buf_cursor_pos(ed.buffer());
let line_chars = buf_line_chars(ed.buffer(), cursor.row);
if cursor.col < line_chars {
ed.mutate_edit(Edit::DeleteRange {
start: cursor,
end: Position::new(cursor.row, cursor.col + 1),
kind: MotionKind::Char,
});
buf_set_cursor_pos(ed.buffer_mut(), cursor);
true
} else if cursor.row + 1 < buf_row_count(ed.buffer()) {
ed.mutate_edit(Edit::JoinLines {
row: cursor.row,
count: 1,
with_space: false,
});
buf_set_cursor_pos(ed.buffer_mut(), cursor);
true
} else {
false
}
}
pub(crate) fn insert_arrow_bridge<H: hjkl_engine::types::Host>(
ed: &mut Editor<hjkl_buffer::View, H>,
dir: InsertDir,
) -> bool {
ed.sync_buffer_content_from_textarea();
ed.pending_closes_mut().clear();
match dir {
InsertDir::Left => {
hjkl_engine::motions::move_left(ed.buffer_mut(), 1);
}
InsertDir::Right => {
hjkl_engine::motions::move_right_to_end(ed.buffer_mut(), 1);
}
InsertDir::Up => {
let folds = hjkl_engine::SnapshotFoldProvider::from_buffer(ed.buffer());
let mut sticky = ed.sticky_col();
hjkl_engine::motions::move_up(ed.buffer_mut(), &folds, 1, &mut sticky);
ed.set_sticky_col(sticky);
}
InsertDir::Down => {
let folds = hjkl_engine::SnapshotFoldProvider::from_buffer(ed.buffer());
let mut sticky = ed.sticky_col();
hjkl_engine::motions::move_down(ed.buffer_mut(), &folds, 1, &mut sticky);
ed.set_sticky_col(sticky);
}
}
break_undo_group_in_insert(ed);
false
}
pub(crate) fn insert_home_bridge<H: hjkl_engine::types::Host>(
ed: &mut Editor<hjkl_buffer::View, H>,
) -> bool {
ed.sync_buffer_content_from_textarea();
ed.pending_closes_mut().clear();
hjkl_engine::motions::move_line_start(ed.buffer_mut());
break_undo_group_in_insert(ed);
false
}
pub(crate) fn insert_end_bridge<H: hjkl_engine::types::Host>(
ed: &mut Editor<hjkl_buffer::View, H>,
) -> bool {
ed.sync_buffer_content_from_textarea();
ed.pending_closes_mut().clear();
hjkl_engine::motions::move_line_end(ed.buffer_mut());
break_undo_group_in_insert(ed);
false
}
pub(crate) fn insert_pageup_bridge<H: hjkl_engine::types::Host>(
ed: &mut Editor<hjkl_buffer::View, H>,
viewport_h: u16,
) -> bool {
let rows = viewport_h.saturating_sub(2).max(1) as isize;
ed.scroll_cursor_rows(-rows);
false
}
pub(crate) fn insert_pagedown_bridge<H: hjkl_engine::types::Host>(
ed: &mut Editor<hjkl_buffer::View, H>,
viewport_h: u16,
) -> bool {
let rows = viewport_h.saturating_sub(2).max(1) as isize;
ed.scroll_cursor_rows(rows);
false
}
pub(crate) fn insert_ctrl_w_bridge<H: hjkl_engine::types::Host>(
ed: &mut Editor<hjkl_buffer::View, H>,
) -> bool {
use hjkl_buffer::{Edit, MotionKind};
ed.sync_buffer_content_from_textarea();
let cursor = buf_cursor_pos(ed.buffer());
if cursor.row == 0 && cursor.col == 0 {
return true;
}
if cursor.col == 0 {
let prev_row = cursor.row - 1;
let prev_chars = buf_line_chars(ed.buffer(), prev_row);
ed.mutate_edit(Edit::JoinLines {
row: prev_row,
count: 1,
with_space: false,
});
buf_set_cursor_rc(ed.buffer_mut(), prev_row, prev_chars);
return true;
}
let iskeyword = ed.settings().iskeyword.clone();
hjkl_engine::motions::move_word_back(ed.buffer_mut(), false, 1, &iskeyword);
let word_start = buf_cursor_pos(ed.buffer());
if word_start == cursor {
return true;
}
buf_set_cursor_pos(ed.buffer_mut(), cursor);
ed.mutate_edit(Edit::DeleteRange {
start: word_start,
end: cursor,
kind: MotionKind::Char,
});
true
}
pub(crate) fn insert_ctrl_u_bridge<H: hjkl_engine::types::Host>(
ed: &mut Editor<hjkl_buffer::View, H>,
) -> bool {
use hjkl_buffer::{Edit, MotionKind, Position};
ed.sync_buffer_content_from_textarea();
let cursor = buf_cursor_pos(ed.buffer());
if cursor.col == 0 {
return true;
}
let session_start_col = vim(ed)
.insert_session
.as_ref()
.filter(|s| s.start_row == cursor.row)
.map(|s| s.start_col);
let target = match session_start_col {
Some(start_col) if cursor.col > start_col => start_col,
_ => {
let line = buf_line(ed.buffer(), cursor.row).unwrap_or_default();
let first_non_blank = line
.chars()
.position(|c| c != ' ' && c != '\t')
.unwrap_or(0);
if cursor.col > first_non_blank {
first_non_blank
} else {
0
}
}
};
if target < cursor.col {
ed.mutate_edit(Edit::DeleteRange {
start: Position::new(cursor.row, target),
end: cursor,
kind: MotionKind::Char,
});
}
true
}
pub(crate) fn insert_ctrl_h_bridge<H: hjkl_engine::types::Host>(
ed: &mut Editor<hjkl_buffer::View, H>,
) -> bool {
use hjkl_buffer::{Edit, MotionKind, Position};
ed.sync_buffer_content_from_textarea();
let cursor = buf_cursor_pos(ed.buffer());
if cursor.col > 0 {
ed.mutate_edit(Edit::DeleteRange {
start: Position::new(cursor.row, cursor.col - 1),
end: cursor,
kind: MotionKind::Char,
});
} else if cursor.row > 0 {
let prev_row = cursor.row - 1;
let prev_chars = buf_line_chars(ed.buffer(), prev_row);
ed.mutate_edit(Edit::JoinLines {
row: prev_row,
count: 1,
with_space: false,
});
buf_set_cursor_rc(ed.buffer_mut(), prev_row, prev_chars);
}
true
}
pub(crate) fn insert_ctrl_a_bridge<H: hjkl_engine::types::Host>(
ed: &mut Editor<hjkl_buffer::View, H>,
) -> bool {
use hjkl_buffer::Edit;
let Some(text) = vim(ed).last_insert_text.clone() else {
return false;
};
if text.is_empty() {
return false;
}
ed.sync_buffer_content_from_textarea();
let cursor = buf_cursor_pos(ed.buffer());
ed.mutate_edit(Edit::InsertStr { at: cursor, text });
true
}
pub(crate) fn insert_ctrl_e_bridge<H: hjkl_engine::types::Host>(
ed: &mut Editor<hjkl_buffer::View, H>,
) -> bool {
insert_char_from_adjacent_line(ed, 1)
}
pub(crate) fn insert_ctrl_y_bridge<H: hjkl_engine::types::Host>(
ed: &mut Editor<hjkl_buffer::View, H>,
) -> bool {
insert_char_from_adjacent_line(ed, -1)
}
fn insert_char_from_adjacent_line<H: hjkl_engine::types::Host>(
ed: &mut Editor<hjkl_buffer::View, H>,
delta: isize,
) -> bool {
use hjkl_buffer::Edit;
ed.sync_buffer_content_from_textarea();
let cursor = buf_cursor_pos(ed.buffer());
let Some(other_row) = cursor.row.checked_add_signed(delta) else {
return false;
};
if other_row >= buf_row_count(ed.buffer()) {
return false;
}
let Some(ch) = buf_line(ed.buffer(), other_row).and_then(|l| l.chars().nth(cursor.col)) else {
return false;
};
ed.mutate_edit(Edit::InsertChar { at: cursor, ch });
true
}
pub(crate) fn insert_ctrl_t_bridge<H: hjkl_engine::types::Host>(
ed: &mut Editor<hjkl_buffer::View, H>,
) -> bool {
let (row, col) = ed.cursor();
let sw = ed.settings().shiftwidth;
indent_rows(ed, row, row, 1);
ed.jump_cursor(row, col + sw);
true
}
pub(crate) fn insert_ctrl_d_bridge<H: hjkl_engine::types::Host>(
ed: &mut Editor<hjkl_buffer::View, H>,
) -> bool {
let (row, col) = ed.cursor();
let before_len = buf_line_bytes(ed.buffer(), row);
outdent_rows(ed, row, row, 1);
let after_len = buf_line_bytes(ed.buffer(), row);
let stripped = before_len.saturating_sub(after_len);
let new_col = col.saturating_sub(stripped);
ed.jump_cursor(row, new_col);
true
}
pub(crate) fn insert_ctrl_o_bridge<H: hjkl_engine::types::Host>(
ed: &mut Editor<hjkl_buffer::View, H>,
) -> bool {
vim_mut(ed).one_shot_normal = true;
vim_mut(ed).mode = Mode::Normal;
vim_mut(ed).current_mode = hjkl_engine::VimMode::Normal;
false
}
pub(crate) fn insert_ctrl_r_bridge<H: hjkl_engine::types::Host>(
ed: &mut Editor<hjkl_buffer::View, H>,
) -> bool {
vim_mut(ed).insert_pending_register = true;
false
}
pub(crate) fn insert_paste_register_bridge<H: hjkl_engine::types::Host>(
ed: &mut Editor<hjkl_buffer::View, H>,
reg: char,
) -> bool {
insert_register_text(ed, reg);
true
}
pub(crate) fn leave_insert_to_normal_bridge<H: hjkl_engine::types::Host>(
ed: &mut Editor<hjkl_buffer::View, H>,
) -> bool {
ed.pending_closes_mut().clear();
if !ed.abbrevs_is_empty() {
check_and_apply_abbrev(ed, AbbrevTrigger::Esc);
}
finish_insert_session(ed);
sync_paired_tag_on_exit(ed);
vim_mut(ed).mode = Mode::Normal;
vim_mut(ed).current_mode = hjkl_engine::VimMode::Normal;
let col = ed.cursor().1;
vim_mut(ed).last_insert_pos = Some(ed.cursor());
if col > 0 {
hjkl_engine::motions::move_left(ed.buffer_mut(), 1);
}
ed.set_sticky_col(Some(ed.cursor().1));
true
}