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, 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;
}
locate_table(state, byte)
}
pub fn locate_table(state: &EditorState, byte: usize) -> Option<TableInfo> {
let rope = state.buffer.rope();
let line = rope.byte_to_line(byte.min(rope.len_bytes()));
let (first, cols) = crate::document::row_map::table_lines(&state.parsed, line)?;
let last = first + cols.len();
if last > rope.len_lines() {
return None;
}
let base = rope.line_to_byte(first);
let end = rope.line_to_byte(last);
let text = rope.byte_slice(base..end).to_string();
let mut info = table_edit::table_from_lines(&text, 0, &cols)?;
info.start += base;
info.end += base;
for row in &mut info.rows {
row.start += base;
row.end += base;
}
Some(info)
}
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 < info.last_col(row) {
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.last_col(pr)))
}
}
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,
) {
if let Some(info) = current_table(state) {
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;
};
if col < info.last_col(row) {
jump_to_cell(state, row, col + 1, 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.last_col(prev_row);
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);
}
}