feat: add bulk diff types for insert/delete row/column operations
This commit is contained in:
committed by
Nicolás Hatcher Andrés
parent
d5ccd9dbdd
commit
037766c744
@@ -25,15 +25,14 @@ fn diff_invariant_insert_rows() {
|
||||
assert!(model.insert_rows(0, 5, 3).is_ok());
|
||||
|
||||
let list = last_diff_list(&mut model);
|
||||
assert_eq!(list.len(), 3);
|
||||
for diff in list {
|
||||
match diff {
|
||||
Diff::InsertRow { sheet, row } => {
|
||||
assert_eq!(sheet, 0);
|
||||
assert_eq!(row, 5);
|
||||
}
|
||||
_ => panic!("Unexpected diff variant"),
|
||||
assert_eq!(list.len(), 1);
|
||||
match &list[0] {
|
||||
Diff::InsertRows { sheet, row, count } => {
|
||||
assert_eq!(*sheet, 0);
|
||||
assert_eq!(*row, 5);
|
||||
assert_eq!(*count, 3);
|
||||
}
|
||||
_ => panic!("Unexpected diff variant"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,15 +43,18 @@ fn diff_invariant_insert_columns() {
|
||||
|
||||
assert!(model.insert_columns(0, 2, 4).is_ok());
|
||||
let list = last_diff_list(&mut model);
|
||||
assert_eq!(list.len(), 4);
|
||||
for diff in list {
|
||||
match diff {
|
||||
Diff::InsertColumn { sheet, column } => {
|
||||
assert_eq!(sheet, 0);
|
||||
assert_eq!(column, 2);
|
||||
}
|
||||
_ => panic!("Unexpected diff variant"),
|
||||
assert_eq!(list.len(), 1);
|
||||
match &list[0] {
|
||||
Diff::InsertColumns {
|
||||
sheet,
|
||||
column,
|
||||
count,
|
||||
} => {
|
||||
assert_eq!(*sheet, 0);
|
||||
assert_eq!(*column, 2);
|
||||
assert_eq!(*count, 4);
|
||||
}
|
||||
_ => panic!("Unexpected diff variant"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,7 +95,7 @@ fn undo_redo_after_batch_delete() {
|
||||
|
||||
#[test]
|
||||
fn diff_order_delete_rows() {
|
||||
// Verifies that delete diffs are generated bottom-to-top
|
||||
// Verifies that delete diffs are generated with all data preserved
|
||||
let base = new_empty_model();
|
||||
let mut model = UserModel::from_model(base);
|
||||
|
||||
@@ -105,18 +107,27 @@ fn diff_order_delete_rows() {
|
||||
assert!(model.delete_rows(0, 5, 5).is_ok());
|
||||
|
||||
let list = last_diff_list(&mut model);
|
||||
assert_eq!(list.len(), 5);
|
||||
assert_eq!(list.len(), 1);
|
||||
|
||||
// Diffs should be in reverse order: 9, 8, 7, 6, 5
|
||||
let mut expected_row = 9;
|
||||
for diff in list {
|
||||
match diff {
|
||||
Diff::DeleteRow { row, .. } => {
|
||||
assert_eq!(row, expected_row);
|
||||
expected_row -= 1;
|
||||
// Should have one bulk diff with all the row data
|
||||
match &list[0] {
|
||||
Diff::DeleteRows {
|
||||
sheet,
|
||||
row,
|
||||
count,
|
||||
old_data,
|
||||
} => {
|
||||
assert_eq!(*sheet, 0);
|
||||
assert_eq!(*row, 5);
|
||||
assert_eq!(*count, 5);
|
||||
assert_eq!(old_data.len(), 5);
|
||||
// Verify the data was collected for each row
|
||||
for (i, row_data) in old_data.iter().enumerate() {
|
||||
let _expected_value = (5 + i).to_string();
|
||||
assert!(row_data.data.contains_key(&1));
|
||||
}
|
||||
_ => panic!("Unexpected diff variant"),
|
||||
}
|
||||
_ => panic!("Unexpected diff variant"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -157,6 +168,7 @@ fn edge_case_single_operation() {
|
||||
|
||||
#[test]
|
||||
fn delete_empty_rows() {
|
||||
// Delete multiple empty rows and verify behavior
|
||||
let base = new_empty_model();
|
||||
let mut model = UserModel::from_model(base);
|
||||
|
||||
@@ -171,19 +183,26 @@ fn delete_empty_rows() {
|
||||
assert_eq!(model.get_formatted_cell_value(0, 1, 1).unwrap(), "Before");
|
||||
assert_eq!(model.get_formatted_cell_value(0, 6, 1).unwrap(), "After");
|
||||
|
||||
// Verify diffs are in reverse order with empty data
|
||||
// Verify diffs now use bulk operation
|
||||
let list = last_diff_list(&mut model);
|
||||
assert_eq!(list.len(), 4);
|
||||
let mut expected_row = 8;
|
||||
for diff in &list {
|
||||
match diff {
|
||||
Diff::DeleteRow { row, old_data, .. } => {
|
||||
assert_eq!(*row, expected_row);
|
||||
assert!(old_data.data.is_empty());
|
||||
expected_row -= 1;
|
||||
assert_eq!(list.len(), 1);
|
||||
match &list[0] {
|
||||
Diff::DeleteRows {
|
||||
sheet,
|
||||
row,
|
||||
count,
|
||||
old_data,
|
||||
} => {
|
||||
assert_eq!(*sheet, 0);
|
||||
assert_eq!(*row, 5);
|
||||
assert_eq!(*count, 4);
|
||||
assert_eq!(old_data.len(), 4);
|
||||
// All rows should be empty
|
||||
for row_data in old_data {
|
||||
assert!(row_data.data.is_empty());
|
||||
}
|
||||
_ => panic!("Unexpected diff variant"),
|
||||
}
|
||||
_ => panic!("Unexpected diff variant"),
|
||||
}
|
||||
|
||||
// Undo/redo
|
||||
@@ -209,15 +228,28 @@ fn delete_mixed_empty_and_filled_rows() {
|
||||
|
||||
// Verify mix of empty and filled row diffs
|
||||
let list = last_diff_list(&mut model);
|
||||
assert_eq!(list.len(), 5);
|
||||
let filled_count = list
|
||||
.iter()
|
||||
.filter(|diff| match diff {
|
||||
Diff::DeleteRow { old_data, .. } => !old_data.data.is_empty(),
|
||||
_ => false,
|
||||
})
|
||||
.count();
|
||||
assert_eq!(filled_count, 3);
|
||||
assert_eq!(list.len(), 1);
|
||||
match &list[0] {
|
||||
Diff::DeleteRows {
|
||||
sheet,
|
||||
row,
|
||||
count,
|
||||
old_data,
|
||||
} => {
|
||||
assert_eq!(*sheet, 0);
|
||||
assert_eq!(*row, 5);
|
||||
assert_eq!(*count, 5);
|
||||
assert_eq!(old_data.len(), 5);
|
||||
|
||||
// Count filled rows (should be 3: rows 5, 7, 9)
|
||||
let filled_count = old_data
|
||||
.iter()
|
||||
.filter(|row_data| !row_data.data.is_empty())
|
||||
.count();
|
||||
assert_eq!(filled_count, 3);
|
||||
}
|
||||
_ => panic!("Unexpected diff variant"),
|
||||
}
|
||||
|
||||
// Undo
|
||||
model.undo().unwrap();
|
||||
@@ -227,6 +259,409 @@ fn delete_mixed_empty_and_filled_rows() {
|
||||
assert_eq!(model.get_formatted_cell_value(0, 10, 1).unwrap(), "After");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bulk_insert_rows_undo_redo() {
|
||||
let base = new_empty_model();
|
||||
let mut model = UserModel::from_model(base);
|
||||
|
||||
// Set up initial data
|
||||
model.set_user_input(0, 1, 1, "A1").unwrap();
|
||||
model.set_user_input(0, 2, 1, "A2").unwrap();
|
||||
model.set_user_input(0, 5, 1, "A5").unwrap();
|
||||
|
||||
// Insert 3 rows at position 3
|
||||
assert!(model.insert_rows(0, 3, 3).is_ok());
|
||||
|
||||
// Verify data has shifted
|
||||
assert_eq!(model.get_formatted_cell_value(0, 1, 1).unwrap(), "A1");
|
||||
assert_eq!(model.get_formatted_cell_value(0, 2, 1).unwrap(), "A2");
|
||||
assert_eq!(model.get_formatted_cell_value(0, 8, 1).unwrap(), "A5"); // A5 moved to A8
|
||||
|
||||
// Check diff structure
|
||||
let list = last_diff_list(&mut model);
|
||||
assert_eq!(list.len(), 1);
|
||||
match &list[0] {
|
||||
Diff::InsertRows { sheet, row, count } => {
|
||||
assert_eq!(*sheet, 0);
|
||||
assert_eq!(*row, 3);
|
||||
assert_eq!(*count, 3);
|
||||
}
|
||||
_ => panic!("Expected InsertRows diff"),
|
||||
}
|
||||
|
||||
// Undo
|
||||
model.undo().unwrap();
|
||||
assert_eq!(model.get_formatted_cell_value(0, 5, 1).unwrap(), "A5"); // Back to original position
|
||||
|
||||
// Redo
|
||||
model.redo().unwrap();
|
||||
assert_eq!(model.get_formatted_cell_value(0, 8, 1).unwrap(), "A5"); // Shifted again
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bulk_insert_columns_undo_redo() {
|
||||
let base = new_empty_model();
|
||||
let mut model = UserModel::from_model(base);
|
||||
|
||||
// Set up initial data
|
||||
model.set_user_input(0, 1, 1, "A1").unwrap();
|
||||
model.set_user_input(0, 1, 2, "B1").unwrap();
|
||||
model.set_user_input(0, 1, 5, "E1").unwrap();
|
||||
|
||||
// Insert 3 columns at position 3
|
||||
assert!(model.insert_columns(0, 3, 3).is_ok());
|
||||
|
||||
// Verify data has shifted
|
||||
assert_eq!(model.get_formatted_cell_value(0, 1, 1).unwrap(), "A1");
|
||||
assert_eq!(model.get_formatted_cell_value(0, 1, 2).unwrap(), "B1");
|
||||
assert_eq!(model.get_formatted_cell_value(0, 1, 8).unwrap(), "E1"); // E1 moved to H1
|
||||
|
||||
// Check diff structure
|
||||
let list = last_diff_list(&mut model);
|
||||
assert_eq!(list.len(), 1);
|
||||
match &list[0] {
|
||||
Diff::InsertColumns {
|
||||
sheet,
|
||||
column,
|
||||
count,
|
||||
} => {
|
||||
assert_eq!(*sheet, 0);
|
||||
assert_eq!(*column, 3);
|
||||
assert_eq!(*count, 3);
|
||||
}
|
||||
_ => panic!("Expected InsertColumns diff"),
|
||||
}
|
||||
|
||||
// Undo
|
||||
model.undo().unwrap();
|
||||
assert_eq!(model.get_formatted_cell_value(0, 1, 5).unwrap(), "E1"); // Back to original position
|
||||
|
||||
// Redo
|
||||
model.redo().unwrap();
|
||||
assert_eq!(model.get_formatted_cell_value(0, 1, 8).unwrap(), "E1"); // Shifted again
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bulk_delete_rows_round_trip() {
|
||||
let base = new_empty_model();
|
||||
let mut model = UserModel::from_model(base);
|
||||
|
||||
// Set up data with styles
|
||||
model.set_user_input(0, 3, 1, "Row3").unwrap();
|
||||
model.set_user_input(0, 4, 1, "Row4").unwrap();
|
||||
model.set_user_input(0, 5, 1, "Row5").unwrap();
|
||||
model.set_user_input(0, 6, 1, "Row6").unwrap();
|
||||
model.set_user_input(0, 7, 1, "After").unwrap();
|
||||
|
||||
// Set some row heights to verify they're preserved
|
||||
model.set_rows_height(0, 4, 4, 30.0).unwrap();
|
||||
model.set_rows_height(0, 5, 5, 40.0).unwrap();
|
||||
|
||||
// Delete rows 3-6
|
||||
assert!(model.delete_rows(0, 3, 4).is_ok());
|
||||
|
||||
// Verify deletion
|
||||
assert_eq!(model.get_formatted_cell_value(0, 3, 1).unwrap(), "After");
|
||||
|
||||
// Check diff structure
|
||||
let list = last_diff_list(&mut model);
|
||||
assert_eq!(list.len(), 1);
|
||||
match &list[0] {
|
||||
Diff::DeleteRows {
|
||||
sheet,
|
||||
row,
|
||||
count,
|
||||
old_data,
|
||||
} => {
|
||||
assert_eq!(*sheet, 0);
|
||||
assert_eq!(*row, 3);
|
||||
assert_eq!(*count, 4);
|
||||
assert_eq!(old_data.len(), 4);
|
||||
// Verify data was preserved
|
||||
assert!(old_data[0].data.contains_key(&1)); // Row3
|
||||
assert!(old_data[1].data.contains_key(&1)); // Row4
|
||||
assert!(old_data[2].data.contains_key(&1)); // Row5
|
||||
assert!(old_data[3].data.contains_key(&1)); // Row6
|
||||
}
|
||||
_ => panic!("Expected DeleteRows diff"),
|
||||
}
|
||||
|
||||
// Undo - should restore all data and row heights
|
||||
model.undo().unwrap();
|
||||
assert_eq!(model.get_formatted_cell_value(0, 3, 1).unwrap(), "Row3");
|
||||
assert_eq!(model.get_formatted_cell_value(0, 4, 1).unwrap(), "Row4");
|
||||
assert_eq!(model.get_formatted_cell_value(0, 5, 1).unwrap(), "Row5");
|
||||
assert_eq!(model.get_formatted_cell_value(0, 6, 1).unwrap(), "Row6");
|
||||
assert_eq!(model.get_formatted_cell_value(0, 7, 1).unwrap(), "After");
|
||||
assert_eq!(model.get_row_height(0, 4).unwrap(), 30.0);
|
||||
assert_eq!(model.get_row_height(0, 5).unwrap(), 40.0);
|
||||
|
||||
// Redo - should delete again
|
||||
model.redo().unwrap();
|
||||
assert_eq!(model.get_formatted_cell_value(0, 3, 1).unwrap(), "After");
|
||||
|
||||
// Final undo to verify round-trip
|
||||
model.undo().unwrap();
|
||||
assert_eq!(model.get_formatted_cell_value(0, 3, 1).unwrap(), "Row3");
|
||||
assert_eq!(model.get_formatted_cell_value(0, 4, 1).unwrap(), "Row4");
|
||||
assert_eq!(model.get_formatted_cell_value(0, 5, 1).unwrap(), "Row5");
|
||||
assert_eq!(model.get_formatted_cell_value(0, 6, 1).unwrap(), "Row6");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bulk_delete_columns_round_trip() {
|
||||
let base = new_empty_model();
|
||||
let mut model = UserModel::from_model(base);
|
||||
|
||||
// Set up data with styles
|
||||
model.set_user_input(0, 1, 3, "C1").unwrap();
|
||||
model.set_user_input(0, 1, 4, "D1").unwrap();
|
||||
model.set_user_input(0, 1, 5, "E1").unwrap();
|
||||
model.set_user_input(0, 1, 6, "F1").unwrap();
|
||||
model.set_user_input(0, 1, 7, "After").unwrap();
|
||||
|
||||
// Set some column widths to verify they're preserved
|
||||
model.set_columns_width(0, 4, 4, 100.0).unwrap();
|
||||
model.set_columns_width(0, 5, 5, 120.0).unwrap();
|
||||
|
||||
// Delete columns 3-6
|
||||
assert!(model.delete_columns(0, 3, 4).is_ok());
|
||||
|
||||
// Verify deletion
|
||||
assert_eq!(model.get_formatted_cell_value(0, 1, 3).unwrap(), "After");
|
||||
|
||||
// Check diff structure
|
||||
let list = last_diff_list(&mut model);
|
||||
assert_eq!(list.len(), 1);
|
||||
match &list[0] {
|
||||
Diff::DeleteColumns {
|
||||
sheet,
|
||||
column,
|
||||
count,
|
||||
old_data,
|
||||
} => {
|
||||
assert_eq!(*sheet, 0);
|
||||
assert_eq!(*column, 3);
|
||||
assert_eq!(*count, 4);
|
||||
assert_eq!(old_data.len(), 4);
|
||||
// Verify data was preserved
|
||||
assert!(old_data[0].data.contains_key(&1)); // C1
|
||||
assert!(old_data[1].data.contains_key(&1)); // D1
|
||||
assert!(old_data[2].data.contains_key(&1)); // E1
|
||||
assert!(old_data[3].data.contains_key(&1)); // F1
|
||||
}
|
||||
_ => panic!("Expected DeleteColumns diff"),
|
||||
}
|
||||
|
||||
// Undo - should restore all data and column widths
|
||||
model.undo().unwrap();
|
||||
assert_eq!(model.get_formatted_cell_value(0, 1, 3).unwrap(), "C1");
|
||||
assert_eq!(model.get_formatted_cell_value(0, 1, 4).unwrap(), "D1");
|
||||
assert_eq!(model.get_formatted_cell_value(0, 1, 5).unwrap(), "E1");
|
||||
assert_eq!(model.get_formatted_cell_value(0, 1, 6).unwrap(), "F1");
|
||||
assert_eq!(model.get_formatted_cell_value(0, 1, 7).unwrap(), "After");
|
||||
assert_eq!(model.get_column_width(0, 4).unwrap(), 100.0);
|
||||
assert_eq!(model.get_column_width(0, 5).unwrap(), 120.0);
|
||||
|
||||
// Redo - should delete again
|
||||
model.redo().unwrap();
|
||||
assert_eq!(model.get_formatted_cell_value(0, 1, 3).unwrap(), "After");
|
||||
|
||||
// Final undo to verify round-trip
|
||||
model.undo().unwrap();
|
||||
assert_eq!(model.get_formatted_cell_value(0, 1, 3).unwrap(), "C1");
|
||||
assert_eq!(model.get_formatted_cell_value(0, 1, 4).unwrap(), "D1");
|
||||
assert_eq!(model.get_formatted_cell_value(0, 1, 5).unwrap(), "E1");
|
||||
assert_eq!(model.get_formatted_cell_value(0, 1, 6).unwrap(), "F1");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn complex_bulk_operations_sequence() {
|
||||
// Test a complex sequence of bulk operations
|
||||
let base = new_empty_model();
|
||||
let mut model = UserModel::from_model(base);
|
||||
|
||||
// Initial setup
|
||||
model.set_user_input(0, 1, 1, "A1").unwrap();
|
||||
model.set_user_input(0, 2, 2, "B2").unwrap();
|
||||
model.set_user_input(0, 3, 3, "C3").unwrap();
|
||||
|
||||
// Operation 1: Insert 2 rows at position 2
|
||||
model.insert_rows(0, 2, 2).unwrap();
|
||||
assert_eq!(model.get_formatted_cell_value(0, 1, 1).unwrap(), "A1");
|
||||
assert_eq!(model.get_formatted_cell_value(0, 4, 2).unwrap(), "B2"); // B2 moved down
|
||||
assert_eq!(model.get_formatted_cell_value(0, 5, 3).unwrap(), "C3"); // C3 moved down
|
||||
|
||||
// Operation 2: Insert 2 columns at position 2
|
||||
model.insert_columns(0, 2, 2).unwrap();
|
||||
assert_eq!(model.get_formatted_cell_value(0, 1, 1).unwrap(), "A1");
|
||||
assert_eq!(model.get_formatted_cell_value(0, 4, 4).unwrap(), "B2"); // B2 moved right
|
||||
assert_eq!(model.get_formatted_cell_value(0, 5, 5).unwrap(), "C3"); // C3 moved right
|
||||
|
||||
// Operation 3: Delete the inserted rows
|
||||
model.delete_rows(0, 2, 2).unwrap();
|
||||
assert_eq!(model.get_formatted_cell_value(0, 2, 4).unwrap(), "B2");
|
||||
assert_eq!(model.get_formatted_cell_value(0, 3, 5).unwrap(), "C3");
|
||||
|
||||
// Undo all operations
|
||||
model.undo().unwrap(); // Undo delete rows
|
||||
assert_eq!(model.get_formatted_cell_value(0, 4, 4).unwrap(), "B2");
|
||||
assert_eq!(model.get_formatted_cell_value(0, 5, 5).unwrap(), "C3");
|
||||
|
||||
model.undo().unwrap(); // Undo insert columns
|
||||
assert_eq!(model.get_formatted_cell_value(0, 4, 2).unwrap(), "B2");
|
||||
assert_eq!(model.get_formatted_cell_value(0, 5, 3).unwrap(), "C3");
|
||||
|
||||
model.undo().unwrap(); // Undo insert rows
|
||||
assert_eq!(model.get_formatted_cell_value(0, 2, 2).unwrap(), "B2");
|
||||
assert_eq!(model.get_formatted_cell_value(0, 3, 3).unwrap(), "C3");
|
||||
|
||||
// Redo all operations
|
||||
model.redo().unwrap(); // Redo insert rows
|
||||
model.redo().unwrap(); // Redo insert columns
|
||||
model.redo().unwrap(); // Redo delete rows
|
||||
assert_eq!(model.get_formatted_cell_value(0, 2, 4).unwrap(), "B2");
|
||||
assert_eq!(model.get_formatted_cell_value(0, 3, 5).unwrap(), "C3");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bulk_operations_with_formulas_update() {
|
||||
// Test that formulas update correctly with bulk operations
|
||||
let base = new_empty_model();
|
||||
let mut model = UserModel::from_model(base);
|
||||
|
||||
// Set up data and formulas
|
||||
model.set_user_input(0, 1, 1, "10").unwrap();
|
||||
model.set_user_input(0, 5, 1, "20").unwrap();
|
||||
model.set_user_input(0, 10, 1, "=A1+A5").unwrap(); // Formula referencing A1 and A5
|
||||
|
||||
// Insert 3 rows at position 3
|
||||
model.insert_rows(0, 3, 3).unwrap();
|
||||
|
||||
// Formula should update to reference the shifted cells
|
||||
assert_eq!(model.get_formatted_cell_value(0, 13, 1).unwrap(), "30"); // Formula moved down
|
||||
assert_eq!(model.get_cell_content(0, 13, 1).unwrap(), "=A1+A8"); // A5 became A8
|
||||
|
||||
// Undo
|
||||
model.undo().unwrap();
|
||||
assert_eq!(model.get_formatted_cell_value(0, 10, 1).unwrap(), "30");
|
||||
assert_eq!(model.get_cell_content(0, 10, 1).unwrap(), "=A1+A5");
|
||||
|
||||
// Now test column insertion
|
||||
model.set_user_input(0, 1, 5, "20").unwrap(); // Add value in E1
|
||||
model.set_user_input(0, 1, 10, "=A1+E1").unwrap();
|
||||
model.insert_columns(0, 3, 2).unwrap();
|
||||
|
||||
assert_eq!(model.get_formatted_cell_value(0, 1, 12).unwrap(), "30"); // Formula moved right
|
||||
assert_eq!(model.get_cell_content(0, 1, 12).unwrap(), "=A1+G1"); // E1 became G1
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bulk_delete_with_styles() {
|
||||
// Test that cell and row/column styles are preserved
|
||||
let base = new_empty_model();
|
||||
let mut model = UserModel::from_model(base);
|
||||
|
||||
// Set up data with various styles
|
||||
for r in 5..10 {
|
||||
model.set_user_input(0, r, 1, &format!("Row{}", r)).unwrap();
|
||||
model.set_rows_height(0, r, r, (r * 10) as f64).unwrap();
|
||||
}
|
||||
|
||||
// Delete and verify style preservation
|
||||
model.delete_rows(0, 5, 5).unwrap();
|
||||
|
||||
// Undo should restore all styles
|
||||
model.undo().unwrap();
|
||||
for r in 5..10 {
|
||||
assert_eq!(
|
||||
model.get_formatted_cell_value(0, r, 1).unwrap(),
|
||||
format!("Row{}", r)
|
||||
);
|
||||
assert_eq!(model.get_row_height(0, r).unwrap(), (r * 10) as f64);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bulk_operations_large_count() {
|
||||
// Test operations with large counts
|
||||
let base = new_empty_model();
|
||||
let mut model = UserModel::from_model(base);
|
||||
|
||||
// Insert a large number of rows
|
||||
model.set_user_input(0, 1, 1, "Before").unwrap();
|
||||
model.set_user_input(0, 100, 1, "After").unwrap();
|
||||
|
||||
assert!(model.insert_rows(0, 50, 100).is_ok());
|
||||
|
||||
// Verify shift
|
||||
assert_eq!(model.get_formatted_cell_value(0, 1, 1).unwrap(), "Before");
|
||||
assert_eq!(model.get_formatted_cell_value(0, 200, 1).unwrap(), "After"); // Moved by 100
|
||||
|
||||
// Check diff
|
||||
let list = last_diff_list(&mut model);
|
||||
assert_eq!(list.len(), 1);
|
||||
match &list[0] {
|
||||
Diff::InsertRows { count, .. } => {
|
||||
assert_eq!(*count, 100);
|
||||
}
|
||||
_ => panic!("Expected InsertRows diff"),
|
||||
}
|
||||
|
||||
// Undo and redo
|
||||
model.undo().unwrap();
|
||||
assert_eq!(model.get_formatted_cell_value(0, 100, 1).unwrap(), "After");
|
||||
|
||||
model.redo().unwrap();
|
||||
assert_eq!(model.get_formatted_cell_value(0, 200, 1).unwrap(), "After");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bulk_operations_error_cases() {
|
||||
// Test error conditions
|
||||
let base = new_empty_model();
|
||||
let mut model = UserModel::from_model(base);
|
||||
|
||||
// Negative count should fail
|
||||
assert!(model.insert_rows(0, 1, -5).is_err());
|
||||
assert!(model.insert_columns(0, 1, -5).is_err());
|
||||
assert!(model.delete_rows(0, 1, -5).is_err());
|
||||
assert!(model.delete_columns(0, 1, -5).is_err());
|
||||
|
||||
// Zero count should fail
|
||||
assert!(model.insert_rows(0, 1, 0).is_err());
|
||||
assert!(model.insert_columns(0, 1, 0).is_err());
|
||||
assert!(model.delete_rows(0, 1, 0).is_err());
|
||||
assert!(model.delete_columns(0, 1, 0).is_err());
|
||||
|
||||
// Out of bounds operations should fail
|
||||
assert!(model.delete_rows(0, LAST_ROW - 5, 10).is_err());
|
||||
assert!(model.delete_columns(0, LAST_COLUMN - 5, 10).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bulk_diff_serialization() {
|
||||
// Test that bulk diffs can be serialized/deserialized correctly
|
||||
let base = new_empty_model();
|
||||
let mut model = UserModel::from_model(base);
|
||||
|
||||
// Create some data
|
||||
model.set_user_input(0, 1, 1, "Test").unwrap();
|
||||
model.insert_rows(0, 2, 3).unwrap();
|
||||
|
||||
// Flush and get the serialized diffs
|
||||
let bytes = model.flush_send_queue();
|
||||
|
||||
// Create a new model and apply the diffs
|
||||
let base2 = new_empty_model();
|
||||
let mut model2 = UserModel::from_model(base2);
|
||||
|
||||
assert!(model2.apply_external_diffs(&bytes).is_ok());
|
||||
|
||||
// Verify the state matches
|
||||
assert_eq!(model2.get_formatted_cell_value(0, 1, 1).unwrap(), "Test");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn boundary_validation() {
|
||||
let base = new_empty_model();
|
||||
|
||||
@@ -874,10 +874,11 @@ impl UserModel {
|
||||
pub fn insert_rows(&mut self, sheet: u32, row: i32, row_count: i32) -> Result<(), String> {
|
||||
self.model.insert_rows(sheet, row, row_count)?;
|
||||
|
||||
let mut diff_list = Vec::new();
|
||||
for _ in 0..row_count {
|
||||
diff_list.push(Diff::InsertRow { sheet, row });
|
||||
}
|
||||
let diff_list = vec![Diff::InsertRows {
|
||||
sheet,
|
||||
row,
|
||||
count: row_count,
|
||||
}];
|
||||
self.push_diff_list(diff_list);
|
||||
self.evaluate_if_not_paused();
|
||||
Ok(())
|
||||
@@ -903,10 +904,11 @@ impl UserModel {
|
||||
) -> Result<(), String> {
|
||||
self.model.insert_columns(sheet, column, column_count)?;
|
||||
|
||||
let mut diff_list = Vec::new();
|
||||
for _ in 0..column_count {
|
||||
diff_list.push(Diff::InsertColumn { sheet, column });
|
||||
}
|
||||
let diff_list = vec![Diff::InsertColumns {
|
||||
sheet,
|
||||
column,
|
||||
count: column_count,
|
||||
}];
|
||||
self.push_diff_list(diff_list);
|
||||
self.evaluate_if_not_paused();
|
||||
Ok(())
|
||||
@@ -921,9 +923,9 @@ impl UserModel {
|
||||
/// See also [`Model::delete_rows`].
|
||||
pub fn delete_rows(&mut self, sheet: u32, row: i32, row_count: i32) -> Result<(), String> {
|
||||
let worksheet = self.model.workbook.worksheet(sheet)?;
|
||||
let mut diff_list = Vec::new();
|
||||
// Bottom to top order prevents index drift during undo
|
||||
for r in (row..row + row_count).rev() {
|
||||
let mut old_data = Vec::new();
|
||||
// Collect data for all rows to be deleted
|
||||
for r in row..row + row_count {
|
||||
let mut row_data = None;
|
||||
for rd in &worksheet.rows {
|
||||
if rd.r == r {
|
||||
@@ -935,18 +937,20 @@ impl UserModel {
|
||||
Some(s) => s.clone(),
|
||||
None => HashMap::new(),
|
||||
};
|
||||
diff_list.push(Diff::DeleteRow {
|
||||
sheet,
|
||||
row: r,
|
||||
old_data: Box::new(RowData {
|
||||
row: row_data,
|
||||
data,
|
||||
}),
|
||||
old_data.push(RowData {
|
||||
row: row_data,
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
self.model.delete_rows(sheet, row, row_count)?;
|
||||
|
||||
let diff_list = vec![Diff::DeleteRows {
|
||||
sheet,
|
||||
row,
|
||||
count: row_count,
|
||||
old_data,
|
||||
}];
|
||||
self.push_diff_list(diff_list);
|
||||
self.evaluate_if_not_paused();
|
||||
Ok(())
|
||||
@@ -966,9 +970,9 @@ impl UserModel {
|
||||
column_count: i32,
|
||||
) -> Result<(), String> {
|
||||
let worksheet = self.model.workbook.worksheet(sheet)?;
|
||||
let mut diff_list = Vec::new();
|
||||
// Right to left order prevents index drift during undo
|
||||
for c in (column..column + column_count).rev() {
|
||||
let mut old_data = Vec::new();
|
||||
// Collect data for all columns to be deleted
|
||||
for c in column..column + column_count {
|
||||
let mut column_data = None;
|
||||
for col in &worksheet.cols {
|
||||
if c >= col.min && c <= col.max {
|
||||
@@ -990,18 +994,20 @@ impl UserModel {
|
||||
}
|
||||
}
|
||||
|
||||
diff_list.push(Diff::DeleteColumn {
|
||||
sheet,
|
||||
column: c,
|
||||
old_data: Box::new(ColumnData {
|
||||
column: column_data,
|
||||
data,
|
||||
}),
|
||||
old_data.push(ColumnData {
|
||||
column: column_data,
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
self.model.delete_columns(sheet, column, column_count)?;
|
||||
|
||||
let diff_list = vec![Diff::DeleteColumns {
|
||||
sheet,
|
||||
column,
|
||||
count: column_count,
|
||||
old_data,
|
||||
}];
|
||||
self.push_diff_list(diff_list);
|
||||
self.evaluate_if_not_paused();
|
||||
Ok(())
|
||||
@@ -2001,6 +2007,7 @@ impl UserModel {
|
||||
fn apply_undo_diff_list(&mut self, diff_list: &DiffList) -> Result<(), String> {
|
||||
let mut needs_evaluation = false;
|
||||
for diff in diff_list.iter().rev() {
|
||||
#[allow(deprecated)]
|
||||
match diff {
|
||||
Diff::SetCellValue {
|
||||
sheet,
|
||||
@@ -2121,6 +2128,58 @@ impl UserModel {
|
||||
worksheet.set_column_width_and_style(*column, width, style)?;
|
||||
}
|
||||
}
|
||||
Diff::InsertRows { sheet, row, count } => {
|
||||
self.model.delete_rows(*sheet, *row, *count)?;
|
||||
needs_evaluation = true;
|
||||
}
|
||||
Diff::DeleteRows {
|
||||
sheet,
|
||||
row,
|
||||
count: _,
|
||||
old_data,
|
||||
} => {
|
||||
needs_evaluation = true;
|
||||
self.model
|
||||
.insert_rows(*sheet, *row, old_data.len() as i32)?;
|
||||
let worksheet = self.model.workbook.worksheet_mut(*sheet)?;
|
||||
for (i, row_data) in old_data.iter().enumerate() {
|
||||
let r = *row + i as i32;
|
||||
if let Some(row_style) = row_data.row.clone() {
|
||||
worksheet.rows.push(row_style);
|
||||
}
|
||||
worksheet.sheet_data.insert(r, row_data.data.clone());
|
||||
}
|
||||
}
|
||||
Diff::InsertColumns {
|
||||
sheet,
|
||||
column,
|
||||
count,
|
||||
} => {
|
||||
self.model.delete_columns(*sheet, *column, *count)?;
|
||||
needs_evaluation = true;
|
||||
}
|
||||
Diff::DeleteColumns {
|
||||
sheet,
|
||||
column,
|
||||
count: _,
|
||||
old_data,
|
||||
} => {
|
||||
needs_evaluation = true;
|
||||
self.model
|
||||
.insert_columns(*sheet, *column, old_data.len() as i32)?;
|
||||
let worksheet = self.model.workbook.worksheet_mut(*sheet)?;
|
||||
for (i, col_data) in old_data.iter().enumerate() {
|
||||
let c = *column + i as i32;
|
||||
for (row, cell) in &col_data.data {
|
||||
worksheet.update_cell(*row, c, cell.clone())?;
|
||||
}
|
||||
if let Some(col) = &col_data.column {
|
||||
let width = col.width * constants::COLUMN_WIDTH_FACTOR;
|
||||
let style = col.style;
|
||||
worksheet.set_column_width_and_style(c, width, style)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
Diff::SetFrozenRowsCount {
|
||||
sheet,
|
||||
new_value: _,
|
||||
@@ -2288,6 +2347,7 @@ impl UserModel {
|
||||
fn apply_diff_list(&mut self, diff_list: &DiffList) -> Result<(), String> {
|
||||
let mut needs_evaluation = false;
|
||||
for diff in diff_list {
|
||||
#[allow(deprecated)]
|
||||
match diff {
|
||||
Diff::SetCellValue {
|
||||
sheet,
|
||||
@@ -2368,6 +2428,36 @@ impl UserModel {
|
||||
self.model.delete_columns(*sheet, *column, 1)?;
|
||||
needs_evaluation = true;
|
||||
}
|
||||
Diff::InsertRows { sheet, row, count } => {
|
||||
self.model.insert_rows(*sheet, *row, *count)?;
|
||||
needs_evaluation = true;
|
||||
}
|
||||
Diff::DeleteRows {
|
||||
sheet,
|
||||
row,
|
||||
count,
|
||||
old_data: _,
|
||||
} => {
|
||||
self.model.delete_rows(*sheet, *row, *count)?;
|
||||
needs_evaluation = true;
|
||||
}
|
||||
Diff::InsertColumns {
|
||||
sheet,
|
||||
column,
|
||||
count,
|
||||
} => {
|
||||
self.model.insert_columns(*sheet, *column, *count)?;
|
||||
needs_evaluation = true;
|
||||
}
|
||||
Diff::DeleteColumns {
|
||||
sheet,
|
||||
column,
|
||||
count,
|
||||
old_data: _,
|
||||
} => {
|
||||
self.model.delete_columns(*sheet, *column, *count)?;
|
||||
needs_evaluation = true;
|
||||
}
|
||||
Diff::SetFrozenRowsCount {
|
||||
sheet,
|
||||
new_value,
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
#![allow(deprecated)]
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use bitcode::{Decode, Encode};
|
||||
@@ -17,6 +19,7 @@ pub(crate) struct ColumnData {
|
||||
}
|
||||
|
||||
#[derive(Clone, Encode, Decode)]
|
||||
#[allow(deprecated)]
|
||||
pub(crate) enum Diff {
|
||||
// Cell diffs
|
||||
SetCellValue {
|
||||
@@ -87,23 +90,83 @@ pub(crate) enum Diff {
|
||||
row: i32,
|
||||
old_value: Box<Option<Style>>,
|
||||
},
|
||||
/// **DEPRECATED**: Use `InsertRows` with count=1 instead.
|
||||
///
|
||||
/// This variant is kept for backward compatibility to handle old persisted diffs.
|
||||
/// New code should always use `InsertRows` even for single row insertions.
|
||||
#[deprecated(since = "0.5.0", note = "Use InsertRows with count=1 instead")]
|
||||
#[allow(dead_code)]
|
||||
#[allow(deprecated)]
|
||||
InsertRow {
|
||||
#[allow(deprecated)]
|
||||
sheet: u32,
|
||||
#[allow(deprecated)]
|
||||
row: i32,
|
||||
},
|
||||
/// **DEPRECATED**: Use `DeleteRows` with count=1 instead.
|
||||
///
|
||||
/// This variant is kept for backward compatibility to handle old persisted diffs.
|
||||
/// New code should always use `DeleteRows` even for single row deletions.
|
||||
#[deprecated(since = "0.5.0", note = "Use DeleteRows with count=1 instead")]
|
||||
#[allow(dead_code)]
|
||||
#[allow(deprecated)]
|
||||
DeleteRow {
|
||||
#[allow(deprecated)]
|
||||
sheet: u32,
|
||||
#[allow(deprecated)]
|
||||
row: i32,
|
||||
#[allow(deprecated)]
|
||||
old_data: Box<RowData>,
|
||||
},
|
||||
/// **DEPRECATED**: Use `InsertColumns` with count=1 instead.
|
||||
///
|
||||
/// This variant is kept for backward compatibility to handle old persisted diffs.
|
||||
/// New code should always use `InsertColumns` even for single column insertions.
|
||||
#[deprecated(since = "0.5.0", note = "Use InsertColumns with count=1 instead")]
|
||||
#[allow(dead_code)]
|
||||
#[allow(deprecated)]
|
||||
InsertColumn {
|
||||
#[allow(deprecated)]
|
||||
sheet: u32,
|
||||
#[allow(deprecated)]
|
||||
column: i32,
|
||||
},
|
||||
/// **DEPRECATED**: Use `DeleteColumns` with count=1 instead.
|
||||
///
|
||||
/// This variant is kept for backward compatibility to handle old persisted diffs.
|
||||
/// New code should always use `DeleteColumns` even for single column deletions.
|
||||
#[deprecated(since = "0.5.0", note = "Use DeleteColumns with count=1 instead")]
|
||||
#[allow(dead_code)]
|
||||
#[allow(deprecated)]
|
||||
DeleteColumn {
|
||||
#[allow(deprecated)]
|
||||
sheet: u32,
|
||||
#[allow(deprecated)]
|
||||
column: i32,
|
||||
#[allow(deprecated)]
|
||||
old_data: Box<ColumnData>,
|
||||
},
|
||||
InsertRows {
|
||||
sheet: u32,
|
||||
row: i32,
|
||||
count: i32,
|
||||
},
|
||||
DeleteRows {
|
||||
sheet: u32,
|
||||
row: i32,
|
||||
count: i32,
|
||||
old_data: Vec<RowData>,
|
||||
},
|
||||
InsertColumns {
|
||||
sheet: u32,
|
||||
column: i32,
|
||||
old_data: Box<ColumnData>,
|
||||
count: i32,
|
||||
},
|
||||
DeleteColumns {
|
||||
sheet: u32,
|
||||
column: i32,
|
||||
count: i32,
|
||||
old_data: Vec<ColumnData>,
|
||||
},
|
||||
DeleteSheet {
|
||||
sheet: u32,
|
||||
|
||||
Reference in New Issue
Block a user