@veltra/sheet 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (137) hide show
  1. package/dist/core/address.d.ts +60 -0
  2. package/dist/core/address.js +142 -0
  3. package/dist/core/address.js.map +1 -0
  4. package/dist/core/cell-store.d.ts +61 -0
  5. package/dist/core/cell-store.js +135 -0
  6. package/dist/core/cell-store.js.map +1 -0
  7. package/dist/core/command/default-registry.d.ts +10 -0
  8. package/dist/core/command/default-registry.js +18 -0
  9. package/dist/core/command/default-registry.js.map +1 -0
  10. package/dist/core/command/history.d.ts +64 -0
  11. package/dist/core/command/history.js +127 -0
  12. package/dist/core/command/history.js.map +1 -0
  13. package/dist/core/command/merge-cells.d.ts +21 -0
  14. package/dist/core/command/merge-cells.js +74 -0
  15. package/dist/core/command/merge-cells.js.map +1 -0
  16. package/dist/core/command/registry.d.ts +17 -0
  17. package/dist/core/command/registry.js +26 -0
  18. package/dist/core/command/registry.js.map +1 -0
  19. package/dist/core/command/set-cell-formula.d.ts +17 -0
  20. package/dist/core/command/set-cell-formula.js +29 -0
  21. package/dist/core/command/set-cell-formula.js.map +1 -0
  22. package/dist/core/command/set-cell-value.d.ts +22 -0
  23. package/dist/core/command/set-cell-value.js +35 -0
  24. package/dist/core/command/set-cell-value.js.map +1 -0
  25. package/dist/core/command/types.d.ts +68 -0
  26. package/dist/core/events.js +29 -0
  27. package/dist/core/events.js.map +1 -0
  28. package/dist/core/formula/ast.d.ts +58 -0
  29. package/dist/core/formula/ast.js +38 -0
  30. package/dist/core/formula/ast.js.map +1 -0
  31. package/dist/core/formula/dependency-graph.d.ts +70 -0
  32. package/dist/core/formula/dependency-graph.js +269 -0
  33. package/dist/core/formula/dependency-graph.js.map +1 -0
  34. package/dist/core/formula/errors.d.ts +25 -0
  35. package/dist/core/formula/errors.js +36 -0
  36. package/dist/core/formula/errors.js.map +1 -0
  37. package/dist/core/formula/evaluator.d.ts +41 -0
  38. package/dist/core/formula/evaluator.js +154 -0
  39. package/dist/core/formula/evaluator.js.map +1 -0
  40. package/dist/core/formula/functions.d.ts +30 -0
  41. package/dist/core/formula/functions.js +209 -0
  42. package/dist/core/formula/functions.js.map +1 -0
  43. package/dist/core/formula/parser.d.ts +7 -0
  44. package/dist/core/formula/parser.js +208 -0
  45. package/dist/core/formula/parser.js.map +1 -0
  46. package/dist/core/formula/tokenizer.d.ts +37 -0
  47. package/dist/core/formula/tokenizer.js +146 -0
  48. package/dist/core/formula/tokenizer.js.map +1 -0
  49. package/dist/core/merge-manager.d.ts +64 -0
  50. package/dist/core/merge-manager.js +111 -0
  51. package/dist/core/merge-manager.js.map +1 -0
  52. package/dist/core/selection.d.ts +31 -0
  53. package/dist/core/selection.js +62 -0
  54. package/dist/core/selection.js.map +1 -0
  55. package/dist/core/sheet.d.ts +105 -0
  56. package/dist/core/sheet.js +202 -0
  57. package/dist/core/sheet.js.map +1 -0
  58. package/dist/core/workbook.d.ts +43 -0
  59. package/dist/core/workbook.js +77 -0
  60. package/dist/core/workbook.js.map +1 -0
  61. package/dist/grid/sheet-grid.d.ts +59 -0
  62. package/dist/grid/sheet-grid.js +230 -0
  63. package/dist/grid/sheet-grid.js.map +1 -0
  64. package/dist/index.d.ts +28 -0
  65. package/dist/index.js +25 -0
  66. package/dist/tools/builtin.js +72 -0
  67. package/dist/tools/builtin.js.map +1 -0
  68. package/dist/tools/context.d.ts +57 -0
  69. package/dist/tools/context.js +43 -0
  70. package/dist/tools/context.js.map +1 -0
  71. package/dist/tools/registry.d.ts +68 -0
  72. package/dist/tools/registry.js +77 -0
  73. package/dist/tools/registry.js.map +1 -0
  74. package/dist/types/index.d.ts +2 -0
  75. package/dist/types/sheet.d.ts +43 -0
  76. package/dist/vue/index.d.ts +2 -0
  77. package/dist/vue/sheet.js +185 -0
  78. package/dist/vue/sheet.js.map +1 -0
  79. package/dist/vue/sheet.vue.d.ts +24 -0
  80. package/dist/vue/style.d.ts +1 -0
  81. package/dist/vue/style.js +1 -0
  82. package/dist/vue/style2.css +93 -0
  83. package/package.json +37 -0
  84. package/src/core/__test__/address.test.ts +113 -0
  85. package/src/core/__test__/cell-store.test.ts +97 -0
  86. package/src/core/__test__/command.test.ts +377 -0
  87. package/src/core/__test__/formula-adversarial.test.ts +265 -0
  88. package/src/core/__test__/formula-engine.test.ts +470 -0
  89. package/src/core/__test__/formula-functions.test.ts +178 -0
  90. package/src/core/__test__/formula-parser.test.ts +160 -0
  91. package/src/core/__test__/history.test.ts +225 -0
  92. package/src/core/__test__/merge-manager.test.ts +106 -0
  93. package/src/core/__test__/sheet.test.ts +142 -0
  94. package/src/core/address.ts +166 -0
  95. package/src/core/cell-store.ts +173 -0
  96. package/src/core/command/default-registry.ts +15 -0
  97. package/src/core/command/history.ts +167 -0
  98. package/src/core/command/index.ts +7 -0
  99. package/src/core/command/merge-cells.ts +74 -0
  100. package/src/core/command/registry.ts +30 -0
  101. package/src/core/command/set-cell-formula.ts +29 -0
  102. package/src/core/command/set-cell-value.ts +38 -0
  103. package/src/core/command/types.ts +73 -0
  104. package/src/core/events.ts +37 -0
  105. package/src/core/formula/ast.ts +66 -0
  106. package/src/core/formula/dependency-graph.ts +329 -0
  107. package/src/core/formula/errors.ts +41 -0
  108. package/src/core/formula/evaluator.ts +219 -0
  109. package/src/core/formula/functions.ts +266 -0
  110. package/src/core/formula/index.ts +7 -0
  111. package/src/core/formula/parser.ts +198 -0
  112. package/src/core/formula/tokenizer.ts +160 -0
  113. package/src/core/merge-manager.ts +158 -0
  114. package/src/core/selection.ts +66 -0
  115. package/src/core/sheet.ts +257 -0
  116. package/src/core/workbook.ts +99 -0
  117. package/src/env.d.ts +7 -0
  118. package/src/grid/__test__/canvas-mock.ts +49 -0
  119. package/src/grid/__test__/setup.ts +3 -0
  120. package/src/grid/__test__/sheet-grid-formula.test.ts +140 -0
  121. package/src/grid/__test__/sheet-grid.test.ts +177 -0
  122. package/src/grid/sheet-grid.ts +283 -0
  123. package/src/index.ts +16 -0
  124. package/src/tools/__test__/builtin.test.ts +128 -0
  125. package/src/tools/__test__/context.test.ts +157 -0
  126. package/src/tools/__test__/registry.test.ts +102 -0
  127. package/src/tools/builtin.ts +75 -0
  128. package/src/tools/context.ts +106 -0
  129. package/src/tools/index.ts +2 -0
  130. package/src/tools/registry.ts +129 -0
  131. package/src/types/index.ts +1 -0
  132. package/src/types/sheet.ts +41 -0
  133. package/src/vue/__test__/sheet-component.test.ts +267 -0
  134. package/src/vue/index.ts +1 -0
  135. package/src/vue/sheet.vue +195 -0
  136. package/src/vue/style.scss +94 -0
  137. package/src/vue/style.ts +1 -0
