use std::time::Instant;
use crate::document::EditDelta;
use crate::editor::table_edit;
use crate::editor::EditorState;
use crate::markdown::table_layout::{self, MIN_COL_WIDTH, PER_COL_OVERHEAD, ROW_END_OVERHEAD};
pub(super) fn current_widths_for_table(
state: &EditorState,
info: &table_edit::TableInfo,
) -> (Vec<usize>, Vec<Option<usize>>) {
let natural = natural_widths(info);
if let Some((start, widths)) = state.live_table_widths.as_ref() {
if *start == info.start && widths.len() == info.col_count {
let rendered = apply_user_widths(&natural, widths);
return (rendered, widths.clone());
}
}
let source = state.buffer.contents();
if info.end < source.len() {
let comment_line_end = source[info.end..]
.find('\n')
.map(|i| info.end + i)
.unwrap_or(source.len());
let comment_line = &source[info.end..comment_line_end];
if let Some(persisted) = table_layout::parse_column_widths_comment(comment_line) {
if persisted.len() == info.col_count {
let rendered = apply_user_widths(&natural, &persisted);
return (rendered, persisted);
}
}
}
(natural, vec![None; info.col_count])
}
fn natural_widths(info: &table_edit::TableInfo) -> Vec<usize> {
let col_count = info.col_count;
let mut cell_widths: Vec<Vec<usize>> = Vec::with_capacity(info.rows.len());
let mut cell_min_widths: Vec<Vec<usize>> = Vec::with_capacity(info.rows.len());
for row in &info.rows {
let mut row_widths = Vec::with_capacity(col_count);
let mut row_min_widths = Vec::with_capacity(col_count);
for cell in row.cells.iter().take(col_count) {
let trimmed = cell.raw.trim();
row_widths.push(trimmed.chars().count());
row_min_widths.push(longest_word_chars(trimmed));
}
while row_widths.len() < col_count {
row_widths.push(0);
row_min_widths.push(0);
}
cell_widths.push(row_widths);
cell_min_widths.push(row_min_widths);
}
table_layout::compute_widths(&cell_widths, &cell_min_widths, col_count, usize::MAX, None)
}
fn longest_word_chars(text: &str) -> usize {
text.split_whitespace()
.map(|w| w.chars().count())
.max()
.unwrap_or(0)
}
fn apply_user_widths(natural: &[usize], user: &[Option<usize>]) -> Vec<usize> {
let mut out = natural.to_vec();
for (i, u) in user.iter().take(out.len()).enumerate() {
if let Some(w) = u {
out[i] = (*w).max(MIN_COL_WIDTH);
}
}
out
}
pub(super) fn resize_widths(
start_widths: &[usize],
start_user_widths: &[Option<usize>],
col_idx: usize,
delta: i32,
viewport_width: usize,
) -> Option<Vec<Option<usize>>> {
let n = start_widths.len();
if col_idx == 0 || col_idx > n {
return None;
}
let left = col_idx - 1;
let border_budget = PER_COL_OVERHEAD * n + ROW_END_OVERHEAD;
let other_total: usize = start_widths
.iter()
.enumerate()
.filter(|(i, _)| *i != left)
.map(|(_, w)| *w)
.sum();
let max_left = viewport_width
.saturating_sub(border_budget + other_total)
.max(MIN_COL_WIDTH);
let target = (start_widths[left] as i32 + delta).max(MIN_COL_WIDTH as i32) as usize;
let new_left = target.min(max_left);
let mut out = start_user_widths.to_vec();
while out.len() < n {
out.push(None);
}
out[left] = Some(new_left);
Some(out)
}
pub(super) fn commit_row_drag(
state: &mut EditorState,
table_byte_start: usize,
src_idx: usize,
dst_idx: usize,
) {
if src_idx == dst_idx || src_idx < 2 || dst_idx < 2 {
return;
}
commit_swap_chain(
state,
table_byte_start,
src_idx,
dst_idx,
table_edit::swap_rows,
);
}
fn commit_swap_chain(
state: &mut EditorState,
table_byte_start: usize,
src_idx: usize,
dst_idx: usize,
swap: fn(&table_edit::TableInfo, usize, usize) -> Option<EditDelta>,
) {
let original = state.buffer.contents();
let mut composed = original.clone();
let mut cur = src_idx;
while cur != dst_idx {
let step = if cur < dst_idx { cur + 1 } else { cur - 1 };
let Some(info) = table_edit::find_table_at(&composed, table_byte_start) else {
break;
};
let Some(delta) = swap(&info, cur, step) else {
break;
};
composed = delta.apply_to_string(&composed);
cur = step;
}
let Some(byte_delta) = EditDelta::diff(&original, &composed) else {
return; };
let saved_cursor = state.cursor.offset;
let rope = state.buffer.rope();
let char_delta = EditDelta {
offset: rope.byte_to_char(byte_delta.offset),
removed: byte_delta.removed,
inserted: byte_delta.inserted,
};
state.apply_delta(char_delta);
state.cursor.offset = saved_cursor.min(state.buffer.len_chars());
state.cursor.preferred_col = state.cursor.cell_col(&state.buffer);
state.update_cursor_block();
}
pub(super) fn commit_column_border_drag(state: &mut EditorState, table_byte_start: usize) {
if state
.live_table_widths
.as_ref()
.is_some_and(|(start, _)| *start == table_byte_start)
{
state.pending_column_widths_commit = Some(table_byte_start);
} else {
state.live_table_widths = None;
state.refresh_parsed();
}
}
pub(super) fn delete_table_row_at(
state: &mut EditorState,
table_byte_start: usize,
row_idx: usize,
viewport_height: usize,
viewport_width: usize,
) {
let source = state.buffer.contents();
let Some(info) = table_edit::find_table_at(&source, table_byte_start) else {
return;
};
let Some(delta) = table_edit::delete_row(&info, row_idx) else {
return;
};
let rope = state.buffer.rope();
let char_delta = EditDelta {
offset: rope.byte_to_char(delta.offset),
removed: delta.removed,
inserted: delta.inserted,
};
state.selection = None;
state.apply_delta(char_delta);
state.last_table_delete_at = Some(Instant::now());
state.update_cursor_block();
state.ensure_cursor_visible(viewport_height, viewport_width);
}
pub(super) fn delete_table_column_at(
state: &mut EditorState,
table_byte_start: usize,
col_idx: usize,
viewport_height: usize,
viewport_width: usize,
) {
let source = state.buffer.contents();
let Some(info) = table_edit::find_table_at(&source, table_byte_start) else {
return;
};
let Some(delta) = table_edit::delete_column(&info, col_idx) else {
return;
};
let rope = state.buffer.rope();
let char_delta = EditDelta {
offset: rope.byte_to_char(delta.offset),
removed: delta.removed,
inserted: delta.inserted,
};
state.selection = None;
state.apply_delta(char_delta);
state.last_table_delete_at = Some(Instant::now());
state.update_cursor_block();
state.ensure_cursor_visible(viewport_height, viewport_width);
}
pub(super) fn commit_column_drag(
state: &mut EditorState,
table_byte_start: usize,
src_idx: usize,
dst_idx: usize,
) {
if src_idx == dst_idx {
return;
}
commit_swap_chain(
state,
table_byte_start,
src_idx,
dst_idx,
table_edit::swap_columns,
);
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::Theme;
use crate::document::Buffer;
fn theme() -> &'static Theme {
Box::leak(Box::new(Theme::default()))
}
#[test]
fn a_chain_refused_partway_commits_the_partial_move_as_one_undo_step() {
let src = "| a | b |\n|---|---|\n| 1 | 2 |\n| 3 | 4 |\n| 5 | 6 |\n";
let mut state = EditorState::new(Buffer::from_str(src), theme());
commit_row_drag(&mut state, 0, 2, 6);
let moved = state.buffer.contents();
let rows: Vec<&str> = moved.lines().skip(2).collect();
assert_eq!(
rows,
["| 3 | 4 |", "| 5 | 6 |", "| 1 | 2 |"],
"the two legal steps should have applied"
);
assert_eq!(state.history.undo_depth(), 1, "partial chain is one entry");
state.history.undo(&mut state.buffer).expect("one undo");
assert_eq!(state.buffer.contents(), src);
}
#[test]
fn a_chain_refused_outright_records_nothing() {
let src = "| a | b |\n|---|---|\n| 1 | 2 |\n";
let mut state = EditorState::new(Buffer::from_str(src), theme());
commit_row_drag(&mut state, 0, 2, 3);
assert_eq!(state.buffer.contents(), src);
assert_eq!(state.history.undo_depth(), 0);
assert!(!state.dirty, "a refused chain must not dirty the buffer");
}
}