use crate::document::{next_grapheme_offset, prev_grapheme_offset};
use crate::editor::edit_ops::{apply_byte_delta, cursor_byte, set_cursor_byte};
use crate::editor::table_edit::{
self, cell_cursor_offset, cell_end_cursor_offset, cursor_cell, find_table_at, RowKind,
TableInfo,
};
use crate::editor::{EditorState, Mode};
pub(super) fn current_table(state: &EditorState) -> Option<TableInfo> {
table_at(state, cursor_byte(state))
}
pub(super) fn table_at(state: &EditorState, byte: usize) -> Option<TableInfo> {
if state.mode == Mode::Raw {
return None;
}
let (base, run) = table_line_run(state, byte)?;
let mut info = find_table_at(&run, byte - base)?;
info.start += base;
info.end += base;
for row in &mut info.rows {
row.start += base;
row.end += base;
}
Some(info)
}
fn table_line_run(state: &EditorState, byte: usize) -> Option<(usize, String)> {
let rope = state.buffer.rope();
let len_bytes = rope.len_bytes();
let byte = byte.min(len_bytes);
let line = rope.byte_to_line(byte);
if !line_is_table_line(state, line) {
return None;
}
let mut first = line;
while first > 0 && line_is_table_line(state, first - 1) {
first -= 1;
}
let last_line = rope.len_lines().saturating_sub(1);
let mut last = line;
while last < last_line && line_is_table_line(state, last + 1) {
last += 1;
}
let start = rope.line_to_byte(first);
let end = if last >= last_line {
len_bytes
} else {
rope.line_to_byte(last + 1)
};
Some((start, rope.byte_slice(start..end).to_string()))
}
fn line_is_table_line(state: &EditorState, idx: usize) -> bool {
let rope = state.buffer.rope();
if idx >= rope.len_lines() {
return false;
}
let line = rope.line(idx);
match line.as_str() {
Some(s) => table_edit::is_table_line(s),
None => table_edit::is_table_line(&line.to_string()),
}
}
pub(super) fn cursor_in_table(state: &EditorState) -> bool {
current_table(state).is_some()
}
pub(super) fn cursor_on_alignment_row(state: &EditorState) -> bool {
let Some(info) = current_table(state) else {
return false;
};
let byte = cursor_byte(state);
cursor_cell(&info, byte)
.and_then(|(row, _)| info.rows.get(row))
.map(|row| row.kind == RowKind::Alignment)
.unwrap_or(false)
}
fn cursor_table_cell(state: &EditorState) -> Option<(TableInfo, usize, usize, usize)> {
let info = current_table(state)?;
let byte = cursor_byte(state);
let (row, col) = cursor_cell(&info, byte)?;
Some((info, byte, row, col))
}
fn skip_alignment_row(row: usize) -> usize {
if row == 1 {
2
} else {
row
}
}
pub(super) fn table_move_horizontal(state: &mut EditorState, forward: bool) -> bool {
let Some((info, byte, row, col)) = cursor_table_cell(state) else {
return false;
};
if info.rows[row].kind == RowKind::Alignment {
return false;
}
let Some(cell_first) = cell_cursor_offset(&info, row, col) else {
return false;
};
let Some(cell_end) = cell_end_cursor_offset(&info, row, col) else {
return false;
};
if forward {
if byte >= cell_end {
if let Some((nr, nc)) = adjacent_cell(&info, row, col, true) {
if let Some(target) = cell_end_cursor_offset(&info, nr, nc) {
set_cursor_byte(state, target);
}
}
return true;
}
let new_char = next_grapheme_offset(&state.buffer, state.cursor.offset);
let new_byte = state.buffer.rope().char_to_byte(new_char);
set_cursor_byte(state, new_byte.min(cell_end));
} else {
if byte <= cell_first {
if let Some((pr, pc)) = adjacent_cell(&info, row, col, false) {
if let Some(target) = cell_end_cursor_offset(&info, pr, pc) {
set_cursor_byte(state, target);
}
}
return true;
}
let new_char = prev_grapheme_offset(&state.buffer, state.cursor.offset);
let new_byte = state.buffer.rope().char_to_byte(new_char);
set_cursor_byte(state, new_byte.max(cell_first));
}
true
}
fn adjacent_cell(
info: &TableInfo,
row: usize,
col: usize,
forward: bool,
) -> Option<(usize, usize)> {
if forward {
if col + 1 < info.col_count {
return Some((row, col + 1));
}
let nr = skip_alignment_row(row + 1);
if nr < info.rows.len() {
Some((nr, 0))
} else {
None
}
} else {
if col > 0 {
return Some((row, col - 1));
}
if row == 0 {
return None;
}
let pr = if row == 2 { 0 } else { row - 1 };
if pr == 1 {
return None;
}
Some((pr, info.col_count.saturating_sub(1)))
}
}
pub(super) fn try_move_cell_vertical(
state: &mut EditorState,
down: bool,
viewport_height: usize,
viewport_width: usize,
) -> bool {
let Some((info, _byte, row, col)) = cursor_table_cell(state) else {
return false;
};
let target = if down {
skip_alignment_row(row + 1)
} else if row == 2 {
0
} else if row < 2 {
return false;
} else {
row.saturating_sub(1)
};
if target >= info.rows.len() || target == row {
return false;
}
let Some(target_byte) = cell_end_cursor_offset(&info, target, col) else {
return false;
};
let char_off = state.buffer.rope().byte_to_char(target_byte);
state.cursor.offset = char_off.min(state.buffer.len_chars());
state.cursor.preferred_col = state.cursor.cell_col(&state.buffer);
state.update_cursor_block();
state.ensure_cursor_visible(viewport_height, viewport_width);
true
}
pub(super) fn jump_to_cell(
state: &mut EditorState,
row_idx: usize,
col_idx: usize,
viewport_height: usize,
viewport_width: usize,
) {
let source = state.buffer.contents();
let byte = cursor_byte(state);
if let Some(info) = find_table_at(&source, byte) {
let row = row_idx.min(info.rows.len().saturating_sub(1));
let col = col_idx.min(info.col_count.saturating_sub(1));
if let Some(target_byte) = cell_end_cursor_offset(&info, row, col) {
let char_off = state.buffer.rope().byte_to_char(target_byte);
state.cursor.offset = char_off.min(state.buffer.len_chars());
state.cursor.preferred_col = state.cursor.cell_col(&state.buffer);
state.update_cursor_block();
state.ensure_cursor_visible(viewport_height, viewport_width);
}
}
}
pub(super) fn table_next_cell(
state: &mut EditorState,
viewport_height: usize,
viewport_width: usize,
) {
let Some((info, _byte, row, col)) = cursor_table_cell(state) else {
return;
};
let next_col = col + 1;
if next_col < info.col_count {
jump_to_cell(state, row, next_col, viewport_height, viewport_width);
return;
}
let next_row = skip_alignment_row(row + 1);
if next_row < info.rows.len() {
jump_to_cell(state, next_row, 0, viewport_height, viewport_width);
return;
}
let (byte_delta, new_row_idx) = table_edit::insert_row(&info, row, true);
let insertion_byte = byte_delta.offset;
apply_byte_delta(state, byte_delta, insertion_byte);
jump_to_cell(state, new_row_idx, 0, viewport_height, viewport_width);
}
pub(super) fn table_prev_cell(
state: &mut EditorState,
viewport_height: usize,
viewport_width: usize,
) {
let Some((info, _byte, row, col)) = cursor_table_cell(state) else {
return;
};
if col > 0 {
jump_to_cell(state, row, col - 1, viewport_height, viewport_width);
return;
}
let prev_row = row.saturating_sub(1);
let prev_row = if prev_row == 1 { 0 } else { prev_row };
if prev_row < info.rows.len() && prev_row != row {
let last_col = info.col_count.saturating_sub(1);
jump_to_cell(state, prev_row, last_col, viewport_height, viewport_width);
}
}
pub(super) fn table_next_row(
state: &mut EditorState,
viewport_height: usize,
viewport_width: usize,
) {
let Some((info, _byte, row, col)) = cursor_table_cell(state) else {
return;
};
let target = skip_alignment_row(row + 1);
if target < info.rows.len() {
jump_to_cell(state, target, col, viewport_height, viewport_width);
return;
}
let (byte_delta, new_row_idx) = table_edit::insert_row(&info, row, true);
let insertion_byte = byte_delta.offset;
apply_byte_delta(state, byte_delta, insertion_byte);
jump_to_cell(state, new_row_idx, col, viewport_height, viewport_width);
}
pub(super) fn table_prev_row(
state: &mut EditorState,
viewport_height: usize,
viewport_width: usize,
) {
let Some((info, _byte, row, col)) = cursor_table_cell(state) else {
return;
};
let target = if row == 2 { 0 } else { row.saturating_sub(1) };
if target == 1 || target >= info.rows.len() {
return;
}
jump_to_cell(state, target, col, viewport_height, viewport_width);
}
pub(super) fn table_move_row(
state: &mut EditorState,
down: bool,
viewport_height: usize,
viewport_width: usize,
) {
let Some((info, byte, row, col)) = cursor_table_cell(state) else {
return;
};
if row < 2 {
return;
}
let other = if down { row + 1 } else { row.saturating_sub(1) };
if other < 2 || other >= info.rows.len() || other == row {
return;
}
let Some(byte_delta) = table_edit::swap_rows(&info, row, other) else {
return;
};
apply_byte_delta(state, byte_delta, byte);
jump_to_cell(state, other, col, viewport_height, viewport_width);
}
pub(super) fn table_move_column(
state: &mut EditorState,
right: bool,
viewport_height: usize,
viewport_width: usize,
) {
let Some((info, byte, row, col)) = cursor_table_cell(state) else {
return;
};
let other = if right {
col + 1
} else {
col.saturating_sub(1)
};
if other >= info.col_count || other == col {
return;
}
let Some(byte_delta) = table_edit::swap_columns(&info, col, other) else {
return;
};
apply_byte_delta(state, byte_delta, byte);
jump_to_cell(state, row, other, viewport_height, viewport_width);
}
pub(super) fn table_insert_row(
state: &mut EditorState,
below: bool,
viewport_height: usize,
viewport_width: usize,
) {
let Some((info, _byte, row, _col)) = cursor_table_cell(state) else {
return;
};
let (byte_delta, new_row_idx) = table_edit::insert_row(&info, row, below);
let insertion_byte = byte_delta.offset;
apply_byte_delta(state, byte_delta, insertion_byte);
jump_to_cell(state, new_row_idx, 0, viewport_height, viewport_width);
}
pub(super) fn table_insert_column(
state: &mut EditorState,
right: bool,
viewport_height: usize,
viewport_width: usize,
) {
let Some((info, _byte, row, col)) = cursor_table_cell(state) else {
return;
};
let byte_delta = table_edit::insert_column(&info, col, right);
let insertion_byte = byte_delta.offset;
apply_byte_delta(state, byte_delta, insertion_byte);
let new_col = if right { col + 1 } else { col };
jump_to_cell(state, row, new_col, viewport_height, viewport_width);
}
pub(super) fn table_delete_row(
state: &mut EditorState,
viewport_height: usize,
viewport_width: usize,
) {
let Some((info, _byte, row, col)) = cursor_table_cell(state) else {
return;
};
if info.rows[row].kind != RowKind::Data {
return;
}
let Some(byte_delta) = table_edit::delete_row(&info, row) else {
return;
};
let delta_offset = byte_delta.offset;
apply_byte_delta(state, byte_delta, delta_offset);
let target_row = if row < info.rows.len() - 1 {
row
} else {
row.saturating_sub(1).max(2)
};
jump_to_cell(state, target_row, col, viewport_height, viewport_width);
}
pub(super) fn table_delete_column(
state: &mut EditorState,
viewport_height: usize,
viewport_width: usize,
) {
let Some((info, _byte, row, col)) = cursor_table_cell(state) else {
return;
};
let Some(byte_delta) = table_edit::delete_column(&info, col) else {
return;
};
let delta_offset = byte_delta.offset;
apply_byte_delta(state, byte_delta, delta_offset);
let new_col = col.min(info.col_count.saturating_sub(2));
jump_to_cell(state, row, new_col, viewport_height, viewport_width);
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn skip_alignment_row_only_remaps_one() {
assert_eq!(skip_alignment_row(0), 0);
assert_eq!(skip_alignment_row(1), 2);
assert_eq!(skip_alignment_row(2), 2);
assert_eq!(skip_alignment_row(3), 3);
assert_eq!(skip_alignment_row(99), 99);
}
}