@@ -0,0 +1,166 @@
1
+ /**
2
+ * A1 地址系统(纯函数)。
3
+ *
4
+ * 坐标统一 0-based:`{ row: 0, col: 0 }` 即 A1。
5
+ * 区域为闭区间,且经过规范化:start 恒为左上角,end 恒为右下角。
6
+ */
7
+
8
+ /** 单元格地址(0-based) */
9
+ export interface CellAddress {
10
+ row: number
11
+ col: number
12
+ }
13
+
14
+ /** 单元格区域(闭区间,start 左上角,end 右下角) */
15
+ export interface CellRange {
16
+ start: CellAddress
17
+ end: CellAddress
18
+ }
19
+
20
+ /**
21
+ * cellKey 的列基数(2^20)。
22
+ * key = row * COL_KEY_BASE + col,要求 col < COL_KEY_BASE;
23
+ * JS 安全整数范围内可容纳约 8.5e9 行,远超实际使用。
24
+ */
25
+ export const COL_KEY_BASE = 2 ** 20
26
+
27
+ /** 地址 → 数值 key(Map 索引用) */
28
+ export function cellKey(addr: CellAddress): number {
29
+ return addr.row * COL_KEY_BASE + addr.col
30
+ }
31
+
32
+ /** 数值 key → 地址 */
33
+ export function addressFromKey(key: number): CellAddress {
34
+ return { row: Math.floor(key / COL_KEY_BASE), col: key % COL_KEY_BASE }
35
+ }
36
+
37
+ /** 列号(0-based)→ 列名:0 → 'A',25 → 'Z',26 → 'AA' */
38
+ export function colIndexToName(col: number): string {
39
+ if (!Number.isInteger(col) || col < 0) {
40
+ throw new RangeError(`列号必须是非负整数: ${col}`)
41
+ }
42
+ let name = ''
43
+ let n = col
44
+ do {
45
+ name = String.fromCharCode(65 + (n % 26)) + name
46
+ n = Math.floor(n / 26) - 1
47
+ } while (n >= 0)
48
+ return name
49
+ }
50
+
51
+ /** 列名 → 列号(0-based):'A' → 0,'AA' → 26;非法列名返回 -1 */
52
+ export function colNameToIndex(name: string): number {
53
+ if (!/^[A-Za-z]+$/.test(name)) return -1
54
+ let index = 0
55
+ for (const ch of name.toUpperCase()) {
56
+ index = index * 26 + (ch.charCodeAt(0) - 64)
57
+ }
58
+ return index - 1
59
+ }
60
+
61
+ const ADDRESS_RE = /^\$?([A-Za-z]+)\$?([1-9]\d*)$/
62
+
63
+ /** 解析 A1 记法(兼容 `$A$1` 绝对引用写法)→ 地址;非法返回 null */
64
+ export function parseAddress(text: string): CellAddress | null {
65
+ const match = ADDRESS_RE.exec(text.trim())
66
+ if (!match) return null
67
+ const col = colNameToIndex(match[1]!)
68
+ const row = Number.parseInt(match[2]!, 10) - 1
69
+ return { row, col }
70
+ }
71
+
72
+ /** 地址 → A1 记法:{ row: 1, col: 1 } → 'B2' */
73
+ export function formatAddress(addr: CellAddress): string {
74
+ return `${colIndexToName(addr.col)}${addr.row + 1}`
75
+ }
76
+
77
+ /** 由两个角点构造规范化区域(start ≤ end) */
78
+ export function createRange(a: CellAddress, b: CellAddress): CellRange {
79
+ return {
80
+ start: { row: Math.min(a.row, b.row), col: Math.min(a.col, b.col) },
81
+ end: { row: Math.max(a.row, b.row), col: Math.max(a.col, b.col) }
82
+ }
83
+ }
84
+
85
+ /** 解析区域记法:'B2' 或 'B2:D5'(兼容绝对引用);非法返回 null */
86
+ export function parseRange(text: string): CellRange | null {
87
+ const parts = text.trim().split(':')
88
+ if (parts.length > 2) return null
89
+ const start = parseAddress(parts[0]!)
90
+ if (!start) return null
91
+ if (parts.length === 1) return { start, end: { ...start } }
92
+ const end = parseAddress(parts[1]!)
93
+ if (!end) return null
94
+ return createRange(start, end)
95
+ }
96
+
97
+ /** 区域 → 记法:单格 → 'B2',多格 → 'B2:D5' */
98
+ export function formatRange(range: CellRange): string {
99
+ const start = formatAddress(range.start)
100
+ if (range.start.row === range.end.row && range.start.col === range.end.col) return start
101
+ return `${start}:${formatAddress(range.end)}`
102
+ }
103
+
104
+ /** 地址相等 */
105
+ export function addressesEqual(a: CellAddress, b: CellAddress): boolean {
106
+ return a.row === b.row && a.col === b.col
107
+ }
108
+
109
+ /** 区域相等 */
110
+ export function rangesEqual(a: CellRange, b: CellRange): boolean {
111
+ return addressesEqual(a.start, b.start) && addressesEqual(a.end, b.end)
112
+ }
113
+
114
+ /** 两区域是否相交 */
115
+ export function rangesIntersect(a: CellRange, b: CellRange): boolean {
116
+ return (
117
+ a.start.row <= b.end.row &&
118
+ a.end.row >= b.start.row &&
119
+ a.start.col <= b.end.col &&
120
+ a.end.col >= b.start.col
121
+ )
122
+ }
123
+
124
+ /** 区域是否包含地址 */
125
+ export function rangeContainsAddress(range: CellRange, addr: CellAddress): boolean {
126
+ return (
127
+ addr.row >= range.start.row &&
128
+ addr.row <= range.end.row &&
129
+ addr.col >= range.start.col &&
130
+ addr.col <= range.end.col
131
+ )
132
+ }
133
+
134
+ /** 区域是否完整包含另一区域 */
135
+ export function rangeContainsRange(outer: CellRange, inner: CellRange): boolean {
136
+ return rangeContainsAddress(outer, inner.start) && rangeContainsAddress(outer, inner.end)
137
+ }
138
+
139
+ /** 一组区域的最小包围盒(至少传入一个区域) */
140
+ export function boundingBox(ranges: readonly [CellRange, ...CellRange[]]): CellRange {
141
+ let minRow = Infinity
142
+ let minCol = Infinity
143
+ let maxRow = -Infinity
144
+ let maxCol = -Infinity
145
+ for (const range of ranges) {
146
+ minRow = Math.min(minRow, range.start.row)
147
+ minCol = Math.min(minCol, range.start.col)
148
+ maxRow = Math.max(maxRow, range.end.row)
149
+ maxCol = Math.max(maxCol, range.end.col)
150
+ }
151
+ return { start: { row: minRow, col: minCol }, end: { row: maxRow, col: maxCol } }
152
+ }
153
+
154
+ /** 按行主序遍历区域内的所有地址 */
155
+ export function* iterateRange(range: CellRange): Generator<CellAddress, void, undefined> {
156
+ for (let row = range.start.row; row <= range.end.row; row++) {
157
+ for (let col = range.start.col; col <= range.end.col; col++) {
158
+ yield { row, col }
159
+ }
160
+ }
161
+ }
162
+
163
+ /** 区域内的单元格数量 */
164
+ export function rangeSize(range: CellRange): number {
165
+ return (range.end.row - range.start.row + 1) * (range.end.col - range.start.col + 1)
166
+ }
@@ -0,0 +1,173 @@
1
+ import type { CellAddress, CellRange } from './address'
2
+
3
+ /**
4
+ * 稀疏矩阵存储:`Map<row, Map<col, CellData>>`。
5
+ * 空单元格 = 存储中不存在该 key(稀疏的第一原则);
6
+ * rowCount/colCount 只是高水位,用于渲染行数,不分配空间。
7
+ */
8
+
9
+ /** 单元格值类型:n=数字 s=字符串 b=布尔 str=公式结果字符串 e=错误 d=日期(序列数) */
10
+ export type CellType = 'n' | 's' | 'b' | 'str' | 'e' | 'd'
11
+
12
+ /** 单元格原始值 */
13
+ export type CellValue = string | number | boolean | null
14
+
15
+ /** 单元格数据(参考 univer ICellData 裁剪版) */
16
+ export interface CellData {
17
+ /** 原始值(公式格为计算缓存值) */
18
+ v?: CellValue
19
+ /** 值类型,缺省按 v 推断 */
20
+ t?: CellType
21
+ /** 公式文本(不含 '='),如 'SUM(A1:B2)+Sheet2!C3' */
22
+ f?: string
23
+ // s?: StyleId —— 预留样式位,本期不实现
24
+ }
25
+
26
+ /** 序列化条目(snapshot/restore 用) */
27
+ export interface CellSnapshotItem extends CellData {
28
+ row: number
29
+ col: number
30
+ }
31
+
32
+ /** 按原始值推断类型 */
33
+ export function inferCellType(v: CellValue): CellType | undefined {
34
+ switch (typeof v) {
35
+ case 'number':
36
+ return 'n'
37
+ case 'boolean':
38
+ return 'b'
39
+ case 'string':
40
+ return 's'
41
+ default:
42
+ return undefined
43
+ }
44
+ }
45
+
46
+ /** 判定为空:无公式且值为 null/undefined/'' */
47
+ export function isEmptyCellData(data: CellData | undefined): boolean {
48
+ if (!data) return true
49
+ if (data.f != null && data.f !== '') return false
50
+ return data.v == null || data.v === ''
51
+ }
52
+
53
+ /** CellData 相等(v/t/f 三字段逐一比较;命令系统据此跳过无实际变更的补丁) */
54
+ export function cellDataEqual(a: CellData | undefined, b: CellData | undefined): boolean {
55
+ if (a === b) return true
56
+ if (!a || !b) return false
57
+ return a.v === b.v && a.t === b.t && a.f === b.f
58
+ }
59
+
60
+ export class CellStore {
61
+ private cells = new Map<number, Map<number, CellData>>()
62
+ private _rowCount = 0
63
+ private _colCount = 0
64
+
65
+ /** 行数高水位(只增不减,restore 时重算) */
66
+ get rowCount(): number {
67
+ return this._rowCount
68
+ }
69
+
70
+ /** 列数高水位 */
71
+ get colCount(): number {
72
+ return this._colCount
73
+ }
74
+
75
+ /** 真实存在的单元格数量 */
76
+ get size(): number {
77
+ let size = 0
78
+ for (const rowMap of this.cells.values()) size += rowMap.size
79
+ return size
80
+ }
81
+
82
+ /** 读取(返回副本,外部修改不影响存储) */
83
+ getCell(addr: CellAddress): CellData | undefined {
84
+ const data = this.cells.get(addr.row)?.get(addr.col)
85
+ return data ? { ...data } : undefined
86
+ }
87
+
88
+ /** 写入(存储副本);空数据等价于删除 */
89
+ setCell(addr: CellAddress, data?: CellData): void {
90
+ if (isEmptyCellData(data)) {
91
+ this.deleteCell(addr)
92
+ return
93
+ }
94
+ let rowMap = this.cells.get(addr.row)
95
+ if (!rowMap) {
96
+ rowMap = new Map()
97
+ this.cells.set(addr.row, rowMap)
98
+ }
99
+ rowMap.set(addr.col, { ...data })
100
+ this._rowCount = Math.max(this._rowCount, addr.row + 1)
101
+ this._colCount = Math.max(this._colCount, addr.col + 1)
102
+ }
103
+
104
+ /** 便捷写入原始值,类型自动推断;空值等价于删除 */
105
+ setCellValue(addr: CellAddress, value: CellValue): void {
106
+ if (value == null || value === '') {
107
+ this.deleteCell(addr)
108
+ return
109
+ }
110
+ this.setCell(addr, { v: value, t: inferCellType(value) })
111
+ }
112
+
113
+ deleteCell(addr: CellAddress): boolean {
114
+ const rowMap = this.cells.get(addr.row)
115
+ if (!rowMap) return false
116
+ const deleted = rowMap.delete(addr.col)
117
+ if (rowMap.size === 0) this.cells.delete(addr.row)
118
+ return deleted
119
+ }
120
+
121
+ /** 只遍历真实存在的单元格(行主序) */
122
+ *entries(): Generator<[CellAddress, CellData], void, undefined> {
123
+ for (const [row, rowMap] of this.cells) {
124
+ for (const [col, data] of rowMap) {
125
+ yield [{ row, col }, { ...data }]
126
+ }
127
+ }
128
+ }
129
+
130
+ /** 只遍历区域内真实存在的单元格(行主序;公式区域展开用,空行/空格不分配不访问) */
131
+ *entriesInRange(range: CellRange): Generator<[CellAddress, CellData], void, undefined> {
132
+ for (let row = range.start.row; row <= range.end.row; row++) {
133
+ const rowMap = this.cells.get(row)
134
+ if (!rowMap) continue
135
+ for (let col = range.start.col; col <= range.end.col; col++) {
136
+ const data = rowMap.get(col)
137
+ if (data) yield [{ row, col }, { ...data }]
138
+ }
139
+ }
140
+ }
141
+
142
+ /** 序列化(只含真实存在的格) */
143
+ snapshot(): CellSnapshotItem[] {
144
+ const items: CellSnapshotItem[] = []
145
+ for (const [addr, data] of this.entries()) {
146
+ items.push({ row: addr.row, col: addr.col, ...data })
147
+ }
148
+ return items
149
+ }
150
+
151
+ /** 从序列化恢复(清空重建,重算高水位) */
152
+ restore(items: readonly CellSnapshotItem[]): void {
153
+ this.clear()
154
+ for (const item of items) {
155
+ const { row, col, ...data } = item
156
+ if (isEmptyCellData(data)) continue
157
+ let rowMap = this.cells.get(row)
158
+ if (!rowMap) {
159
+ rowMap = new Map()
160
+ this.cells.set(row, rowMap)
161
+ }
162
+ rowMap.set(col, { ...data })
163
+ this._rowCount = Math.max(this._rowCount, row + 1)
164
+ this._colCount = Math.max(this._colCount, col + 1)
165
+ }
166
+ }
167
+
168
+ clear(): void {
169
+ this.cells.clear()
170
+ this._rowCount = 0
171
+ this._colCount = 0
172
+ }
173
+ }
@@ -0,0 +1,15 @@
1
+ import { MergeCellsCommand, UnmergeCellsCommand } from './merge-cells'
2
+ import { CommandRegistry } from './registry'
3
+ import { SetCellFormulaCommand } from './set-cell-formula'
4
+ import { SetCellValueCommand } from './set-cell-value'
5
+
6
+ /**
7
+ * 默认命令注册表:内置命令模块加载时注册一次,全局共享(命令无状态)。
8
+ * Sheet 默认经此表执行命令;阶段 4 工具扩展可继续注册自定义命令。
9
+ */
10
+ export const defaultCommandRegistry = new CommandRegistry()
11
+
12
+ defaultCommandRegistry.register(SetCellValueCommand)
13
+ defaultCommandRegistry.register(SetCellFormulaCommand)
14
+ defaultCommandRegistry.register(MergeCellsCommand)
15
+ defaultCommandRegistry.register(UnmergeCellsCommand)
@@ -0,0 +1,167 @@
1
+ import { TypedEventEmitter } from '../events'
2
+ import type { Mutation, Patch, PatchDirection } from './types'
3
+
4
+ /**
5
+ * HistoryManager:undo/redo 栈 + 事务。
6
+ *
7
+ * - 一个 undo 单元 = 一次命令(或一次事务)的全部 Mutation
8
+ * - 事务:beginTransaction / commit,支持嵌套(深度计数,拍平到最外层);
9
+ * rollback 回滚缓冲区中已应用的变更并放弃事务
10
+ * - 容量上限(默认 200):超出淘汰最旧条目
11
+ * - 新命令入栈即清空 redo 栈
12
+ */
13
+
14
+ /** 历史栈容量默认值 */
15
+ export const DEFAULT_HISTORY_CAPACITY = 200
16
+
17
+ /** 历史状态(history-change 事件负载) */
18
+ export interface HistoryState {
19
+ canUndo: boolean
20
+ canRedo: boolean
21
+ }
22
+
23
+ type HistoryEvents = { change: HistoryState }
24
+
25
+ /** 一个 undo 单元 = 一组 Mutation */
26
+ type HistoryEntry = Mutation[]
27
+
28
+ export class HistoryManager {
29
+ private undoStack: HistoryEntry[] = []
30
+ private redoStack: HistoryEntry[] = []
31
+ /** 事务深度(>0 表示事务进行中) */
32
+ private transactionDepth = 0
33
+ /** 事务缓冲:commit 时合并为一个 undo 单元 */
34
+ private transactionBuffer: Mutation[] = []
35
+ private emitter = new TypedEventEmitter<HistoryEvents>()
36
+
37
+ constructor(
38
+ /** 补丁应用器(由 Sheet 注入,命令执行与回放共用) */
39
+ private readonly applyPatch: (patch: Patch, direction: PatchDirection) => void,
40
+ private readonly capacity: number = DEFAULT_HISTORY_CAPACITY
41
+ ) {}
42
+
43
+ get canUndo(): boolean {
44
+ return this.undoStack.length > 0
45
+ }
46
+
47
+ get canRedo(): boolean {
48
+ return this.redoStack.length > 0
49
+ }
50
+
51
+ /** undo 栈深度(调试/测试用) */
52
+ get undoSize(): number {
53
+ return this.undoStack.length
54
+ }
55
+
56
+ /** redo 栈深度(调试/测试用) */
57
+ get redoSize(): number {
58
+ return this.redoStack.length
59
+ }
60
+
61
+ /** 是否处于事务中 */
62
+ get inTransaction(): boolean {
63
+ return this.transactionDepth > 0
64
+ }
65
+
66
+ /** 命令执行产生的 mutation 入栈(事务中则缓冲);空列表直接忽略 */
67
+ push(mutations: Mutation[]): void {
68
+ if (mutations.length === 0) return
69
+ if (this.transactionDepth > 0) {
70
+ this.transactionBuffer.push(...mutations)
71
+ return
72
+ }
73
+ this.pushEntry(mutations)
74
+ }
75
+
76
+ /** 开启事务(可嵌套,拍平到最外层) */
77
+ beginTransaction(): void {
78
+ this.transactionDepth++
79
+ }
80
+
81
+ /** 提交事务:缓冲的 mutation 合并为一个 undo 单元入栈 */
82
+ commit(): void {
83
+ if (this.transactionDepth === 0) {
84
+ throw new Error('HistoryManager.commit:没有进行中的事务')
85
+ }
86
+ this.transactionDepth--
87
+ if (this.transactionDepth === 0 && this.transactionBuffer.length > 0) {
88
+ const entry = this.transactionBuffer
89
+ this.transactionBuffer = []
90
+ this.pushEntry(entry)
91
+ }
92
+ }
93
+
94
+ /**
95
+ * 回滚事务:逆序应用缓冲区中各 mutation 的 undo 补丁还原模型,
96
+ * 丢弃缓冲并结束事务(含嵌套);不动 undo/redo 栈。
97
+ */
98
+ rollback(): void {
99
+ if (this.transactionDepth === 0) return
100
+ const buffer = this.transactionBuffer
101
+ this.transactionBuffer = []
102
+ this.transactionDepth = 0
103
+ for (let i = buffer.length - 1; i >= 0; i--) {
104
+ for (const patch of buffer[i]!.undo) {
105
+ this.applyPatch(patch, 'undo')
106
+ }
107
+ }
108
+ }
109
+
110
+ /** 撤销一个单元;事务进行中或栈空时返回 false */
111
+ undo(): boolean {
112
+ if (this.transactionDepth > 0) return false
113
+ const entry = this.undoStack.pop()
114
+ if (!entry) return false
115
+ for (let i = entry.length - 1; i >= 0; i--) {
116
+ for (const patch of entry[i]!.undo) {
117
+ this.applyPatch(patch, 'undo')
118
+ }
119
+ }
120
+ this.redoStack.push(entry)
121
+ this.emitChange()
122
+ return true
123
+ }
124
+
125
+ /** 重做一个单元;事务进行中或栈空时返回 false */
126
+ redo(): boolean {
127
+ if (this.transactionDepth > 0) return false
128
+ const entry = this.redoStack.pop()
129
+ if (!entry) return false
130
+ for (const mutation of entry) {
131
+ for (const patch of mutation.redo) {
132
+ this.applyPatch(patch, 'redo')
133
+ }
134
+ }
135
+ this.undoStack.push(entry)
136
+ this.emitChange()
137
+ return true
138
+ }
139
+
140
+ /** 清空全部历史(含事务缓冲) */
141
+ clear(): void {
142
+ this.undoStack = []
143
+ this.redoStack = []
144
+ this.transactionBuffer = []
145
+ this.transactionDepth = 0
146
+ this.emitChange()
147
+ }
148
+
149
+ onChange(handler: (state: HistoryState) => void): () => void {
150
+ return this.emitter.on('change', handler)
151
+ }
152
+
153
+ private pushEntry(entry: HistoryEntry): void {
154
+ this.undoStack.push(entry)
155
+ if (this.undoStack.length > this.capacity) {
156
+ // 淘汰最旧条目;redo 栈不受影响(淘汰只发生在 undo 栈底部)
157
+ this.undoStack.shift()
158
+ }
159
+ // 新命令清空 redo 栈
160
+ this.redoStack = []
161
+ this.emitChange()
162
+ }
163
+
164
+ private emitChange(): void {
165
+ this.emitter.emit('change', { canUndo: this.canUndo, canRedo: this.canRedo })
166
+ }
167
+ }
@@ -0,0 +1,7 @@
1
+ export * from './types'
2
+ export * from './registry'
3
+ export * from './history'
4
+ export * from './set-cell-value'
5
+ export * from './set-cell-formula'
6
+ export * from './merge-cells'
7
+ export * from './default-registry'
@@ -0,0 +1,74 @@
1
+ import { iterateRange, rangesIntersect, type CellRange } from '../address'
2
+ import { cellDataEqual, isEmptyCellData, type CellData } from '../cell-store'
3
+ import type { CellPatch, Command, CommandResult, MergePatch, Patch } from './types'
4
+
5
+ export interface MergeCellsParams {
6
+ range: CellRange
7
+ }
8
+
9
+ /**
10
+ * 合并命令。Patch 捕获完整 before 状态:
11
+ * - 被解除的既有合并记录(undo 时逐一恢复)
12
+ * - 包围盒内每一格的原数据(undo 时连被清空的值一起还原)
13
+ * 值保留规则同 Excel/univer:行主序第一个有值格的值写入新锚点,其余清空。
14
+ */
15
+ export const MergeCellsCommand: Command<MergeCellsParams, CellRange> = {
16
+ id: 'sheet.command.merge-cells',
17
+
18
+ handler(ctx, params): CommandResult<CellRange> {
19
+ const { sheet } = ctx
20
+ const { range: finalRange, removed } = sheet.merges.computeMerge(params.range)
21
+
22
+ // 捕获包围盒内各格 before;同时按行主序找出第一个有值格
23
+ const cellPatches: CellPatch[] = []
24
+ let retained: CellData | undefined
25
+ let anchorPatch: CellPatch | undefined
26
+ for (const addr of iterateRange(finalRange)) {
27
+ const before = sheet.store.getCell(addr)
28
+ if (before && !retained && !isEmptyCellData(before)) retained = before
29
+ const patch: CellPatch = { kind: 'cell', addr, before, after: undefined }
30
+ if (addr.row === finalRange.start.row && addr.col === finalRange.start.col) {
31
+ anchorPatch = patch
32
+ }
33
+ cellPatches.push(patch)
34
+ }
35
+ if (retained && anchorPatch) {
36
+ anchorPatch.after = { ...retained }
37
+ }
38
+
39
+ const mergePatches: MergePatch[] = [
40
+ ...removed.map((range): MergePatch => ({ kind: 'merge', range, before: true, after: false })),
41
+ { kind: 'merge', range: finalRange, before: false, after: true }
42
+ ]
43
+
44
+ // 过滤无实际变化的 cell 补丁(如被覆盖格本就为空)
45
+ const patches: Patch[] = [
46
+ ...mergePatches,
47
+ ...cellPatches.filter((patch) => !cellDataEqual(patch.before, patch.after))
48
+ ]
49
+ for (const patch of patches) ctx.applyPatch(patch, 'redo')
50
+
51
+ return { mutations: [{ redo: patches, undo: [...patches].reverse() }], result: finalRange }
52
+ }
53
+ }
54
+
55
+ export interface UnmergeCellsParams {
56
+ range: CellRange
57
+ }
58
+
59
+ /** 取消合并命令:解除与 range 相交的所有合并;undo 恢复合并记录 */
60
+ export const UnmergeCellsCommand: Command<UnmergeCellsParams> = {
61
+ id: 'sheet.command.unmerge-cells',
62
+
63
+ handler(ctx, params): CommandResult {
64
+ const removed: MergePatch[] = []
65
+ for (const range of ctx.sheet.merges.getMerges()) {
66
+ if (rangesIntersect(range, params.range)) {
67
+ removed.push({ kind: 'merge', range, before: true, after: false })
68
+ }
69
+ }
70
+ if (removed.length === 0) return { mutations: [] }
71
+ for (const patch of removed) ctx.applyPatch(patch, 'redo')
72
+ return { mutations: [{ redo: removed, undo: [...removed].reverse() }] }
73
+ }
74
+ }
@@ -0,0 +1,30 @@
1
+ import type { Command, CommandContext, CommandResult } from './types'
2
+
3
+ /**
4
+ * 命令注册表:register / execute。
5
+ * 命令无状态,可全局共享(见 default-registry);阶段 4 工具扩展可注册自定义命令。
6
+ */
7
+ export class CommandRegistry {
8
+ private commands = new Map<string, Command>()
9
+
10
+ /** 注册命令;同 id 重复注册视为错误 */
11
+ register(command: Command): void {
12
+ if (this.commands.has(command.id)) {
13
+ throw new Error(`命令重复注册: ${command.id}`)
14
+ }
15
+ this.commands.set(command.id, command)
16
+ }
17
+
18
+ get(id: string): Command | undefined {
19
+ return this.commands.get(id)
20
+ }
21
+
22
+ /** 执行命令;未注册的 id 抛错 */
23
+ execute<R>(ctx: CommandContext, id: string, params: unknown): CommandResult<R> | undefined {
24
+ const command = this.commands.get(id)
25
+ if (!command) {
26
+ throw new Error(`命令未注册: ${id}`)
27
+ }
28
+ return command.handler(ctx, params) as CommandResult<R> | undefined
29
+ }
30
+ }
@@ -0,0 +1,29 @@
1
+ import type { CellAddress } from '../address'
2
+ import type { CellData } from '../cell-store'
3
+ import type { CellPatch, Command, CommandResult } from './types'
4
+
5
+ export interface SetCellFormulaParams {
6
+ addr: CellAddress
7
+ /** 公式文本(不含 '=') */
8
+ formula: string
9
+ }
10
+
11
+ /**
12
+ * 公式写入命令:只登记公式原文(f);计算缓存(v/t)由命令执行后的
13
+ * 增量重算以派生补丁填充(见 Sheet.executeCommand),重算波及的所有变更
14
+ * 与源补丁并入同一 undo 单元。
15
+ */
16
+ export const SetCellFormulaCommand: Command<SetCellFormulaParams> = {
17
+ id: 'sheet.command.set-cell-formula',
18
+
19
+ handler(ctx, params): CommandResult {
20
+ const anchor = ctx.sheet.merges.resolveAnchor(params.addr)
21
+ const before = ctx.sheet.store.getCell(anchor)
22
+ // 同公式重复输入 = 无实际变更,不入历史
23
+ if (before?.f === params.formula) return { mutations: [] }
24
+ const after: CellData = { f: params.formula }
25
+ const patch: CellPatch = { kind: 'cell', addr: anchor, before, after }
26
+ ctx.applyPatch(patch, 'redo')
27
+ return { mutations: [{ redo: [patch], undo: [patch] }] }
28
+ }
29
+ }