@miy2/xml-api 0.9.0 → 0.9.1
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.
- package/README.md +22 -2
- package/dist/collab/bridge.js +1 -2
- package/dist/cst/cst-utils.d.ts +11 -0
- package/dist/cst/cst-utils.js +32 -0
- package/dist/cst/grammar.js +11 -25
- package/dist/cst/parser.js +7 -11
- package/dist/cst/xml-cst.js +1 -5
- package/dist/cst/xml-grammar.js +83 -86
- package/dist/dom.d.ts +20 -1
- package/dist/dom.js +93 -60
- package/dist/engine/editor-state.js +1 -5
- package/dist/engine/sync-engine.d.ts +43 -9
- package/dist/engine/sync-engine.js +166 -122
- package/dist/engine/transaction-builder.d.ts +14 -0
- package/dist/engine/transaction-builder.js +143 -0
- package/dist/engine/transaction.d.ts +3 -0
- package/dist/engine/transaction.js +12 -8
- package/dist/history-manager.d.ts +4 -0
- package/dist/history-manager.js +75 -5
- package/dist/model/formatter.d.ts +2 -0
- package/dist/model/formatter.js +15 -18
- package/dist/model/xml-api-model.d.ts +17 -6
- package/dist/model/xml-api-model.js +40 -34
- package/dist/model/xml-binder.d.ts +8 -1
- package/dist/model/xml-binder.js +48 -34
- package/dist/model/xml-schema.js +1 -5
- package/dist/view/schema-view.d.ts +80 -0
- package/dist/view/schema-view.js +222 -0
- package/dist/view/view-binder.d.ts +47 -0
- package/dist/view/view-binder.js +163 -0
- package/dist/xml-api-events.d.ts +8 -4
- package/dist/xml-api-events.js +1 -5
- package/dist/xml-api.d.ts +36 -22
- package/dist/xml-api.js +77 -59
- package/package.json +4 -2
- package/LICENSE.md +0 -21
|
@@ -1,24 +1,31 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
1
|
+
import { Parser } from "../cst/parser";
|
|
2
|
+
import { grammar as defaultGrammar } from "../cst/xml-grammar";
|
|
3
|
+
import { HistoryManager } from "../history-manager";
|
|
4
|
+
import { ModelElement } from "../model/xml-api-model";
|
|
5
|
+
import { XMLBinder } from "../model/xml-binder";
|
|
6
|
+
import { EventEmitter } from "../xml-api-events";
|
|
7
|
+
import { EditorState } from "./editor-state";
|
|
8
|
+
import { Transaction } from "./transaction";
|
|
9
|
+
import { TransactionBuilder } from "./transaction-builder";
|
|
10
|
+
/**
|
|
11
|
+
* The core engine that manages the editor state and coordinates synchronization.
|
|
12
|
+
*
|
|
13
|
+
* It implements a transaction-based update cycle:
|
|
14
|
+
* 1. Receives a `Transaction` describing changes.
|
|
15
|
+
* 2. Updates the `EditorState` (Source).
|
|
16
|
+
* 3. Triggers the `Parser` (Source -> CST).
|
|
17
|
+
* 4. Triggers the `XMLBinder` (CST -> Model).
|
|
18
|
+
* 5. Notifies listeners (including `SchemaView`s) of changes.
|
|
19
|
+
*/
|
|
20
|
+
export class SyncEngine {
|
|
21
|
+
constructor(source, grammar = defaultGrammar) {
|
|
15
22
|
this.isTransacting = false;
|
|
16
23
|
this.collabBridge = null;
|
|
17
|
-
this._state =
|
|
18
|
-
this.parser = new
|
|
19
|
-
this.binder = new
|
|
20
|
-
this.history = new
|
|
21
|
-
this.events = new
|
|
24
|
+
this._state = EditorState.create(source);
|
|
25
|
+
this.parser = new Parser(grammar);
|
|
26
|
+
this.binder = new XMLBinder(source);
|
|
27
|
+
this.history = new HistoryManager();
|
|
28
|
+
this.events = new EventEmitter();
|
|
22
29
|
this.fullParse();
|
|
23
30
|
}
|
|
24
31
|
get state() {
|
|
@@ -47,6 +54,15 @@ class SyncEngine {
|
|
|
47
54
|
}
|
|
48
55
|
/**
|
|
49
56
|
* Applies a transaction to the engine, updating the state and notifying listeners.
|
|
57
|
+
* This is the single point of truth for all state transitions in the system.
|
|
58
|
+
*
|
|
59
|
+
* It handles:
|
|
60
|
+
* - History recording (Undo/Redo)
|
|
61
|
+
* - Incremental Parsing and Reconciliation
|
|
62
|
+
* - Event Dispatching
|
|
63
|
+
* - Collaboration hooks
|
|
64
|
+
*
|
|
65
|
+
* @param tr The transaction to apply.
|
|
50
66
|
*/
|
|
51
67
|
dispatch(tr) {
|
|
52
68
|
if (!tr.docChanged)
|
|
@@ -66,6 +82,7 @@ class SyncEngine {
|
|
|
66
82
|
const oldText = oldState.source.slice(p.from, p.to);
|
|
67
83
|
const newEnd = p.from + p.text.length;
|
|
68
84
|
this.history.push({
|
|
85
|
+
timestamp: Date.now(),
|
|
69
86
|
redo: { from: p.from, to: p.to, text: p.text },
|
|
70
87
|
undo: { from: p.from, to: newEnd, text: oldText },
|
|
71
88
|
});
|
|
@@ -78,7 +95,7 @@ class SyncEngine {
|
|
|
78
95
|
}
|
|
79
96
|
this._state = this._state.update({ source: newSource });
|
|
80
97
|
// Core Update Logic (Parser / Binder)
|
|
81
|
-
this.binder = new
|
|
98
|
+
this.binder = new XMLBinder(newSource);
|
|
82
99
|
// Optimization: If single patch, try incremental. Else full parse.
|
|
83
100
|
let handled = false;
|
|
84
101
|
if (tr.patches.length === 1 && oldState.cst) {
|
|
@@ -86,17 +103,23 @@ class SyncEngine {
|
|
|
86
103
|
const delta = p.text.length - (p.to - p.from);
|
|
87
104
|
const incrementalResult = this.tryIncrementalUpdate(p.from, p.to, delta);
|
|
88
105
|
if (incrementalResult) {
|
|
106
|
+
let success = true;
|
|
89
107
|
if (oldState.model) {
|
|
90
|
-
this.updateModelIncremental(incrementalResult.oldNode, incrementalResult.newNode, tr);
|
|
108
|
+
success = this.updateModelIncremental(incrementalResult.oldNode, incrementalResult.newNode, tr);
|
|
91
109
|
}
|
|
92
110
|
else {
|
|
93
111
|
this.events.emit({ type: "full", transaction: tr });
|
|
94
112
|
}
|
|
95
|
-
if (
|
|
96
|
-
this._state
|
|
97
|
-
|
|
113
|
+
if (success) {
|
|
114
|
+
if (this._state.cst && !this._state.cst.wellFormed) {
|
|
115
|
+
this._state = this._state.update({ model: null });
|
|
116
|
+
this.events.emit({ type: "full", transaction: tr });
|
|
117
|
+
}
|
|
118
|
+
handled = true;
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
handled = false;
|
|
98
122
|
}
|
|
99
|
-
handled = true;
|
|
100
123
|
}
|
|
101
124
|
}
|
|
102
125
|
if (!handled) {
|
|
@@ -112,18 +135,33 @@ class SyncEngine {
|
|
|
112
135
|
* Update the source code (e.g. from text editor).
|
|
113
136
|
* Handles history recording and incremental parsing.
|
|
114
137
|
*/
|
|
115
|
-
updateSource(from, to, text
|
|
116
|
-
|
|
138
|
+
updateSource(from, to, text,
|
|
139
|
+
// biome-ignore lint/suspicious/noExplicitAny: Metadata can store any type
|
|
140
|
+
meta) {
|
|
141
|
+
const tr = new Transaction(this._state);
|
|
142
|
+
if (meta) {
|
|
143
|
+
for (const [key, value] of Object.entries(meta)) {
|
|
144
|
+
tr.setMeta(key, value);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
117
147
|
tr.replace(from, to, text);
|
|
118
148
|
this.dispatch(tr);
|
|
119
149
|
}
|
|
120
150
|
/**
|
|
121
151
|
* Apply a programmatic change derived from Model operations.
|
|
122
152
|
* This is the "Application -> Source" flow.
|
|
153
|
+
* @deprecated Use `dispatch(new Transaction(state).replace(...))` instead.
|
|
123
154
|
*/
|
|
124
|
-
applyPatch(start, end, text
|
|
155
|
+
applyPatch(start, end, text,
|
|
156
|
+
// biome-ignore lint/suspicious/noExplicitAny: Metadata can store any type
|
|
157
|
+
meta) {
|
|
125
158
|
// Uses dispatch via updateSource logic, but conceptually distinct
|
|
126
|
-
const tr = new
|
|
159
|
+
const tr = new Transaction(this._state);
|
|
160
|
+
if (meta) {
|
|
161
|
+
for (const [key, value] of Object.entries(meta)) {
|
|
162
|
+
tr.setMeta(key, value);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
127
165
|
tr.replace(start, end, text);
|
|
128
166
|
this.dispatch(tr);
|
|
129
167
|
}
|
|
@@ -153,78 +191,80 @@ class SyncEngine {
|
|
|
153
191
|
}
|
|
154
192
|
}
|
|
155
193
|
// --- High-Level Model Operations (delegated to Binder) ---
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
194
|
+
/**
|
|
195
|
+
* @deprecated Use `dispatch(new TransactionBuilder(engine.state, engine.binder).setAttribute(...))` instead.
|
|
196
|
+
*/
|
|
197
|
+
setAttribute(modelNode, key, value,
|
|
198
|
+
// biome-ignore lint/suspicious/noExplicitAny: Metadata can store any type
|
|
199
|
+
meta) {
|
|
200
|
+
const builder = new TransactionBuilder(this._state, this.binder);
|
|
201
|
+
const tr = builder.setAttribute(modelNode, key, value);
|
|
202
|
+
if (meta) {
|
|
203
|
+
for (const [k, v] of Object.entries(meta)) {
|
|
204
|
+
tr.setMeta(k, v);
|
|
205
|
+
}
|
|
162
206
|
}
|
|
207
|
+
this.dispatch(tr);
|
|
163
208
|
}
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
209
|
+
/**
|
|
210
|
+
* @deprecated Use `dispatch(new TransactionBuilder(engine.state, engine.binder).updateText(...))` instead.
|
|
211
|
+
*/
|
|
212
|
+
updateText(modelNode, text,
|
|
213
|
+
// biome-ignore lint/suspicious/noExplicitAny: Metadata can store any type
|
|
214
|
+
meta) {
|
|
215
|
+
const builder = new TransactionBuilder(this._state, this.binder);
|
|
216
|
+
const tr = builder.updateText(modelNode, text);
|
|
217
|
+
if (meta) {
|
|
218
|
+
for (const [k, v] of Object.entries(meta)) {
|
|
219
|
+
tr.setMeta(k, v);
|
|
220
|
+
}
|
|
170
221
|
}
|
|
222
|
+
this.dispatch(tr);
|
|
171
223
|
}
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
if (currentIndent.startsWith(parentIndent)) {
|
|
184
|
-
const diff = currentIndent.slice(parentIndent.length);
|
|
185
|
-
if (diff.length > 0 && !diff.includes("\n")) {
|
|
186
|
-
indentUnit = diff;
|
|
187
|
-
}
|
|
188
|
-
}
|
|
224
|
+
/**
|
|
225
|
+
* @deprecated Use `dispatch(new TransactionBuilder(engine.state, engine.binder).replaceNode(...))` instead.
|
|
226
|
+
*/
|
|
227
|
+
replaceNode(target, content,
|
|
228
|
+
// biome-ignore lint/suspicious/noExplicitAny: Metadata can store any type
|
|
229
|
+
meta) {
|
|
230
|
+
const builder = new TransactionBuilder(this._state, this.binder);
|
|
231
|
+
const tr = builder.replaceNode(target, content);
|
|
232
|
+
if (meta) {
|
|
233
|
+
for (const [k, v] of Object.entries(meta)) {
|
|
234
|
+
tr.setMeta(k, v);
|
|
189
235
|
}
|
|
190
236
|
}
|
|
191
|
-
|
|
192
|
-
let newXml = formatter.format(content);
|
|
193
|
-
if (currentIndent && newXml.includes("\n")) {
|
|
194
|
-
newXml = newXml
|
|
195
|
-
.split("\n")
|
|
196
|
-
.map((line, index) => (index === 0 ? line : currentIndent + line))
|
|
197
|
-
.join("\n");
|
|
198
|
-
}
|
|
199
|
-
const patch = this.binder.calcReplaceNodePatch(target, newXml);
|
|
200
|
-
if (patch) {
|
|
201
|
-
this.applyPatch(patch.start, patch.end, patch.text);
|
|
202
|
-
}
|
|
237
|
+
this.dispatch(tr);
|
|
203
238
|
}
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
const patch = this.binder.calcInsertNodePatch(parent, index, insertText);
|
|
217
|
-
if (patch) {
|
|
218
|
-
this.applyPatch(patch.start, patch.end, patch.text);
|
|
239
|
+
/**
|
|
240
|
+
* @deprecated Use `dispatch(new TransactionBuilder(engine.state, engine.binder).insertNode(...))` instead.
|
|
241
|
+
*/
|
|
242
|
+
insertNode(parent, child, index,
|
|
243
|
+
// biome-ignore lint/suspicious/noExplicitAny: Metadata can store any type
|
|
244
|
+
meta) {
|
|
245
|
+
const builder = new TransactionBuilder(this._state, this.binder);
|
|
246
|
+
const tr = builder.insertNode(parent, child, index);
|
|
247
|
+
if (meta) {
|
|
248
|
+
for (const [k, v] of Object.entries(meta)) {
|
|
249
|
+
tr.setMeta(k, v);
|
|
250
|
+
}
|
|
219
251
|
}
|
|
252
|
+
this.dispatch(tr);
|
|
220
253
|
}
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
254
|
+
/**
|
|
255
|
+
* @deprecated Use `dispatch(new TransactionBuilder(engine.state, engine.binder).removeNode(...))` instead.
|
|
256
|
+
*/
|
|
257
|
+
removeNode(parent, child,
|
|
258
|
+
// biome-ignore lint/suspicious/noExplicitAny: Metadata can store any type
|
|
259
|
+
meta) {
|
|
260
|
+
const builder = new TransactionBuilder(this._state, this.binder);
|
|
261
|
+
const tr = builder.removeNode(parent, child);
|
|
262
|
+
if (meta) {
|
|
263
|
+
for (const [k, v] of Object.entries(meta)) {
|
|
264
|
+
tr.setMeta(k, v);
|
|
265
|
+
}
|
|
227
266
|
}
|
|
267
|
+
this.dispatch(tr);
|
|
228
268
|
}
|
|
229
269
|
// --- Internal Logic ---
|
|
230
270
|
fullParse() {
|
|
@@ -232,10 +272,19 @@ class SyncEngine {
|
|
|
232
272
|
const cst = this.parser.parse(this._state.source);
|
|
233
273
|
let model = null;
|
|
234
274
|
if (cst === null || cst === void 0 ? void 0 : cst.wellFormed) {
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
275
|
+
if (this._state.model) {
|
|
276
|
+
// Attempt to reconcile with existing model to preserve identity
|
|
277
|
+
const result = this.binder.reconcile(this._state.model, cst);
|
|
278
|
+
if (result.node instanceof ModelElement) {
|
|
279
|
+
model = result.node;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
else {
|
|
283
|
+
// Initial hydration
|
|
284
|
+
const newModel = this.binder.hydrate(cst);
|
|
285
|
+
if (newModel instanceof ModelElement) {
|
|
286
|
+
model = newModel;
|
|
287
|
+
}
|
|
239
288
|
}
|
|
240
289
|
}
|
|
241
290
|
this._state = this._state.update({ cst, model });
|
|
@@ -283,24 +332,28 @@ class SyncEngine {
|
|
|
283
332
|
return null;
|
|
284
333
|
}
|
|
285
334
|
updateModelIncremental(oldNode, newNode, tr) {
|
|
335
|
+
var _a, _b;
|
|
286
336
|
const currentModel = this._state.model;
|
|
287
337
|
if (!currentModel)
|
|
288
|
-
return;
|
|
338
|
+
return false;
|
|
289
339
|
if (currentModel.cst === oldNode) {
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
target: newModelNode,
|
|
296
|
-
transaction: tr,
|
|
297
|
-
});
|
|
340
|
+
// Reconcile root to preserve identity
|
|
341
|
+
const result = this.binder.reconcile(currentModel, newNode);
|
|
342
|
+
const reconciled = result.node;
|
|
343
|
+
if (reconciled !== currentModel) {
|
|
344
|
+
this._state = this._state.update({ model: reconciled });
|
|
298
345
|
}
|
|
299
|
-
|
|
346
|
+
this.events.emit({
|
|
347
|
+
type: "full", // Or structure? Full implies root changed/updated
|
|
348
|
+
target: reconciled || undefined,
|
|
349
|
+
transaction: tr,
|
|
350
|
+
});
|
|
351
|
+
return true;
|
|
300
352
|
}
|
|
301
353
|
const modelPath = this.findModelNodePath(currentModel, oldNode);
|
|
302
354
|
if (modelPath) {
|
|
303
|
-
const
|
|
355
|
+
const result = this.binder.reconcile(modelPath.node, newNode);
|
|
356
|
+
const reconciledModel = result.node;
|
|
304
357
|
if (reconciledModel) {
|
|
305
358
|
if (reconciledModel !== modelPath.node) {
|
|
306
359
|
modelPath.parent.children[modelPath.index] = reconciledModel;
|
|
@@ -310,8 +363,14 @@ class SyncEngine {
|
|
|
310
363
|
type: "structure",
|
|
311
364
|
target: reconciledModel,
|
|
312
365
|
transaction: tr,
|
|
366
|
+
addedNodes: (_a = result.diff) === null || _a === void 0 ? void 0 : _a.addedNodes,
|
|
367
|
+
removedNodes: (_b = result.diff) === null || _b === void 0 ? void 0 : _b.removedNodes,
|
|
313
368
|
});
|
|
314
369
|
}
|
|
370
|
+
return true;
|
|
371
|
+
}
|
|
372
|
+
else {
|
|
373
|
+
return false;
|
|
315
374
|
}
|
|
316
375
|
}
|
|
317
376
|
// --- Helpers ---
|
|
@@ -354,7 +413,7 @@ class SyncEngine {
|
|
|
354
413
|
if (child.cst === cstNode) {
|
|
355
414
|
return { parent: root, index: i, node: child };
|
|
356
415
|
}
|
|
357
|
-
if (child instanceof
|
|
416
|
+
if (child instanceof ModelElement) {
|
|
358
417
|
const found = this.findModelNodePath(child, cstNode);
|
|
359
418
|
if (found)
|
|
360
419
|
return found;
|
|
@@ -383,19 +442,4 @@ class SyncEngine {
|
|
|
383
442
|
current = current.parent;
|
|
384
443
|
}
|
|
385
444
|
}
|
|
386
|
-
detectIndent(node) {
|
|
387
|
-
const input = this._state.source;
|
|
388
|
-
let i = node.start - 1;
|
|
389
|
-
while (i >= 0) {
|
|
390
|
-
if (input[i] === "\n") {
|
|
391
|
-
return input.slice(i + 1, node.start);
|
|
392
|
-
}
|
|
393
|
-
if (input[i] !== " " && input[i] !== "\t") {
|
|
394
|
-
return "";
|
|
395
|
-
}
|
|
396
|
-
i--;
|
|
397
|
-
}
|
|
398
|
-
return "";
|
|
399
|
-
}
|
|
400
445
|
}
|
|
401
|
-
exports.SyncEngine = SyncEngine;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type ModelElement, type ModelNode } from "../model/xml-api-model";
|
|
2
|
+
import type { XMLBinder } from "../model/xml-binder";
|
|
3
|
+
import type { EditorState } from "./editor-state";
|
|
4
|
+
import { Transaction } from "./transaction";
|
|
5
|
+
export declare class TransactionBuilder {
|
|
6
|
+
private state;
|
|
7
|
+
private binder;
|
|
8
|
+
constructor(state: EditorState, binder: XMLBinder);
|
|
9
|
+
setAttribute(modelNode: ModelElement, key: string, value: string): Transaction;
|
|
10
|
+
updateText(modelNode: ModelElement, text: string): Transaction;
|
|
11
|
+
removeNode(parent: ModelElement, child: ModelNode): Transaction;
|
|
12
|
+
replaceNode(target: ModelNode, content: ModelNode): Transaction;
|
|
13
|
+
insertNode(parent: ModelElement, child: ModelNode, index: number): Transaction;
|
|
14
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { detectIndent } from "../cst/cst-utils";
|
|
2
|
+
import { Formatter } from "../model/formatter";
|
|
3
|
+
import { ModelText, } from "../model/xml-api-model";
|
|
4
|
+
import { Transaction } from "./transaction";
|
|
5
|
+
export class TransactionBuilder {
|
|
6
|
+
constructor(state, binder) {
|
|
7
|
+
this.state = state;
|
|
8
|
+
this.binder = binder;
|
|
9
|
+
}
|
|
10
|
+
setAttribute(modelNode, key, value) {
|
|
11
|
+
if (!modelNode.cst)
|
|
12
|
+
throw new Error("Model node not linked to CST");
|
|
13
|
+
const patch = this.binder.calcSetAttributePatch(modelNode, key, value);
|
|
14
|
+
const tr = new Transaction(this.state);
|
|
15
|
+
if (patch) {
|
|
16
|
+
tr.replace(patch.start, patch.end, patch.text);
|
|
17
|
+
}
|
|
18
|
+
return tr;
|
|
19
|
+
}
|
|
20
|
+
updateText(modelNode, text) {
|
|
21
|
+
if (!modelNode.cst)
|
|
22
|
+
throw new Error("Model node not linked to CST");
|
|
23
|
+
const patch = this.binder.calcUpdateTextPatch(modelNode, text);
|
|
24
|
+
const tr = new Transaction(this.state);
|
|
25
|
+
if (patch) {
|
|
26
|
+
tr.replace(patch.start, patch.end, patch.text);
|
|
27
|
+
}
|
|
28
|
+
return tr;
|
|
29
|
+
}
|
|
30
|
+
removeNode(parent, child) {
|
|
31
|
+
if (!parent.cst)
|
|
32
|
+
throw new Error("Parent node not linked to CST");
|
|
33
|
+
const patch = this.binder.calcRemoveNodePatch(child);
|
|
34
|
+
const tr = new Transaction(this.state);
|
|
35
|
+
if (patch) {
|
|
36
|
+
tr.replace(patch.start, patch.end, patch.text);
|
|
37
|
+
}
|
|
38
|
+
return tr;
|
|
39
|
+
}
|
|
40
|
+
replaceNode(target, content) {
|
|
41
|
+
var _a;
|
|
42
|
+
if (!target.cst)
|
|
43
|
+
throw new Error("Model node not linked to CST");
|
|
44
|
+
// Formatting logic
|
|
45
|
+
let indentUnit = " ";
|
|
46
|
+
let currentIndent = "";
|
|
47
|
+
if (target.cst) {
|
|
48
|
+
currentIndent = detectIndent(target.cst, this.state.source) || "";
|
|
49
|
+
if ((_a = target.parent) === null || _a === void 0 ? void 0 : _a.cst) {
|
|
50
|
+
const parentIndent = detectIndent(target.parent.cst, this.state.source) || "";
|
|
51
|
+
if (currentIndent.startsWith(parentIndent)) {
|
|
52
|
+
const diff = currentIndent.slice(parentIndent.length);
|
|
53
|
+
if (diff.length > 0 && !diff.includes("\n")) {
|
|
54
|
+
indentUnit = diff;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
const formatter = new Formatter({ indent: indentUnit });
|
|
60
|
+
let newXml = formatter.format(content);
|
|
61
|
+
if (currentIndent && newXml.includes("\n")) {
|
|
62
|
+
newXml = newXml
|
|
63
|
+
.split("\n")
|
|
64
|
+
.map((line, index) => (index === 0 ? line : currentIndent + line))
|
|
65
|
+
.join("\n");
|
|
66
|
+
}
|
|
67
|
+
const patch = this.binder.calcReplaceNodePatch(target, newXml);
|
|
68
|
+
const tr = new Transaction(this.state);
|
|
69
|
+
if (patch) {
|
|
70
|
+
tr.replace(patch.start, patch.end, patch.text);
|
|
71
|
+
}
|
|
72
|
+
return tr;
|
|
73
|
+
}
|
|
74
|
+
insertNode(parent, child, index) {
|
|
75
|
+
if (!parent.cst)
|
|
76
|
+
throw new Error("Parent node not linked to CST");
|
|
77
|
+
// Determine basic indentation
|
|
78
|
+
const indentUnit = " ";
|
|
79
|
+
// Smart Formatting: Determine baseIndent and prefix/suffix
|
|
80
|
+
let baseIndent = "";
|
|
81
|
+
let prefix = "";
|
|
82
|
+
let suffix = "";
|
|
83
|
+
// Check if child is already in model (DOM usage)
|
|
84
|
+
const isAlreadyInModel = parent.children[index] === child;
|
|
85
|
+
// Scan backwards for significant node
|
|
86
|
+
let probe = isAlreadyInModel ? index - 1 : index - 1;
|
|
87
|
+
let refNode = null;
|
|
88
|
+
let newlineFound = false;
|
|
89
|
+
while (probe >= 0) {
|
|
90
|
+
const node = parent.children[probe];
|
|
91
|
+
if (node instanceof ModelText && node.text.trim().length === 0) {
|
|
92
|
+
if (node.text.includes("\n"))
|
|
93
|
+
newlineFound = true;
|
|
94
|
+
probe--;
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
refNode = node;
|
|
98
|
+
break;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
if (refNode && refNode.formatting.indent !== null) {
|
|
102
|
+
baseIndent = refNode.formatting.indent;
|
|
103
|
+
prefix = newlineFound ? baseIndent : `\n${baseIndent}`;
|
|
104
|
+
}
|
|
105
|
+
else if (!refNode) {
|
|
106
|
+
// Empty or first significant child
|
|
107
|
+
// Check next sibling to decide mode
|
|
108
|
+
const nextNode = isAlreadyInModel
|
|
109
|
+
? index + 1 < parent.children.length
|
|
110
|
+
? parent.children[index + 1]
|
|
111
|
+
: null
|
|
112
|
+
: index < parent.children.length
|
|
113
|
+
? parent.children[index]
|
|
114
|
+
: null;
|
|
115
|
+
if (nextNode && nextNode.formatting.indent === null) {
|
|
116
|
+
// Next is inline. Stay inline.
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
// Next is block (or doesn't exist).
|
|
120
|
+
// If parent has indent, assume block.
|
|
121
|
+
if (parent.formatting.indent !== null) {
|
|
122
|
+
baseIndent = parent.formatting.indent + indentUnit;
|
|
123
|
+
prefix = `\n${baseIndent}`;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
// Suffix logic: ensure closing tag is on new line if block mode
|
|
128
|
+
// If we are appending at the end, or next is end-tag
|
|
129
|
+
// Simple heuristic: if we added a newline prefix (block mode), add a newline suffix
|
|
130
|
+
if (prefix.includes("\n") || newlineFound) {
|
|
131
|
+
// Use parent's indent for the closing tag
|
|
132
|
+
suffix = `\n${parent.formatting.indent || ""}`;
|
|
133
|
+
}
|
|
134
|
+
const formatter = new Formatter({ indent: indentUnit, baseIndent });
|
|
135
|
+
const insertText = prefix + formatter.format(child) + suffix;
|
|
136
|
+
const patch = this.binder.calcInsertNodePatch(parent, index, insertText);
|
|
137
|
+
const tr = new Transaction(this.state);
|
|
138
|
+
if (patch) {
|
|
139
|
+
tr.replace(patch.start, patch.end, patch.text);
|
|
140
|
+
}
|
|
141
|
+
return tr;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
@@ -14,7 +14,10 @@ export declare class Transaction {
|
|
|
14
14
|
docChanged: boolean;
|
|
15
15
|
/** Indicates if this transaction originated from a remote source (collaboration). */
|
|
16
16
|
isRemote: boolean;
|
|
17
|
+
metadata: Map<string, any>;
|
|
17
18
|
constructor(startState: EditorState);
|
|
19
|
+
setMeta(key: string, value: any): this;
|
|
20
|
+
getMeta(key: string): any;
|
|
18
21
|
/**
|
|
19
22
|
* Adds a text change to the transaction.
|
|
20
23
|
* @param from Start index
|
|
@@ -1,20 +1,25 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.Transaction = void 0;
|
|
4
1
|
/**
|
|
5
2
|
* Represents a unit of change to the editor state.
|
|
6
3
|
* Currently focuses on text changes (patches).
|
|
7
4
|
*/
|
|
8
|
-
class Transaction {
|
|
9
|
-
// Placeholder for future selection and metadata
|
|
10
|
-
// public selection: Selection | null = null;
|
|
11
|
-
// public meta: Map<string, any> = new Map();
|
|
5
|
+
export class Transaction {
|
|
12
6
|
constructor(startState) {
|
|
13
7
|
this.startState = startState;
|
|
14
8
|
this.patches = [];
|
|
15
9
|
this.docChanged = false;
|
|
16
10
|
/** Indicates if this transaction originated from a remote source (collaboration). */
|
|
17
11
|
this.isRemote = false;
|
|
12
|
+
// biome-ignore lint/suspicious/noExplicitAny: Metadata can store any type
|
|
13
|
+
this.metadata = new Map();
|
|
14
|
+
}
|
|
15
|
+
// biome-ignore lint/suspicious/noExplicitAny: Metadata can store any type
|
|
16
|
+
setMeta(key, value) {
|
|
17
|
+
this.metadata.set(key, value);
|
|
18
|
+
return this;
|
|
19
|
+
}
|
|
20
|
+
// biome-ignore lint/suspicious/noExplicitAny: Metadata can store any type
|
|
21
|
+
getMeta(key) {
|
|
22
|
+
return this.metadata.get(key);
|
|
18
23
|
}
|
|
19
24
|
/**
|
|
20
25
|
* Adds a text change to the transaction.
|
|
@@ -49,4 +54,3 @@ class Transaction {
|
|
|
49
54
|
return source;
|
|
50
55
|
}
|
|
51
56
|
}
|
|
52
|
-
exports.Transaction = Transaction;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export interface Transaction {
|
|
2
|
+
timestamp: number;
|
|
2
3
|
redo: {
|
|
3
4
|
from: number;
|
|
4
5
|
to: number;
|
|
@@ -14,8 +15,11 @@ export declare class HistoryManager {
|
|
|
14
15
|
private undoStack;
|
|
15
16
|
private redoStack;
|
|
16
17
|
private maxHistory;
|
|
18
|
+
private mergeThreshold;
|
|
17
19
|
constructor(maxHistory?: number);
|
|
18
20
|
push(transaction: Transaction): void;
|
|
21
|
+
private shouldMerge;
|
|
22
|
+
private merge;
|
|
19
23
|
undo(): Transaction | null;
|
|
20
24
|
redo(): Transaction | null;
|
|
21
25
|
canUndo(): boolean;
|