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