use logisheets_base::{CellId, ColId, RowId, SheetId, errors::BasicError};
use super::sheet_nav::{MAX_COL_CNT, MAX_ROW_CNT};
use super::{BlockPlace, Navigator, SheetNav, ctx::NavExecCtx};
use crate::{Error, edit_action::EditPayload};
pub const MAX_BLOCK_CELLS: u64 = 1_048_576;
fn check_line_range(start: usize, count: u32, limit: u32) -> Result<(), Error> {
if start >= limit as usize || start as u64 + count as u64 > limit as u64 {
return Err(BasicError::LineRangeOutOfSheet(count, start, limit).into());
}
Ok(())
}
fn check_block_line_range(
start: usize,
cnt: u32,
len: usize,
block_id: logisheets_base::BlockId,
inserting: bool,
) -> Result<(), Error> {
let end = start as u64 + cnt as u64;
let ok = if inserting {
start <= len && end <= len as u64 + cnt as u64
} else {
end <= len as u64
};
if !ok {
return Err(BasicError::LineRangeOutOfBlock(start, cnt, block_id, len).into());
}
Ok(())
}
fn pivot_from(
parts: Option<&crate::block_manager::schema_manager::field_type::PivotSpecParts>,
analyzes: Option<usize>,
block_id: usize,
) -> Result<Option<crate::block_manager::schema_manager::field_type::PivotSpec>, Error> {
use crate::block_manager::schema_manager::field_type::PivotSpec;
let Some(parts) = parts else {
return Ok(None);
};
match analyzes {
None => Err(Error::PayloadError(format!(
"block {block_id} declares a pivot but analyses nothing — a pivot \
needs a source block to aggregate"
))),
Some(src) if src == block_id => Err(Error::PayloadError(format!(
"block {block_id} cannot pivot over itself"
))),
Some(_) => Ok(PivotSpec::from_parts(parts)),
}
}
fn check_block_size(row_cnt: u32, col_cnt: u32) -> Result<(), Error> {
let cells = row_cnt as u64 * col_cnt as u64;
if row_cnt > MAX_ROW_CNT || col_cnt > MAX_COL_CNT || cells > MAX_BLOCK_CELLS {
return Err(BasicError::BlockTooLarge(
row_cnt,
col_cnt,
MAX_ROW_CNT,
MAX_COL_CNT,
MAX_BLOCK_CELLS,
)
.into());
}
Ok(())
}
pub struct NavExecutor {
pub nav: Navigator,
pub row_inserted: Vec<(SheetId, RowId)>,
pub row_removed: Vec<(SheetId, RowId)>,
pub col_inserted: Vec<(SheetId, ColId)>,
pub col_removed: Vec<(SheetId, ColId)>,
}
impl NavExecutor {
pub fn new(nav: Navigator) -> Self {
NavExecutor {
nav,
row_inserted: vec![],
row_removed: vec![],
col_inserted: vec![],
col_removed: vec![],
}
}
pub fn execute<C: NavExecCtx>(
mut self,
ctx: &mut C,
payload: EditPayload,
) -> Result<(Self, bool), Error> {
match payload {
EditPayload::MoveBlock(move_block) => {
let sheet_id = ctx
.fetch_sheet_id_by_index(move_block.sheet_idx)
.map_err(|l| BasicError::SheetIdxExceed(l))?;
let cell_id = self.nav.fetch_cell_id(
&sheet_id,
move_block.new_master_row,
move_block.new_master_col,
)?;
if let CellId::BlockCell(id) = cell_id {
if id.block_id != move_block.id {
return Err(BasicError::CreatingBlockOn(id.block_id).into());
}
}
self.nav.move_block(
&sheet_id,
&move_block.id,
move_block.new_master_row,
move_block.new_master_col,
);
Ok((self, true))
}
EditPayload::MoveBlockLine(p) => {
let sheet_id = ctx
.fetch_sheet_id_by_index(p.sheet_idx)
.map_err(|l| BasicError::SheetIdxExceed(l))?;
let bp = self.nav.get_block_place(&sheet_id, &p.block_id)?.clone();
let len = if p.is_row {
bp.rows.len()
} else {
bp.cols.len()
};
if p.from >= len || p.to >= len {
return Err(BasicError::LineRangeOutOfBlock(
p.from.max(p.to),
1,
p.block_id,
len,
)
.into());
}
let new_bp = bp.move_line(p.from, p.to, p.is_row);
let nav = self.nav.add_block_place(sheet_id, p.block_id, new_bp);
Ok((
NavExecutor {
nav,
row_inserted: self.row_inserted,
row_removed: self.row_removed,
col_inserted: self.col_inserted,
col_removed: self.col_removed,
},
true,
))
}
EditPayload::ReorderBlockLines(p) => {
let sheet_id = ctx
.fetch_sheet_id_by_index(p.sheet_idx)
.map_err(|l| BasicError::SheetIdxExceed(l))?;
let bp = self.nav.get_block_place(&sheet_id, &p.block_id)?.clone();
let len = if p.is_row {
bp.rows.len()
} else {
bp.cols.len()
};
if p.new_order.len() != len || p.new_order.iter().any(|&i| i >= len) {
return Err(BasicError::BadBlockLineOrder(p.block_id, len).into());
}
let new_bp = bp.reorder_lines(&p.new_order, p.is_row);
let nav = self.nav.add_block_place(sheet_id, p.block_id, new_bp);
Ok((
NavExecutor {
nav,
row_inserted: self.row_inserted,
row_removed: self.row_removed,
col_inserted: self.col_inserted,
col_removed: self.col_removed,
},
true,
))
}
EditPayload::RemoveBlock(remove_block) => {
let sheet_id = ctx
.fetch_sheet_id_by_index(remove_block.sheet_idx)
.map_err(|l| BasicError::SheetIdxExceed(l))?;
self.nav.remove_block(&sheet_id, &remove_block.id);
Ok((self, true))
}
EditPayload::CreateBlock(create_block) => {
let sheet_id = ctx
.fetch_sheet_id_by_index(create_block.sheet_idx)
.map_err(|l| BasicError::SheetIdxExceed(l))?;
let cell_id = self.nav.fetch_cell_id(
&sheet_id,
create_block.master_row,
create_block.master_col,
)?;
let cell_id = match cell_id {
CellId::NormalCell(c) => c,
CellId::BlockCell(id) => {
return Err(BasicError::CreatingBlockOn(id.block_id).into());
}
CellId::EphemeralCell(id) => {
return Err(BasicError::CreatingBlockOnEphemeral(id).into());
}
};
let sheet_nav = self.nav.get_sheet_nav_mut(&sheet_id);
if sheet_nav.data.has_block_id(&create_block.id) {
return Err(BasicError::BlockIdHasAlreadyExisted(create_block.id).into());
}
check_block_size(create_block.row_cnt as u32, create_block.col_cnt as u32)?;
let block_place = BlockPlace::new(
cell_id,
create_block.row_cnt as u32,
create_block.col_cnt as u32,
create_block.owner.clone().unwrap_or_default(),
create_block.modify_policy.unwrap_or_default(),
)
.with_pivot(pivot_from(
create_block.pivot.as_ref(),
create_block.analyzes,
create_block.id,
)?)
.with_description(create_block.description.clone().unwrap_or_default())
.with_permissions(create_block.permissions.clone().unwrap_or_default())
.with_analyzes(create_block.analyzes);
let block_id = create_block.id;
sheet_nav.data.blocks.insert(block_id, block_place);
sheet_nav.cache = Default::default();
Ok((self, true))
}
EditPayload::SetBlockDescription(p) => {
let sheet_id = ctx
.fetch_sheet_id_by_index(p.sheet_idx)
.map_err(|l| BasicError::SheetIdxExceed(l))?;
let sheet_nav = self.nav.get_sheet_nav_mut(&sheet_id);
let Some(bp) = sheet_nav.data.blocks.get(&p.block_id) else {
return Err(BasicError::BlockIdNotFound(sheet_id, p.block_id).into());
};
let mut bp = bp.clone();
bp.description = p.description.clone();
sheet_nav.data.blocks.insert(p.block_id, bp);
Ok((self, true))
}
EditPayload::SetBlockAnalyzes(p) => {
let sheet_id = ctx
.fetch_sheet_id_by_index(p.sheet_idx)
.map_err(|l| BasicError::SheetIdxExceed(l))?;
let sheet_nav = self.nav.get_sheet_nav_mut(&sheet_id);
if p.analyzes == Some(p.block_id) {
return Err(Error::PayloadError(format!(
"block {} cannot analyse itself",
p.block_id
)));
}
if let Some(source) = p.analyzes {
if !sheet_nav.data.has_block_id(&source) {
return Err(BasicError::BlockIdNotFound(sheet_id, source).into());
}
}
let Some(bp) = sheet_nav.data.blocks.get(&p.block_id) else {
return Err(BasicError::BlockIdNotFound(sheet_id, p.block_id).into());
};
let pivot = pivot_from(p.pivot.as_ref(), p.analyzes, p.block_id)?;
let bp = bp.clone().with_analyzes(p.analyzes).with_pivot(pivot);
sheet_nav.data.blocks.insert(p.block_id, bp);
Ok((self, true))
}
EditPayload::SetBlockPermissions(p) => {
let sheet_id = ctx
.fetch_sheet_id_by_index(p.sheet_idx)
.map_err(|l| BasicError::SheetIdxExceed(l))?;
let sheet_nav = self.nav.get_sheet_nav_mut(&sheet_id);
let Some(bp) = sheet_nav.data.blocks.get(&p.block_id) else {
return Err(BasicError::BlockIdNotFound(sheet_id, p.block_id).into());
};
let mut bp = bp.clone();
bp.permissions = p.permissions.clone();
if let Some(policy) = p.modify_policy {
bp.modify_policy = policy;
}
sheet_nav.data.blocks.insert(p.block_id, bp);
Ok((self, true))
}
EditPayload::ConvertBlock(p) => {
let sheet_id = ctx
.fetch_sheet_id_by_index(p.sheet_idx)
.map_err(|l| BasicError::SheetIdxExceed(l))?;
let cell_id = self
.nav
.fetch_cell_id(&sheet_id, p.master_row, p.master_col)?;
let cell_id = match cell_id {
CellId::NormalCell(c) => c,
CellId::BlockCell(id) => {
return Err(BasicError::CreatingBlockOn(id.block_id).into());
}
CellId::EphemeralCell(id) => {
return Err(BasicError::CreatingBlockOnEphemeral(id).into());
}
};
let sheet_nav = self.nav.get_sheet_nav_mut(&sheet_id);
if sheet_nav.data.has_block_id(&p.id) {
return Err(BasicError::BlockIdHasAlreadyExisted(p.id).into());
}
check_block_size(p.row_cnt as u32, p.col_cnt as u32)?;
let block_place = BlockPlace::new(
cell_id,
p.row_cnt as u32,
p.col_cnt as u32,
String::new(),
Default::default(),
);
let block_id = p.id;
sheet_nav.data.blocks.insert(block_id, block_place);
sheet_nav.cache = Default::default();
Ok((self, true))
}
EditPayload::ResizeBlock(p) => {
let sheet_id = ctx
.fetch_sheet_id_by_index(p.sheet_idx)
.map_err(|l| BasicError::SheetIdxExceed(l))?;
let sheet_nav = self.nav.get_sheet_nav_mut(&sheet_id);
if !sheet_nav.data.has_block_id(&p.id) {
return Err(BasicError::BlockIdDoesNotExist(p.id).into());
}
let block_place = sheet_nav
.data
.blocks
.get_mut(&p.id)
.ok_or(BasicError::BlockIdNotFound(sheet_id, p.id))?
.clone();
check_block_size(
p.new_row_cnt.unwrap_or(block_place.rows.len()) as u32,
p.new_col_cnt.unwrap_or(block_place.cols.len()) as u32,
)?;
let new_block_place = block_place.resize(p.new_row_cnt, p.new_col_cnt);
sheet_nav.data.blocks.insert(p.id, new_block_place);
sheet_nav.cache = Default::default();
Ok((self, true))
}
EditPayload::CreateSheet(p) => {
let sheet_id = ctx.get_sheet_id(p.new_name)?;
self.nav.create_sheet(sheet_id);
Ok((self, true))
}
EditPayload::DeleteSheet(delete_sheet) => {
let sheet_id = ctx
.fetch_sheet_id_by_index(delete_sheet.idx)
.map_err(|l| BasicError::SheetIdxExceed(l))?;
self.nav.delete_sheet(&sheet_id);
Ok((self, true))
}
EditPayload::InsertCols(insert_cols) => {
let sheet_id = ctx
.fetch_sheet_id_by_index(insert_cols.sheet_idx)
.map_err(|l| BasicError::SheetIdxExceed(l))?;
check_line_range(insert_cols.start, insert_cols.count as u32, MAX_COL_CNT)?;
let sheet_nav = self.nav.get_sheet_nav_mut(&sheet_id).clone();
let (new_sheet_nav, inserted) =
insert_new_cols(sheet_nav, insert_cols.start, insert_cols.count as u32);
let sheet_navs = self.nav.sheet_navs.update(sheet_id, new_sheet_nav);
let mut col_inserted = self.col_inserted;
col_inserted.extend(inserted.into_iter().map(|id| (sheet_id, id)));
let res = NavExecutor {
nav: Navigator { sheet_navs },
row_inserted: self.row_inserted,
row_removed: self.row_removed,
col_inserted,
col_removed: self.col_removed,
};
Ok((res, true))
}
EditPayload::DeleteCols(p) => {
let sheet_id = ctx
.fetch_sheet_id_by_index(p.sheet_idx)
.map_err(|l| BasicError::SheetIdxExceed(l))?;
check_line_range(p.start, p.count as u32, MAX_COL_CNT)?;
let sheet_nav = self.nav.get_sheet_nav_mut(&sheet_id).clone();
let (new_sheet_nav, removed) = delete_cols(sheet_nav, p.start, p.count as u32);
let sheet_navs = self.nav.sheet_navs.update(sheet_id, new_sheet_nav);
let mut col_removed = self.col_removed;
col_removed.extend(removed.into_iter().map(|id| (sheet_id, id)));
let res = NavExecutor {
nav: Navigator { sheet_navs },
row_inserted: self.row_inserted,
row_removed: self.row_removed,
col_inserted: self.col_inserted,
col_removed,
};
Ok((res, true))
}
EditPayload::InsertRows(insert_rows) => {
let sheet_id = ctx
.fetch_sheet_id_by_index(insert_rows.sheet_idx)
.map_err(|l| BasicError::SheetIdxExceed(l))?;
check_line_range(insert_rows.start, insert_rows.count as u32, MAX_ROW_CNT)?;
let sheet_nav = self.nav.get_sheet_nav_mut(&sheet_id).clone();
let (new_sheet_nav, inserted) =
insert_new_rows(sheet_nav, insert_rows.start, insert_rows.count as u32);
let sheet_navs = self.nav.sheet_navs.update(sheet_id, new_sheet_nav);
let mut row_inserted = self.row_inserted;
row_inserted.extend(inserted.into_iter().map(|id| (sheet_id, id)));
let res = NavExecutor {
nav: Navigator { sheet_navs },
row_inserted,
row_removed: self.row_removed,
col_inserted: self.col_inserted,
col_removed: self.col_removed,
};
Ok((res, true))
}
EditPayload::DeleteRows(p) => {
let sheet_id = ctx
.fetch_sheet_id_by_index(p.sheet_idx)
.map_err(|l| BasicError::SheetIdxExceed(l))?;
check_line_range(p.start, p.count as u32, MAX_ROW_CNT)?;
let sheet_nav = self.nav.get_sheet_nav_mut(&sheet_id).clone();
let (new_sheet_nav, removed) = delete_rows(sheet_nav, p.start, p.count as u32);
let sheet_navs = self.nav.sheet_navs.update(sheet_id, new_sheet_nav);
let mut row_removed = self.row_removed;
row_removed.extend(removed.into_iter().map(|id| (sheet_id, id)));
let res = NavExecutor {
nav: Navigator { sheet_navs },
row_inserted: self.row_inserted,
row_removed,
col_inserted: self.col_inserted,
col_removed: self.col_removed,
};
Ok((res, true))
}
EditPayload::InsertColsInBlock(p) => {
let sheet_id = ctx
.fetch_sheet_id_by_index(p.sheet_idx)
.map_err(|l| BasicError::SheetIdxExceed(l))?;
let bp = self.nav.get_block_place(&sheet_id, &p.block_id)?.clone();
check_block_line_range(p.start, p.cnt as u32, bp.cols.len(), p.block_id, true)?;
let new_bp = bp.add_new_cols(p.start, p.cnt as u32);
let nav = self.nav.add_block_place(sheet_id, p.block_id, new_bp);
let result = NavExecutor {
nav,
row_inserted: self.row_inserted,
row_removed: self.row_removed,
col_inserted: self.col_inserted,
col_removed: self.col_removed,
};
Ok((result, true))
}
EditPayload::DeleteColsInBlock(p) => {
let sheet_id = ctx
.fetch_sheet_id_by_index(p.sheet_idx)
.map_err(|l| BasicError::SheetIdxExceed(l))?;
let bp = self.nav.get_block_place(&sheet_id, &p.block_id)?.clone();
check_block_line_range(p.start, p.cnt as u32, bp.cols.len(), p.block_id, false)?;
let new_bp = bp.delete_cols(p.start, p.cnt as u32);
let nav = self.nav.add_block_place(sheet_id, p.block_id, new_bp);
let result = NavExecutor {
nav,
row_inserted: self.row_inserted,
row_removed: self.row_removed,
col_inserted: self.col_inserted,
col_removed: self.col_removed,
};
Ok((result, true))
}
EditPayload::InsertRowsInBlock(p) => {
let sheet_id = ctx
.fetch_sheet_id_by_index(p.sheet_idx)
.map_err(|l| BasicError::SheetIdxExceed(l))?;
let bp = self.nav.get_block_place(&sheet_id, &p.block_id)?.clone();
check_block_line_range(p.start, p.cnt as u32, bp.rows.len(), p.block_id, true)?;
let new_bp = bp.add_new_rows(p.start, p.cnt as u32);
let nav = self.nav.add_block_place(sheet_id, p.block_id, new_bp);
let result = NavExecutor {
nav,
row_inserted: self.row_inserted,
row_removed: self.row_removed,
col_inserted: self.col_inserted,
col_removed: self.col_removed,
};
Ok((result, true))
}
EditPayload::DeleteRowsInBlock(p) => {
let sheet_id = ctx
.fetch_sheet_id_by_index(p.sheet_idx)
.map_err(|l| BasicError::SheetIdxExceed(l))?;
let bp = self.nav.get_block_place(&sheet_id, &p.block_id)?.clone();
check_block_line_range(p.start, p.cnt as u32, bp.rows.len(), p.block_id, false)?;
let new_bp = bp.delete_rows(p.start, p.cnt as u32);
let nav = self.nav.add_block_place(sheet_id, p.block_id, new_bp);
let result = NavExecutor {
nav,
row_inserted: self.row_inserted,
row_removed: self.row_removed,
col_inserted: self.col_inserted,
col_removed: self.col_removed,
};
Ok((result, true))
}
_ => Ok((self, false)),
}
}
}
fn insert_new_rows(sheet_nav: SheetNav, idx: usize, cnt: u32) -> (SheetNav, Vec<RowId>) {
let mut new_id_manager = sheet_nav.id_manager.clone();
let new_ids = new_id_manager.get_row_ids(cnt);
let inserted: Vec<RowId> = new_ids.iter().cloned().collect();
let new_rows = {
let rows = sheet_nav.data.rows.clone();
let row_max = rows.len();
let (mut left, right) = rows.split_at(idx);
left.append(new_ids.clone());
left.append(right);
left.truncate(row_max);
left
};
let sheet_nav = SheetNav {
sheet_id: sheet_nav.sheet_id,
data: sheet_nav.data.update_rows(new_rows),
cache: Default::default(),
id_manager: new_id_manager,
};
(sheet_nav, inserted)
}
fn insert_new_cols(sheet_nav: SheetNav, idx: usize, cnt: u32) -> (SheetNav, Vec<ColId>) {
let mut new_id_manager = sheet_nav.id_manager.clone();
let new_ids = new_id_manager.get_col_ids(cnt);
let inserted: Vec<ColId> = new_ids.iter().cloned().collect();
let new_cols = {
let cols = sheet_nav.data.cols.clone();
let col_max = cols.len();
let (mut left, right) = cols.split_at(idx);
left.append(new_ids.clone());
left.append(right);
left.truncate(col_max);
left
};
let sheet_nav = SheetNav {
sheet_id: sheet_nav.sheet_id,
data: sheet_nav.data.update_cols(new_cols),
cache: Default::default(),
id_manager: new_id_manager,
};
(sheet_nav, inserted)
}
fn delete_rows(sheet_nav: SheetNav, idx: usize, cnt: u32) -> (SheetNav, Vec<RowId>) {
let sheet_id = sheet_nav.sheet_id;
let result = sheet_nav;
let mut new_id_manager = result.id_manager.clone();
let new_ids = new_id_manager.get_row_ids(cnt);
let removed: Vec<RowId>;
let new_rows = {
let rows = result.data.rows.clone();
let removed_cnt = std::cmp::min(rows.len() - idx, cnt as usize);
let (mut left, right) = rows.split_at(idx);
removed = right.iter().take(removed_cnt).cloned().collect();
left.append(right.skip(removed_cnt));
left.append(new_ids.clone());
left
};
let new_blocks = {
let mut old_blocks = result.data.blocks.clone();
old_blocks.iter_mut().for_each(|(_, bp)| {
let Ok((row, col)) = result.get_fetcher().get_norm_cell_idx(&bp.master) else {
return;
};
if row >= idx && row <= idx + cnt as usize - 1 {
let new_row = idx + cnt as usize;
if let Ok(new_master_id) = result.get_fetcher().get_norm_cell_id(new_row, col) {
bp.master = new_master_id;
}
}
});
old_blocks
};
let sheet_nav = SheetNav {
sheet_id,
data: result.data.update_rows(new_rows).update_blocks(new_blocks),
cache: Default::default(),
id_manager: new_id_manager,
};
(sheet_nav, removed)
}
fn delete_cols(sheet_nav: SheetNav, idx: usize, cnt: u32) -> (SheetNav, Vec<ColId>) {
let sheet_id = sheet_nav.sheet_id;
let result = sheet_nav;
let mut new_id_manager = result.id_manager.clone();
let new_ids = new_id_manager.get_col_ids(cnt);
let removed: Vec<ColId>;
let new_cols = {
let cols = result.data.cols.clone();
let removed_cnt = std::cmp::min(cols.len() - idx, cnt as usize);
let (mut left, right) = cols.split_at(idx);
removed = right.iter().take(removed_cnt).cloned().collect();
left.append(right.skip(removed_cnt));
left.append(new_ids.clone());
left
};
let new_blocks = {
let mut old_blocks = result.data.blocks.clone();
old_blocks.iter_mut().for_each(|(_, bp)| {
let Ok((row, col)) = result.get_fetcher().get_norm_cell_idx(&bp.master) else {
return;
};
if col >= idx && col <= idx + cnt as usize - 1 {
let new_col = idx + cnt as usize;
if let Ok(new_master_id) = result.get_fetcher().get_norm_cell_id(row, new_col) {
bp.master = new_master_id;
}
}
});
old_blocks
};
let sheet_nav = SheetNav {
sheet_id,
data: result.data.update_cols(new_cols).update_blocks(new_blocks),
cache: Default::default(),
id_manager: new_id_manager,
};
(sheet_nav, removed)
}
#[cfg(test)]
mod tests {
use imbl::Vector;
use super::{delete_cols, delete_rows};
use crate::navigator::sheet_nav::SheetNav;
#[test]
fn delete_row_test() {
let sheet_nav = SheetNav::init(5, 5, 0);
assert_eq!(&sheet_nav.data.rows, &Vector::from(vec![0, 1, 2, 3, 4]));
let (new_sheet_nav, _) = delete_rows(sheet_nav, 1, 1);
assert_eq!(&new_sheet_nav.data.rows, &Vector::from(vec![0, 2, 3, 4, 5]));
}
#[test]
fn delete_col_test() {
let sheet_nav = SheetNav::init(5, 5, 0);
assert_eq!(&sheet_nav.data.cols, &Vector::from(vec![0, 1, 2, 3, 4]));
let (new_sheet_nav, _) = delete_cols(sheet_nav, 1, 1);
assert_eq!(&new_sheet_nav.data.cols, &Vector::from(vec![0, 2, 3, 4, 5]));
}
}