diff --git a/base/src/lib.rs b/base/src/lib.rs
index 08fc1072..f441d56f 100644
--- a/base/src/lib.rs
+++ b/base/src/lib.rs
@@ -61,4 +61,6 @@ pub use model::get_milliseconds_since_epoch;
pub use model::Model;
pub use user_model::BorderArea;
pub use user_model::ClipboardData;
+pub use user_model::DiffType;
+pub use user_model::QueueDiffs;
pub use user_model::UserModel;
diff --git a/base/src/types.rs b/base/src/types.rs
index 07b41bc8..74156ef4 100644
--- a/base/src/types.rs
+++ b/base/src/types.rs
@@ -64,7 +64,7 @@ pub struct DefinedName {
/// * state:
/// 18.18.68 ST_SheetState (Sheet Visibility Types)
/// hidden, veryHidden, visible
-#[derive(Encode, Decode, Debug, PartialEq, Eq, Clone)]
+#[derive(Debug, Clone, Encode, Decode, Serialize, PartialEq)]
pub enum SheetState {
Visible,
Hidden,
@@ -84,7 +84,7 @@ impl Display for SheetState {
/// Represents the state of the worksheet as seen by the user. This includes
/// details such as the currently selected cell, the visible range, and the
/// position of the viewport.
-#[derive(Encode, Decode, Debug, PartialEq, Clone)]
+#[derive(Encode, Decode, Debug, PartialEq, Clone, Serialize)]
pub struct WorksheetView {
/// The row index of the currently selected cell.
pub row: i32,
@@ -99,13 +99,13 @@ pub struct WorksheetView {
}
/// Internal representation of a worksheet Excel object
-#[derive(Encode, Decode, Debug, PartialEq, Clone)]
+#[derive(Debug, Clone, Encode, Decode, Serialize, PartialEq)]
pub struct Worksheet {
pub dimension: String,
pub cols: Vec
,
pub rows: Vec,
pub name: String,
- pub sheet_data: SheetData,
+ pub sheet_data: HashMap>,
pub shared_formulas: Vec,
pub sheet_id: u32,
pub state: SheetState,
@@ -124,7 +124,7 @@ pub struct Worksheet {
pub type SheetData = HashMap>;
// ECMA-376-1:2016 section 18.3.1.73
-#[derive(Encode, Decode, Debug, PartialEq, Clone)]
+#[derive(Debug, Clone, Encode, Decode, Serialize, PartialEq)]
pub struct Row {
/// Row index
pub r: i32,
@@ -136,7 +136,7 @@ pub struct Row {
}
// ECMA-376-1:2016 section 18.3.1.13
-#[derive(Encode, Decode, Debug, PartialEq, Clone)]
+#[derive(Debug, Clone, Encode, Decode, Serialize, PartialEq)]
pub struct Col {
// Column definitions are defined on ranges, unlike rows which store unique, per-row entries.
/// First column affected by this record. Settings apply to column in \[min, max\] range.
@@ -159,7 +159,7 @@ pub enum CellType {
CompoundData = 128,
}
-#[derive(Encode, Decode, Debug, Clone, PartialEq)]
+#[derive(Encode, Decode, Debug, Clone, PartialEq, Serialize)]
pub enum Cell {
EmptyCell {
s: i32,
@@ -226,7 +226,7 @@ impl Default for Cell {
}
}
-#[derive(Encode, Decode, Debug, PartialEq, Eq, Clone)]
+#[derive(Encode, Decode, Debug, PartialEq, Eq, Clone, Serialize)]
pub struct Comment {
pub text: String,
pub author_name: String,
diff --git a/base/src/user_model/common.rs b/base/src/user_model/common.rs
index 0b4b172d..f4594405 100644
--- a/base/src/user_model/common.rs
+++ b/base/src/user_model/common.rs
@@ -1868,7 +1868,6 @@ impl UserModel {
self.evaluate_if_not_paused();
Ok(())
}
-
// **** Private methods ****** //
pub(crate) fn push_diff_list(&mut self, diff_list: DiffList) {
@@ -2372,6 +2371,16 @@ impl UserModel {
}
Ok(())
}
+
+ /// Returns the current send queue as a vector of QueueDiffs without removing the diffs.
+ ///
+ /// This is used to inspect recent changes without affecting the queue.
+ ///
+ /// See also:
+ /// * [UserModel::flush_send_queue]
+ pub fn get_recent_diffs(&self) -> Vec {
+ self.send_queue.clone()
+ }
}
#[cfg(test)]
diff --git a/base/src/user_model/history.rs b/base/src/user_model/history.rs
index 53268e12..4cffb672 100644
--- a/base/src/user_model/history.rs
+++ b/base/src/user_model/history.rs
@@ -1,23 +1,33 @@
use std::collections::HashMap;
use bitcode::{Decode, Encode};
+use serde::Serialize;
use crate::types::{Cell, Col, Row, SheetState, Style, Worksheet};
-#[derive(Clone, Encode, Decode)]
-pub(crate) struct RowData {
+#[derive(Clone, Encode, Decode, Serialize)]
+pub struct RowData {
pub(crate) row: Option,
pub(crate) data: HashMap,
}
-#[derive(Clone, Encode, Decode)]
-pub(crate) struct ColumnData {
+#[derive(Clone, Encode, Decode, Serialize)]
+pub struct ColumnData {
pub(crate) column: Option,
pub(crate) data: HashMap,
}
-#[derive(Clone, Encode, Decode)]
-pub(crate) enum Diff {
+/// Represents the type of a diff operation
+#[derive(Clone, Encode, Decode, Serialize)]
+pub enum DiffType {
+ /// An undo operation
+ Undo,
+ /// A redo operation
+ Redo,
+}
+
+#[derive(Clone, Encode, Decode, Serialize)]
+pub enum Diff {
// Cell diffs
SetCellValue {
sheet: u32,
@@ -199,14 +209,12 @@ impl History {
}
}
-#[derive(Clone, Encode, Decode)]
-pub enum DiffType {
- Undo,
- Redo,
-}
-
-#[derive(Clone, Encode, Decode)]
+/// A collection of diffs that can be applied to a model.
+/// This represents a single operation that can be undone or redone.
+#[derive(Clone, Encode, Decode, Serialize)]
pub struct QueueDiffs {
+ /// The type of operation this represents (Undo or Redo)
pub r#type: DiffType,
+ /// The list of individual diffs that make up this operation
pub list: DiffList,
}
diff --git a/base/src/user_model/mod.rs b/base/src/user_model/mod.rs
index 06cd85c4..01584d76 100644
--- a/base/src/user_model/mod.rs
+++ b/base/src/user_model/mod.rs
@@ -13,3 +13,5 @@ pub use ui::SelectedView;
pub use common::BorderArea;
pub use common::ClipboardData;
+pub use history::DiffType;
+pub use history::QueueDiffs;
diff --git a/bindings/nodejs/src/user_model.rs b/bindings/nodejs/src/user_model.rs
index b1ebcb9f..9d8f6382 100644
--- a/bindings/nodejs/src/user_model.rs
+++ b/bindings/nodejs/src/user_model.rs
@@ -651,4 +651,12 @@ impl UserModel {
.delete_defined_name(&name, scope)
.map_err(|e| to_js_error(e.to_string()))
}
+
+ #[napi(js_name = "getRecentDiffs")]
+ pub fn get_recent_diffs(&self, env: Env) -> Result {
+ let diffs = self.model.get_recent_diffs();
+ env
+ .to_js_value(&diffs)
+ .map_err(|e| to_js_error(e.to_string()))
+ }
}
diff --git a/bindings/wasm/fix_types.py b/bindings/wasm/fix_types.py
index fd466ee3..92f8366e 100644
--- a/bindings/wasm/fix_types.py
+++ b/bindings/wasm/fix_types.py
@@ -201,6 +201,20 @@
getDefinedNameList(): DefinedName[];
"""
+get_recent_diffs = r"""
+/**
+* @returns {any}
+*/
+ getRecentDiffs(): any;
+"""
+
+get_recent_diffs_types = r"""
+/**
+* @returns {QueueDiffs[]}
+*/
+ getRecentDiffs(): QueueDiffs[];
+"""
+
def fix_types(text):
text = text.replace(get_tokens_str, get_tokens_str_types)
text = text.replace(update_style_str, update_style_str_types)
@@ -215,12 +229,16 @@ def fix_types(text):
text = text.replace(clipboard, clipboard_types)
text = text.replace(paste_from_clipboard, paste_from_clipboard_types)
text = text.replace(defined_name_list, defined_name_list_types)
+ text = text.replace(get_recent_diffs, get_recent_diffs_types)
with open("types.ts") as f:
types_str = f.read()
header_types = "{}\n\n{}".format(header, types_str)
text = text.replace(header, header_types)
if text.find("any") != -1:
print("There are 'unfixed' types. Please check.")
+ for i, line in enumerate(text.splitlines()):
+ if 'any' in line:
+ print(f"Line {i+1}: {line}")
exit(1)
return text
diff --git a/bindings/wasm/src/lib.rs b/bindings/wasm/src/lib.rs
index 5767a2ed..96cfa270 100644
--- a/bindings/wasm/src/lib.rs
+++ b/bindings/wasm/src/lib.rs
@@ -672,4 +672,10 @@ impl Model {
.delete_defined_name(name, scope)
.map_err(|e| to_js_error(e.to_string()))
}
+
+ #[wasm_bindgen(js_name = "getRecentDiffs")]
+ pub fn get_recent_diffs(&self) -> JsValue {
+ serde_wasm_bindgen::to_value(&self.model.get_recent_diffs())
+ .unwrap_or_else(|_| JsValue::undefined())
+ }
}
diff --git a/bindings/wasm/tests/test.mjs b/bindings/wasm/tests/test.mjs
index 37466996..5dc9b4eb 100644
--- a/bindings/wasm/tests/test.mjs
+++ b/bindings/wasm/tests/test.mjs
@@ -130,5 +130,111 @@ test("autofill", () => {
assert.strictEqual(result, "23");
});
+test('getRecentDiffs returns recent diffs without modifying queue', () => {
+ const model = new Model('Workbook1', 'en', 'UTC');
+ // Perform some actions to generate diffs
+ model.setUserInput(0, 1, 1, "42");
+ model.setUserInput(0, 1, 2, "=A1*2");
+
+ // Get recent diffs
+ const diffs = model.getRecentDiffs();
+ assert.strictEqual(diffs.length > 0, true, 'Diffs array should not be empty after actions');
+
+ // Check structure of diffs - regular operations are marked as "Redo" type
+ const firstDiff = diffs[0];
+ assert.strictEqual(firstDiff.type, 'Redo', 'Regular operations should be of type Redo');
+ assert.strictEqual(Array.isArray(firstDiff.list), true, 'Diff entry should have a list of diffs');
+ assert.strictEqual(firstDiff.list.length > 0, true, 'Diff list should not be empty');
+
+ // Look for SetCellValue diff in any of the diff entries
+ let foundSetCellValue = false;
+ for (const diffEntry of diffs) {
+ const setCellDiff = diffEntry.list.find(d => d.SetCellValue && d.SetCellValue.row === 1 && d.SetCellValue.column === 1);
+ if (setCellDiff) {
+ assert.strictEqual(setCellDiff.SetCellValue.new_value, '42', 'New value for A1 should be 42');
+ foundSetCellValue = true;
+ break;
+ }
+ }
+ assert.ok(foundSetCellValue, 'Should find a SetCellValue diff for cell A1 somewhere in the diffs');
+
+ // Verify queue is not modified by checking again
+ const diffsAgain = model.getRecentDiffs();
+ assert.strictEqual(diffsAgain.length, diffs.length, 'Queue length should remain the same after multiple calls');
+ assert.deepStrictEqual(diffsAgain, diffs, 'Queue contents should remain unchanged after multiple calls');
+});
+
+test('getRecentDiffs captures style changes', () => {
+ const model = new Model('Workbook1', 'en', 'UTC');
+ // Perform a style change
+ model.updateRangeStyle({ sheet: 0, row: 1, column: 1, width: 1, height: 1 }, 'font.b', 'true');
+
+ // Get recent diffs
+ const diffs = model.getRecentDiffs();
+ assert.strictEqual(diffs.length > 0, true, 'Diffs array should not be empty after style change');
+
+ // Look for SetCellStyle diff in any of the diff entries
+ let foundStyleDiff = false;
+ for (const diffEntry of diffs) {
+ const styleDiff = diffEntry.list.find(d => d.SetCellStyle);
+ if (styleDiff) {
+ assert.strictEqual(styleDiff.SetCellStyle.sheet, 0, 'Sheet index should be 0');
+ assert.strictEqual(styleDiff.SetCellStyle.row, 1, 'Row should be 1');
+ assert.strictEqual(styleDiff.SetCellStyle.column, 1, 'Column should be 1');
+ assert.ok(styleDiff.SetCellStyle.new_value.font.b, 'New style should have bold set to true');
+ foundStyleDiff = true;
+ break;
+ }
+ }
+ assert.ok(foundStyleDiff, 'Should find a SetCellStyle diff after style update');
+});
+
+test('getRecentDiffs captures undo and redo diffs', () => {
+ const model = new Model('Workbook1', 'en', 'UTC');
+ // Perform an action and undo it
+ model.setUserInput(0, 1, 1, "100");
+ model.undo();
+
+ // Get recent diffs
+ const diffs = model.getRecentDiffs();
+ assert.strictEqual(diffs.length > 0, true, 'Diffs array should not be empty after undo');
+
+ // Check for Undo type in diffs
+ const undoDiff = diffs.find(d => d.type === 'Undo');
+ assert.ok(undoDiff, 'Should find an Undo diff entry after undo operation');
+ assert.strictEqual(undoDiff.list.length > 0, true, 'Undo diff list should not be empty');
+
+ // Redo the action
+ model.redo();
+ const diffsAfterRedo = model.getRecentDiffs();
+ const redoDiff = diffsAfterRedo.find(d => d.type === 'Redo');
+ assert.ok(redoDiff, 'Should find a Redo diff entry after redo operation');
+});
+
+test('getRecentDiffs captures setCellValue diff', () => {
+ const model = new Model('Workbook1', 'en', 'UTC');
+ // Set a cell value to generate a SetCellValue diff
+ model.setUserInput(0, 2, 3, "99");
+
+ // Get recent diffs
+ const diffs = model.getRecentDiffs();
+ assert.strictEqual(diffs.length > 0, true, 'Diffs array should not be empty after setting cell value');
+
+ // Look for SetCellValue diff in any of the diff entries
+ let foundSetCellDiff = false;
+ for (const diffEntry of diffs) {
+ const setCellDiff = diffEntry.list.find(d => d.SetCellValue);
+ if (setCellDiff) {
+ assert.strictEqual(setCellDiff.SetCellValue.sheet, 0, 'Sheet index should be 0');
+ assert.strictEqual(setCellDiff.SetCellValue.row, 2, 'Row should be 2');
+ assert.strictEqual(setCellDiff.SetCellValue.column, 3, 'Column should be 3');
+ assert.strictEqual(setCellDiff.SetCellValue.new_value, '99', 'New value should be 99');
+ foundSetCellDiff = true;
+ break;
+ }
+ }
+ assert.ok(foundSetCellDiff, 'Should find a SetCellValue diff after setting cell value');
+});
+
diff --git a/bindings/wasm/types.ts b/bindings/wasm/types.ts
index 7af55b8c..c9b21b65 100644
--- a/bindings/wasm/types.ts
+++ b/bindings/wasm/types.ts
@@ -209,6 +209,14 @@ export interface SelectedView {
left_column: number;
}
+export interface WorksheetView {
+ row: number;
+ column: number;
+ range: [number, number, number, number];
+ top_row: number;
+ left_column: number;
+}
+
// type ClipboardData = {
// [row: number]: {
// [column: number]: ClipboardCell;
@@ -233,4 +241,282 @@ export interface DefinedName {
name: string;
scope?: number;
formula: string;
+}
+
+// Types for Diffs and QueueDiffs used in getRecentDiffs
+export interface Cell {
+ type: string; // e.g., "NumberCell", "SharedString", "BooleanCell"
+ v?: number | boolean | string; // value, if applicable
+ s: number; // style index
+ f?: number; // formula index, if applicable
+ ei?: string; // error type, if applicable
+ o?: string; // origin, if applicable
+ m?: string; // error message, if applicable
+}
+
+export interface Style {
+ read_only?: boolean;
+ quote_prefix: boolean;
+ fill: {
+ pattern_type: string;
+ fg_color?: string;
+ bg_color?: string;
+ };
+ font: {
+ u: boolean;
+ b: boolean;
+ i: boolean;
+ strike: boolean;
+ sz: number;
+ color?: string;
+ name: string;
+ family: number;
+ scheme: string;
+ };
+ border: {
+ diagonal_up?: boolean;
+ diagonal_down?: boolean;
+ left?: { style: string; color?: string };
+ right?: { style: string; color?: string };
+ top?: { style: string; color?: string };
+ bottom?: { style: string; color?: string };
+ diagonal?: { style: string; color?: string };
+ };
+ num_fmt: string;
+ alignment?: {
+ horizontal: HorizontalAlignment;
+ vertical: VerticalAlignment;
+ wrap_text: boolean;
+ };
+}
+
+export interface RowData {
+ row?: { r: number; height: number; custom_format: boolean; custom_height: boolean; s: number; hidden: boolean };
+ data: Record;
+}
+
+export interface ColumnData {
+ column?: { min: number; max: number; width: number; custom_width: boolean; style?: number };
+ data: Record;
+}
+
+export interface Worksheet {
+ dimension: string;
+ cols: Array<{ min: number; max: number; width: number; custom_width: boolean; style?: number }>;
+ rows: Array<{ r: number; height: number; custom_format: boolean; custom_height: boolean; s: number; hidden: boolean }>;
+ name: string;
+ sheet_data: Record>;
+ shared_formulas: string[];
+ sheet_id: number;
+ state: string; // e.g., "Visible", "Hidden", "VeryHidden"
+ color?: string;
+ merge_cells: string[];
+ comments: Array<{ text: string; author_name: string; author_id?: string; cell_ref: string }>;
+ frozen_rows: number;
+ frozen_columns: number;
+ views: Record;
+ show_grid_lines: boolean;
+}
+
+// Individual Diff type interfaces for better developer experience
+export interface SetCellValueDiff {
+ sheet: number;
+ row: number;
+ column: number;
+ new_value: string;
+ old_value?: Cell | null;
+}
+
+export interface CellClearContentsDiff {
+ sheet: number;
+ row: number;
+ column: number;
+ old_value?: Cell | null;
+}
+
+export interface CellClearAllDiff {
+ sheet: number;
+ row: number;
+ column: number;
+ old_value?: Cell | null;
+ old_style: Style;
+}
+
+export interface CellClearFormattingDiff {
+ sheet: number;
+ row: number;
+ column: number
+ old_style?: Style | null;
+}
+
+export interface SetCellStyleDiff {
+ sheet: number;
+ row: number;
+ column: number;
+ old_value?: Style | null;
+ new_value: Style;
+}
+
+export interface SetColumnWidthDiff {
+ sheet: number;
+ column: number;
+ new_value: number;
+ old_value: number;
+}
+
+export interface SetRowHeightDiff {
+ sheet: number;
+ row: number;
+ new_value: number;
+ old_value: number;
+}
+
+export interface SetColumnStyleDiff {
+ sheet: number;
+ column: number;
+ old_value?: Style | null;
+ new_value: Style;
+}
+
+export interface SetRowStyleDiff {
+ sheet: number;
+ row: number;
+ old_value?: Style | null;
+ new_value: Style;
+}
+
+export interface DeleteColumnStyleDiff {
+ sheet: number;
+ column: number;
+ old_value?: Style | null;
+}
+
+export interface DeleteRowStyleDiff {
+ sheet: number;
+ row: number;
+ old_value?: Style | null;
+}
+
+export interface InsertRowDiff {
+ sheet: number;
+ row: number;
+}
+
+export interface DeleteRowDiff {
+ sheet: number;
+ row: number;
+ old_data: RowData;
+}
+
+export interface InsertColumnDiff {
+ sheet: number;
+ column: number;
+}
+
+export interface DeleteColumnDiff {
+ sheet: number;
+ column: number;
+ old_data: ColumnData;
+}
+
+export interface DeleteSheetDiff {
+ sheet: number;
+ old_data: Worksheet;
+}
+
+export interface SetFrozenRowsCountDiff {
+ sheet: number;
+ new_value: number;
+ old_value: number;
+}
+
+export interface SetFrozenColumnsCountDiff {
+ sheet: number;
+ new_value: number;
+ old_value: number;
+}
+
+export interface NewSheetDiff {
+ index: number;
+ name: string;
+}
+
+export interface RenameSheetDiff {
+ index: number;
+ old_value: string;
+ new_value: string;
+}
+
+export interface SetSheetColorDiff {
+ index: number;
+ old_value: string;
+ new_value: string;
+}
+
+export interface SetSheetStateDiff {
+ index: number;
+ old_value: string;
+ new_value: string;
+}
+
+export interface SetShowGridLinesDiff {
+ sheet: number;
+ old_value: boolean;
+ new_value: boolean;
+}
+
+export interface CreateDefinedNameDiff {
+ name: string;
+ scope?: number;
+ value: string;
+}
+
+export interface DeleteDefinedNameDiff {
+ name: string;
+ scope?: number;
+ old_value: string;
+}
+
+export interface UpdateDefinedNameDiff {
+ name: string;
+ scope?: number;
+ old_formula: string;
+ new_name: string;
+ new_scope?: number;
+ new_formula: string;
+}
+
+// Union type for all Diff variants - these are serialized as tagged enums with variant names as keys
+export type Diff =
+ | { SetCellValue: SetCellValueDiff }
+ | { CellClearContents: CellClearContentsDiff }
+ | { CellClearAll: CellClearAllDiff }
+ | { CellClearFormatting: CellClearFormattingDiff }
+ | { SetCellStyle: SetCellStyleDiff }
+ | { SetColumnWidth: SetColumnWidthDiff }
+ | { SetRowHeight: SetRowHeightDiff }
+ | { SetColumnStyle: SetColumnStyleDiff }
+ | { SetRowStyle: SetRowStyleDiff }
+ | { DeleteColumnStyle: DeleteColumnStyleDiff }
+ | { DeleteRowStyle: DeleteRowStyleDiff }
+ | { InsertRow: InsertRowDiff }
+ | { DeleteRow: DeleteRowDiff }
+ | { InsertColumn: InsertColumnDiff }
+ | { DeleteColumn: DeleteColumnDiff }
+ | { DeleteSheet: DeleteSheetDiff }
+ | { SetFrozenRowsCount: SetFrozenRowsCountDiff }
+ | { SetFrozenColumnsCount: SetFrozenColumnsCountDiff }
+ | { NewSheet: NewSheetDiff }
+ | { RenameSheet: RenameSheetDiff }
+ | { SetSheetColor: SetSheetColorDiff }
+ | { SetSheetState: SetSheetStateDiff }
+ | { SetShowGridLines: SetShowGridLinesDiff }
+ | { CreateDefinedName: CreateDefinedNameDiff }
+ | { DeleteDefinedName: DeleteDefinedNameDiff }
+ | { UpdateDefinedName: UpdateDefinedNameDiff };
+
+// Interface for QueueDiffs
+export interface QueueDiffs {
+ type: "Undo" | "Redo";
+ list: Diff[];
}
\ No newline at end of file