@malva-ui/editor 0.1.13 → 0.1.14
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 +5 -5
- package/fesm2022/malva-ui-editor-ai.mjs +17 -0
- package/fesm2022/malva-ui-editor-ai.mjs.map +1 -0
- package/fesm2022/malva-ui-editor.mjs +10227 -0
- package/fesm2022/malva-ui-editor.mjs.map +1 -0
- package/package.json +35 -26
- package/types/malva-ui-editor-ai.d.ts +1 -0
- package/types/malva-ui-editor.d.ts +2995 -0
- package/ai/index.d.ts +0 -12
- package/ai/malva-ui-editor-ai.d.ts +0 -5
- package/esm2022/ai/index.js +0 -12
- package/esm2022/ai/index.js.map +0 -1
- package/esm2022/ai/malva-ui-editor-ai.js +0 -5
- package/esm2022/ai/malva-ui-editor-ai.js.map +0 -1
- package/esm2022/index.js +0 -37
- package/esm2022/index.js.map +0 -1
- package/esm2022/lib/ai/editor-ai-actions.js +0 -69
- package/esm2022/lib/ai/editor-ai-actions.js.map +0 -1
- package/esm2022/lib/ai/editor-ai-context.js +0 -612
- package/esm2022/lib/ai/editor-ai-context.js.map +0 -1
- package/esm2022/lib/ai/editor-ai-menu.js +0 -288
- package/esm2022/lib/ai/editor-ai-menu.js.map +0 -1
- package/esm2022/lib/ai/editor-ai-review-bar.js +0 -223
- package/esm2022/lib/ai/editor-ai-review-bar.js.map +0 -1
- package/esm2022/lib/ai/editor-ai-stream.js +0 -677
- package/esm2022/lib/ai/editor-ai-stream.js.map +0 -1
- package/esm2022/lib/ai/editor-ai-suggestions.js +0 -751
- package/esm2022/lib/ai/editor-ai-suggestions.js.map +0 -1
- package/esm2022/lib/ai/editor-ai.tokens.js +0 -10
- package/esm2022/lib/ai/editor-ai.tokens.js.map +0 -1
- package/esm2022/lib/ai/editor-ai.types.js +0 -1
- package/esm2022/lib/ai/editor-ai.types.js.map +0 -1
- package/esm2022/lib/editor/editor.js +0 -985
- package/esm2022/lib/editor/editor.js.map +0 -1
- package/esm2022/lib/editor-extension-preflight.js +0 -83
- package/esm2022/lib/editor-extension-preflight.js.map +0 -1
- package/esm2022/lib/editor-serialization.js +0 -152
- package/esm2022/lib/editor-serialization.js.map +0 -1
- package/esm2022/lib/editor-toolbar-context.js +0 -259
- package/esm2022/lib/editor-toolbar-context.js.map +0 -1
- package/esm2022/lib/editor.tokens.js +0 -10
- package/esm2022/lib/editor.tokens.js.map +0 -1
- package/esm2022/lib/editor.types.js +0 -48
- package/esm2022/lib/editor.types.js.map +0 -1
- package/esm2022/lib/extensions/editor-block-handle.js +0 -760
- package/esm2022/lib/extensions/editor-block-handle.js.map +0 -1
- package/esm2022/lib/extensions/editor-extensions.js +0 -321
- package/esm2022/lib/extensions/editor-extensions.js.map +0 -1
- package/esm2022/lib/extensions/editor-markdown-compatibility.js +0 -162
- package/esm2022/lib/extensions/editor-markdown-compatibility.js.map +0 -1
- package/esm2022/lib/extensions/editor-upload-placeholder.js +0 -156
- package/esm2022/lib/extensions/editor-upload-placeholder.js.map +0 -1
- package/esm2022/lib/status/editor-status.js +0 -64
- package/esm2022/lib/status/editor-status.js.map +0 -1
- package/esm2022/lib/toolbar/editor-alignment.js +0 -203
- package/esm2022/lib/toolbar/editor-alignment.js.map +0 -1
- package/esm2022/lib/toolbar/editor-block-insert.js +0 -101
- package/esm2022/lib/toolbar/editor-block-insert.js.map +0 -1
- package/esm2022/lib/toolbar/editor-color-control.js +0 -71
- package/esm2022/lib/toolbar/editor-color-control.js.map +0 -1
- package/esm2022/lib/toolbar/editor-command-button.js +0 -115
- package/esm2022/lib/toolbar/editor-command-button.js.map +0 -1
- package/esm2022/lib/toolbar/editor-heading.js +0 -266
- package/esm2022/lib/toolbar/editor-heading.js.map +0 -1
- package/esm2022/lib/toolbar/editor-highlight.js +0 -84
- package/esm2022/lib/toolbar/editor-highlight.js.map +0 -1
- package/esm2022/lib/toolbar/editor-image-upload-dialog.js +0 -289
- package/esm2022/lib/toolbar/editor-image-upload-dialog.js.map +0 -1
- package/esm2022/lib/toolbar/editor-image-upload.js +0 -315
- package/esm2022/lib/toolbar/editor-image-upload.js.map +0 -1
- package/esm2022/lib/toolbar/editor-inline-marks.js +0 -124
- package/esm2022/lib/toolbar/editor-inline-marks.js.map +0 -1
- package/esm2022/lib/toolbar/editor-link.js +0 -265
- package/esm2022/lib/toolbar/editor-link.js.map +0 -1
- package/esm2022/lib/toolbar/editor-list.js +0 -249
- package/esm2022/lib/toolbar/editor-list.js.map +0 -1
- package/esm2022/lib/toolbar/editor-table.js +0 -292
- package/esm2022/lib/toolbar/editor-table.js.map +0 -1
- package/esm2022/lib/toolbar/editor-text-color.js +0 -82
- package/esm2022/lib/toolbar/editor-text-color.js.map +0 -1
- package/esm2022/lib/toolbar/editor-toolbar-widget.js +0 -41
- package/esm2022/lib/toolbar/editor-toolbar-widget.js.map +0 -1
- package/esm2022/lib/toolbar/editor-toolbar.defs.js +0 -60
- package/esm2022/lib/toolbar/editor-toolbar.defs.js.map +0 -1
- package/esm2022/lib/toolbar/editor-toolbar.js +0 -826
- package/esm2022/lib/toolbar/editor-toolbar.js.map +0 -1
- package/esm2022/lib/toolbar/editor-undo-redo.js +0 -72
- package/esm2022/lib/toolbar/editor-undo-redo.js.map +0 -1
- package/esm2022/lib/toolbar/editor-zoom.js +0 -330
- package/esm2022/lib/toolbar/editor-zoom.js.map +0 -1
- package/esm2022/lib/upload/editor-image-upload-coordinator.js +0 -480
- package/esm2022/lib/upload/editor-image-upload-coordinator.js.map +0 -1
- package/esm2022/malva-ui-editor.js +0 -5
- package/esm2022/malva-ui-editor.js.map +0 -1
- package/index.d.ts +0 -35
- package/lib/ai/editor-ai-actions.d.ts +0 -27
- package/lib/ai/editor-ai-context.d.ts +0 -307
- package/lib/ai/editor-ai-menu.d.ts +0 -142
- package/lib/ai/editor-ai-review-bar.d.ts +0 -116
- package/lib/ai/editor-ai-stream.d.ts +0 -207
- package/lib/ai/editor-ai-suggestions.d.ts +0 -219
- package/lib/ai/editor-ai.tokens.d.ts +0 -10
- package/lib/ai/editor-ai.types.d.ts +0 -176
- package/lib/editor/editor.d.ts +0 -243
- package/lib/editor-extension-preflight.d.ts +0 -23
- package/lib/editor-serialization.d.ts +0 -74
- package/lib/editor-toolbar-context.d.ts +0 -116
- package/lib/editor.tokens.d.ts +0 -6
- package/lib/editor.types.d.ts +0 -194
- package/lib/extensions/editor-block-handle.d.ts +0 -59
- package/lib/extensions/editor-extensions.d.ts +0 -165
- package/lib/extensions/editor-markdown-compatibility.d.ts +0 -11
- package/lib/extensions/editor-upload-placeholder.d.ts +0 -75
- package/lib/status/editor-status.d.ts +0 -24
- package/lib/toolbar/editor-alignment.d.ts +0 -61
- package/lib/toolbar/editor-block-insert.d.ts +0 -26
- package/lib/toolbar/editor-color-control.d.ts +0 -32
- package/lib/toolbar/editor-command-button.d.ts +0 -29
- package/lib/toolbar/editor-heading.d.ts +0 -68
- package/lib/toolbar/editor-highlight.d.ts +0 -21
- package/lib/toolbar/editor-image-upload-dialog.d.ts +0 -96
- package/lib/toolbar/editor-image-upload.d.ts +0 -71
- package/lib/toolbar/editor-inline-marks.d.ts +0 -30
- package/lib/toolbar/editor-link.d.ts +0 -78
- package/lib/toolbar/editor-list.d.ts +0 -67
- package/lib/toolbar/editor-table.d.ts +0 -133
- package/lib/toolbar/editor-text-color.d.ts +0 -21
- package/lib/toolbar/editor-toolbar-widget.d.ts +0 -19
- package/lib/toolbar/editor-toolbar.d.ts +0 -157
- package/lib/toolbar/editor-toolbar.defs.d.ts +0 -35
- package/lib/toolbar/editor-undo-redo.d.ts +0 -26
- package/lib/toolbar/editor-zoom.d.ts +0 -108
- package/lib/upload/editor-image-upload-coordinator.d.ts +0 -55
- package/malva-ui-editor.d.ts +0 -5
|
@@ -1,751 +0,0 @@
|
|
|
1
|
-
import { closeHistory } from '@tiptap/pm/history';
|
|
2
|
-
import { Fragment, Slice } from '@tiptap/pm/model';
|
|
3
|
-
import { Plugin, PluginKey } from '@tiptap/pm/state';
|
|
4
|
-
import { Decoration, DecorationSet } from '@tiptap/pm/view';
|
|
5
|
-
/**
|
|
6
|
-
* CSS class of the inline decoration highlighting text a pending suggestion
|
|
7
|
-
* inserted into the document.
|
|
8
|
-
*
|
|
9
|
-
* The class is applied through a ProseMirror decoration held in plugin state,
|
|
10
|
-
* never through document content, so it can appear in no HTML, Markdown, or
|
|
11
|
-
* JSON serialization.
|
|
12
|
-
*/
|
|
13
|
-
export const MLV_EDITOR_AI_SUGGESTION_INSERT_CLASS = 'mlv-editor__ai-suggestion-insert';
|
|
14
|
-
/**
|
|
15
|
-
* CSS class of the widget decoration rendering text a pending suggestion
|
|
16
|
-
* removed from the document.
|
|
17
|
-
*
|
|
18
|
-
* The removed text lives only inside the widget element — a strikethrough,
|
|
19
|
-
* `aria-hidden` affordance — so it is absent from the document and from every
|
|
20
|
-
* serialization the moment the suggestion is applied.
|
|
21
|
-
*/
|
|
22
|
-
export const MLV_EDITOR_AI_SUGGESTION_DELETE_CLASS = 'mlv-editor__ai-suggestion-delete';
|
|
23
|
-
/**
|
|
24
|
-
* CSS class added on top of the insert and delete decorations of the one
|
|
25
|
-
* suggestion a review surface currently navigates
|
|
26
|
-
* ({@link MlvEditorAiSuggestionsSession.setCurrent}).
|
|
27
|
-
*
|
|
28
|
-
* A decoration-only outline affordance: it never enters document content or
|
|
29
|
-
* any serialization, and marking a suggestion current dispatches no document
|
|
30
|
-
* step, so it can never create a history entry.
|
|
31
|
-
*/
|
|
32
|
-
export const MLV_EDITOR_AI_SUGGESTION_CURRENT_CLASS = 'mlv-editor__ai-suggestion-current';
|
|
33
|
-
/**
|
|
34
|
-
* @internal Cell budget for the LCS table. Two texts whose token counts
|
|
35
|
-
* multiply past this bound (only reachable with degenerate, book-length
|
|
36
|
-
* "paragraphs") skip the quadratic diff and collapse to one replace run.
|
|
37
|
-
*/
|
|
38
|
-
const MAX_DIFF_CELLS = 1_000_000;
|
|
39
|
-
/** @internal Splits text into alternating whitespace and word tokens; the tokens partition the input exactly. */
|
|
40
|
-
const tokenize = (text) => text.match(/\s+|\S+/g) ?? [];
|
|
41
|
-
/**
|
|
42
|
-
* Token-level word diff between two plain texts, computed over a
|
|
43
|
-
* longest-common-subsequence of tokens. Tokens are maximal whitespace or
|
|
44
|
-
* non-whitespace runs, so whitespace changes are diffed like words.
|
|
45
|
-
* Consecutive non-equal tokens merge into one run; a run that both removes
|
|
46
|
-
* and adds text is a `'replace'`.
|
|
47
|
-
*
|
|
48
|
-
* Pure and framework-free. Returns an empty array for identical inputs.
|
|
49
|
-
*
|
|
50
|
-
* @param oldText Text currently in the document.
|
|
51
|
-
* @param newText Replacement text.
|
|
52
|
-
* @returns Change runs in ascending old-text offset order.
|
|
53
|
-
*/
|
|
54
|
-
export function mlvEditorAiWordDiff(oldText, newText) {
|
|
55
|
-
if (oldText === newText)
|
|
56
|
-
return [];
|
|
57
|
-
const oldTokens = tokenize(oldText);
|
|
58
|
-
const newTokens = tokenize(newText);
|
|
59
|
-
let prefix = 0;
|
|
60
|
-
while (prefix < oldTokens.length &&
|
|
61
|
-
prefix < newTokens.length &&
|
|
62
|
-
oldTokens[prefix] === newTokens[prefix]) {
|
|
63
|
-
prefix += 1;
|
|
64
|
-
}
|
|
65
|
-
let suffix = 0;
|
|
66
|
-
while (suffix < oldTokens.length - prefix &&
|
|
67
|
-
suffix < newTokens.length - prefix &&
|
|
68
|
-
oldTokens[oldTokens.length - 1 - suffix] ===
|
|
69
|
-
newTokens[newTokens.length - 1 - suffix]) {
|
|
70
|
-
suffix += 1;
|
|
71
|
-
}
|
|
72
|
-
const oldCore = oldTokens.slice(prefix, oldTokens.length - suffix);
|
|
73
|
-
const newCore = newTokens.slice(prefix, newTokens.length - suffix);
|
|
74
|
-
// Edit script over the cores as (op, token) pairs in document order.
|
|
75
|
-
const ops = [];
|
|
76
|
-
if ((oldCore.length + 1) * (newCore.length + 1) > MAX_DIFF_CELLS) {
|
|
77
|
-
// Degenerate size: one whole-core replacement instead of an LCS walk.
|
|
78
|
-
oldCore.forEach((token) => ops.push({ op: 'del', token }));
|
|
79
|
-
newCore.forEach((token) => ops.push({ op: 'ins', token }));
|
|
80
|
-
}
|
|
81
|
-
else {
|
|
82
|
-
// dp[i][j] = LCS length of oldCore[i..] and newCore[j..].
|
|
83
|
-
const width = newCore.length + 1;
|
|
84
|
-
const dp = new Uint32Array((oldCore.length + 1) * width);
|
|
85
|
-
for (let i = oldCore.length - 1; i >= 0; i--) {
|
|
86
|
-
for (let j = newCore.length - 1; j >= 0; j--) {
|
|
87
|
-
dp[i * width + j] =
|
|
88
|
-
oldCore[i] === newCore[j]
|
|
89
|
-
? dp[(i + 1) * width + j + 1] + 1
|
|
90
|
-
: Math.max(dp[(i + 1) * width + j], dp[i * width + j + 1]);
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
let i = 0;
|
|
94
|
-
let j = 0;
|
|
95
|
-
while (i < oldCore.length && j < newCore.length) {
|
|
96
|
-
if (oldCore[i] === newCore[j]) {
|
|
97
|
-
ops.push({ op: 'equal', token: oldCore[i] });
|
|
98
|
-
i += 1;
|
|
99
|
-
j += 1;
|
|
100
|
-
}
|
|
101
|
-
else if (dp[(i + 1) * width + j] >= dp[i * width + j + 1]) {
|
|
102
|
-
ops.push({ op: 'del', token: oldCore[i] });
|
|
103
|
-
i += 1;
|
|
104
|
-
}
|
|
105
|
-
else {
|
|
106
|
-
ops.push({ op: 'ins', token: newCore[j] });
|
|
107
|
-
j += 1;
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
while (i < oldCore.length)
|
|
111
|
-
ops.push({ op: 'del', token: oldCore[i++] });
|
|
112
|
-
while (j < newCore.length)
|
|
113
|
-
ops.push({ op: 'ins', token: newCore[j++] });
|
|
114
|
-
}
|
|
115
|
-
const runs = [];
|
|
116
|
-
let oldPos = oldTokens.slice(0, prefix).join('').length;
|
|
117
|
-
let pendingStart = -1;
|
|
118
|
-
let pendingOld = '';
|
|
119
|
-
let pendingNew = '';
|
|
120
|
-
const flush = () => {
|
|
121
|
-
if (pendingStart < 0)
|
|
122
|
-
return;
|
|
123
|
-
if (pendingOld.length > 0 && pendingNew.length > 0) {
|
|
124
|
-
runs.push({
|
|
125
|
-
kind: 'replace',
|
|
126
|
-
oldFrom: pendingStart,
|
|
127
|
-
oldTo: pendingStart + pendingOld.length,
|
|
128
|
-
oldText: pendingOld,
|
|
129
|
-
newText: pendingNew,
|
|
130
|
-
});
|
|
131
|
-
}
|
|
132
|
-
else if (pendingOld.length > 0) {
|
|
133
|
-
runs.push({
|
|
134
|
-
kind: 'delete',
|
|
135
|
-
oldFrom: pendingStart,
|
|
136
|
-
oldTo: pendingStart + pendingOld.length,
|
|
137
|
-
oldText: pendingOld,
|
|
138
|
-
newText: '',
|
|
139
|
-
});
|
|
140
|
-
}
|
|
141
|
-
else {
|
|
142
|
-
runs.push({
|
|
143
|
-
kind: 'insert',
|
|
144
|
-
oldFrom: pendingStart,
|
|
145
|
-
oldTo: pendingStart,
|
|
146
|
-
oldText: '',
|
|
147
|
-
newText: pendingNew,
|
|
148
|
-
});
|
|
149
|
-
}
|
|
150
|
-
pendingStart = -1;
|
|
151
|
-
pendingOld = '';
|
|
152
|
-
pendingNew = '';
|
|
153
|
-
};
|
|
154
|
-
for (const { op, token } of ops) {
|
|
155
|
-
if (op === 'equal') {
|
|
156
|
-
flush();
|
|
157
|
-
oldPos += token.length;
|
|
158
|
-
}
|
|
159
|
-
else if (op === 'del') {
|
|
160
|
-
if (pendingStart < 0)
|
|
161
|
-
pendingStart = oldPos;
|
|
162
|
-
pendingOld += token;
|
|
163
|
-
oldPos += token.length;
|
|
164
|
-
}
|
|
165
|
-
else {
|
|
166
|
-
if (pendingStart < 0)
|
|
167
|
-
pendingStart = oldPos;
|
|
168
|
-
pendingNew += token;
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
flush();
|
|
172
|
-
return runs;
|
|
173
|
-
}
|
|
174
|
-
/**
|
|
175
|
-
* @internal Key identifying the per-session suggestion plugin. Session
|
|
176
|
-
* transactions carry metadata under this key; transactions without it are
|
|
177
|
-
* foreign and subject to the remap-or-drop policy.
|
|
178
|
-
*/
|
|
179
|
-
const MLV_EDITOR_AI_SUGGESTIONS_KEY = new PluginKey('mlvEditorAiSuggestions');
|
|
180
|
-
/**
|
|
181
|
-
* @internal One active review session per editor. Starting a new session
|
|
182
|
-
* tears the previous one down first (its remaining suggestions stay applied —
|
|
183
|
-
* the accepted-by-default rule), so the shared key never registers twice.
|
|
184
|
-
*/
|
|
185
|
-
const activeSessions = new WeakMap();
|
|
186
|
-
/** @internal Monotonic source of session-unique suggestion identifiers. */
|
|
187
|
-
let suggestionIdSequence = 0;
|
|
188
|
-
/** @internal Allocates the next suggestion identifier. */
|
|
189
|
-
const nextSuggestionId = () => `mlv-ai-suggestion-${++suggestionIdSequence}`;
|
|
190
|
-
/** @internal Renders the strikethrough widget carrying one removed text. */
|
|
191
|
-
const renderDeleteWidget = (oldText, current) => () => {
|
|
192
|
-
const element = document.createElement('span');
|
|
193
|
-
element.className = current
|
|
194
|
-
? `${MLV_EDITOR_AI_SUGGESTION_DELETE_CLASS} ${MLV_EDITOR_AI_SUGGESTION_CURRENT_CLASS}`
|
|
195
|
-
: MLV_EDITOR_AI_SUGGESTION_DELETE_CLASS;
|
|
196
|
-
element.textContent = oldText;
|
|
197
|
-
// Purely visual affordance: the removed text must not be read as document
|
|
198
|
-
// content. The review surface announces outcomes separately.
|
|
199
|
-
element.setAttribute('aria-hidden', 'true');
|
|
200
|
-
return element;
|
|
201
|
-
};
|
|
202
|
-
/** @internal Builds the decoration set for a fresh entry list. */
|
|
203
|
-
const buildDecorations = (doc, entries, currentId) => {
|
|
204
|
-
const decorations = [];
|
|
205
|
-
for (const entry of entries) {
|
|
206
|
-
const current = entry.id === currentId;
|
|
207
|
-
if (entry.kind !== 'delete' && entry.from < entry.to) {
|
|
208
|
-
decorations.push(Decoration.inline(entry.from, entry.to, {
|
|
209
|
-
class: current
|
|
210
|
-
? `${MLV_EDITOR_AI_SUGGESTION_INSERT_CLASS} ${MLV_EDITOR_AI_SUGGESTION_CURRENT_CLASS}`
|
|
211
|
-
: MLV_EDITOR_AI_SUGGESTION_INSERT_CLASS,
|
|
212
|
-
}, { id: entry.id }));
|
|
213
|
-
}
|
|
214
|
-
if (entry.kind !== 'insert' && entry.oldText.length > 0) {
|
|
215
|
-
decorations.push(Decoration.widget(entry.from, renderDeleteWidget(entry.oldText, current), {
|
|
216
|
-
id: entry.id,
|
|
217
|
-
side: -1,
|
|
218
|
-
}));
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
return DecorationSet.create(doc, decorations);
|
|
222
|
-
};
|
|
223
|
-
/**
|
|
224
|
-
* @internal Remaps one entry through a session-owned transaction. Session
|
|
225
|
-
* edits (rejects) never overlap other entries, so positions map without the
|
|
226
|
-
* drop policy. Collapsed entries map with the same association their widget
|
|
227
|
-
* decoration uses (`side: -1`), keeping entry and decoration in lockstep.
|
|
228
|
-
*/
|
|
229
|
-
const remapEntryThroughOwn = (entry, mapping) => {
|
|
230
|
-
const collapsed = entry.kind === 'delete';
|
|
231
|
-
const from = mapping.map(entry.from, collapsed ? -1 : 1);
|
|
232
|
-
const to = collapsed ? from : mapping.map(entry.to, -1);
|
|
233
|
-
if (from === entry.from && to === entry.to)
|
|
234
|
-
return entry;
|
|
235
|
-
return { ...entry, from, to };
|
|
236
|
-
};
|
|
237
|
-
/**
|
|
238
|
-
* @internal Remaps one entry through a foreign transaction, or returns `null`
|
|
239
|
-
* when a step touches it — the concurrent-edit drop policy.
|
|
240
|
-
*
|
|
241
|
-
* A non-collapsed entry is touched by any step whose replaced range strictly
|
|
242
|
-
* overlaps `[from, to)`; a pure insertion strictly inside counts, insertions
|
|
243
|
-
* and deletions that only meet a boundary do not. A collapsed (`'delete'`)
|
|
244
|
-
* entry is touched by a replacement strictly spanning its anchor and by an
|
|
245
|
-
* insertion at exactly the anchor — which is also what makes undoing the
|
|
246
|
-
* original application (it re-inserts the removed text at the anchor) drop
|
|
247
|
-
* the widget along with everything else.
|
|
248
|
-
*
|
|
249
|
-
* Boundary association mirrors the decorations: inline ranges exclude
|
|
250
|
-
* content inserted at either edge (`inclusiveStart`/`inclusiveEnd` false),
|
|
251
|
-
* widgets stay put with `side: -1`.
|
|
252
|
-
*/
|
|
253
|
-
const remapEntryThroughForeign = (entry, mapping) => {
|
|
254
|
-
const collapsed = entry.kind === 'delete';
|
|
255
|
-
let from = entry.from;
|
|
256
|
-
let to = entry.to;
|
|
257
|
-
for (const stepMap of mapping.maps) {
|
|
258
|
-
let touched = false;
|
|
259
|
-
stepMap.forEach((oldStart, oldEnd) => {
|
|
260
|
-
if (collapsed) {
|
|
261
|
-
if ((oldStart < from && oldEnd > from) ||
|
|
262
|
-
(oldStart === oldEnd && oldStart === from)) {
|
|
263
|
-
touched = true;
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
else if (oldStart < to && oldEnd > from) {
|
|
267
|
-
touched = true;
|
|
268
|
-
}
|
|
269
|
-
});
|
|
270
|
-
if (touched)
|
|
271
|
-
return null;
|
|
272
|
-
from = stepMap.map(from, collapsed ? -1 : 1);
|
|
273
|
-
to = collapsed ? from : stepMap.map(to, -1);
|
|
274
|
-
}
|
|
275
|
-
if (from === entry.from && to === entry.to)
|
|
276
|
-
return entry;
|
|
277
|
-
return { ...entry, from, to };
|
|
278
|
-
};
|
|
279
|
-
/** @internal Removes every decoration belonging to the given ids. */
|
|
280
|
-
const removeDecorationsFor = (decorations, ids) => decorations.remove(decorations.find(undefined, undefined, (spec) => ids.has(spec.id)));
|
|
281
|
-
/** @internal Creates the per-session suggestion plugin. */
|
|
282
|
-
const createSuggestionsPlugin = () => new Plugin({
|
|
283
|
-
key: MLV_EDITOR_AI_SUGGESTIONS_KEY,
|
|
284
|
-
state: {
|
|
285
|
-
init: () => null,
|
|
286
|
-
apply: (transaction, state) => {
|
|
287
|
-
const metadata = transaction.getMeta(MLV_EDITOR_AI_SUGGESTIONS_KEY);
|
|
288
|
-
if (metadata?.type === 'init') {
|
|
289
|
-
// Entry positions were computed against the transaction's result
|
|
290
|
-
// document, so the decorations build against it directly.
|
|
291
|
-
return {
|
|
292
|
-
entries: metadata.entries,
|
|
293
|
-
currentId: null,
|
|
294
|
-
decorations: buildDecorations(transaction.doc, metadata.entries, null),
|
|
295
|
-
};
|
|
296
|
-
}
|
|
297
|
-
if (!state)
|
|
298
|
-
return null;
|
|
299
|
-
let entries = state.entries;
|
|
300
|
-
let currentId = state.currentId;
|
|
301
|
-
let decorations = state.decorations;
|
|
302
|
-
if (transaction.docChanged) {
|
|
303
|
-
if (metadata) {
|
|
304
|
-
entries = entries.map((entry) => remapEntryThroughOwn(entry, transaction.mapping));
|
|
305
|
-
decorations = decorations.map(transaction.mapping, transaction.doc);
|
|
306
|
-
}
|
|
307
|
-
else {
|
|
308
|
-
const survivors = [];
|
|
309
|
-
const dropped = new Set();
|
|
310
|
-
for (const entry of entries) {
|
|
311
|
-
const mapped = remapEntryThroughForeign(entry, transaction.mapping);
|
|
312
|
-
if (mapped)
|
|
313
|
-
survivors.push(mapped);
|
|
314
|
-
else
|
|
315
|
-
dropped.add(entry.id);
|
|
316
|
-
}
|
|
317
|
-
entries = survivors;
|
|
318
|
-
decorations = decorations.map(transaction.mapping, transaction.doc);
|
|
319
|
-
if (dropped.size > 0) {
|
|
320
|
-
decorations = removeDecorationsFor(decorations, dropped);
|
|
321
|
-
if (currentId !== null && dropped.has(currentId)) {
|
|
322
|
-
currentId = null;
|
|
323
|
-
}
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
}
|
|
327
|
-
if (metadata?.type === 'drop') {
|
|
328
|
-
const ids = new Set(metadata.ids);
|
|
329
|
-
entries = entries.filter((entry) => !ids.has(entry.id));
|
|
330
|
-
decorations = removeDecorationsFor(decorations, ids);
|
|
331
|
-
if (currentId !== null && ids.has(currentId))
|
|
332
|
-
currentId = null;
|
|
333
|
-
}
|
|
334
|
-
if (metadata?.type === 'clear') {
|
|
335
|
-
return {
|
|
336
|
-
entries: [],
|
|
337
|
-
currentId: null,
|
|
338
|
-
decorations: DecorationSet.empty,
|
|
339
|
-
};
|
|
340
|
-
}
|
|
341
|
-
if (metadata?.type === 'current') {
|
|
342
|
-
// The outline moves by rebuilding from the (possibly remapped)
|
|
343
|
-
// entries: entry and decoration positions stay in lockstep, so
|
|
344
|
-
// the rebuild is equivalent to the incrementally mapped set plus
|
|
345
|
-
// the new current marker.
|
|
346
|
-
return {
|
|
347
|
-
entries,
|
|
348
|
-
currentId: metadata.id,
|
|
349
|
-
decorations: buildDecorations(transaction.doc, entries, metadata.id),
|
|
350
|
-
};
|
|
351
|
-
}
|
|
352
|
-
if (entries === state.entries &&
|
|
353
|
-
currentId === state.currentId &&
|
|
354
|
-
decorations === state.decorations) {
|
|
355
|
-
return state;
|
|
356
|
-
}
|
|
357
|
-
return { entries, currentId, decorations };
|
|
358
|
-
},
|
|
359
|
-
},
|
|
360
|
-
props: {
|
|
361
|
-
decorations: (state) => MLV_EDITOR_AI_SUGGESTIONS_KEY.getState(state)?.decorations ?? null,
|
|
362
|
-
},
|
|
363
|
-
});
|
|
364
|
-
/**
|
|
365
|
-
* @internal True when `[from, to]` lies inside one textblock and contains
|
|
366
|
-
* only unmarked text nodes — the precondition for the token-level word diff,
|
|
367
|
-
* where one character is exactly one document position and inserted plain
|
|
368
|
-
* text can drop no formatting.
|
|
369
|
-
*/
|
|
370
|
-
const regionIsPlainInline = (doc, from, to) => {
|
|
371
|
-
const $from = doc.resolve(from);
|
|
372
|
-
const $to = doc.resolve(to);
|
|
373
|
-
if (!$from.sameParent($to) || !$from.parent.isTextblock)
|
|
374
|
-
return false;
|
|
375
|
-
let plain = true;
|
|
376
|
-
$from.parent.nodesBetween($from.parentOffset, $to.parentOffset, (node) => {
|
|
377
|
-
if (!node.isText || node.marks.length > 0)
|
|
378
|
-
plain = false;
|
|
379
|
-
return false;
|
|
380
|
-
});
|
|
381
|
-
return plain;
|
|
382
|
-
};
|
|
383
|
-
/**
|
|
384
|
-
* @internal Extracts the plain text of a parsed replacement that is exactly
|
|
385
|
-
* one paragraph of unmarked text nodes; `null` for anything richer, which
|
|
386
|
-
* routes the application through the whole-region fallback.
|
|
387
|
-
*/
|
|
388
|
-
const plainParagraphText = (blocks) => {
|
|
389
|
-
if (blocks.length !== 1 || blocks[0].type !== 'paragraph')
|
|
390
|
-
return null;
|
|
391
|
-
const content = blocks[0].content ?? [];
|
|
392
|
-
let text = '';
|
|
393
|
-
for (const child of content) {
|
|
394
|
-
if (child.type !== 'text' ||
|
|
395
|
-
typeof child.text !== 'string' ||
|
|
396
|
-
(child.marks?.length ?? 0) > 0) {
|
|
397
|
-
return null;
|
|
398
|
-
}
|
|
399
|
-
text += child.text;
|
|
400
|
-
}
|
|
401
|
-
return text;
|
|
402
|
-
};
|
|
403
|
-
/**
|
|
404
|
-
* @internal Parses the replacement Markdown into top-level blocks through the
|
|
405
|
-
* editor's Markdown manager; without a manager the raw text becomes one plain
|
|
406
|
-
* paragraph — the same fallback the streaming engine's commit uses. Returns
|
|
407
|
-
* `null` for unusable input.
|
|
408
|
-
*/
|
|
409
|
-
const parseReplacement = (editor, markdown) => {
|
|
410
|
-
const manager = editor.markdown;
|
|
411
|
-
if (!manager) {
|
|
412
|
-
return [{ type: 'paragraph', content: [{ type: 'text', text: markdown }] }];
|
|
413
|
-
}
|
|
414
|
-
try {
|
|
415
|
-
const parsed = manager.parse(markdown);
|
|
416
|
-
if (!Array.isArray(parsed.content) || parsed.content.length === 0) {
|
|
417
|
-
return null;
|
|
418
|
-
}
|
|
419
|
-
return parsed.content;
|
|
420
|
-
}
|
|
421
|
-
catch {
|
|
422
|
-
return null;
|
|
423
|
-
}
|
|
424
|
-
};
|
|
425
|
-
/**
|
|
426
|
-
* @internal Restores one entry's original content into the shared reject
|
|
427
|
-
* transaction. Insert entries delete their exact inline range. Other kinds
|
|
428
|
-
* try the precise `replace` with the stored slice first — exact for every
|
|
429
|
-
* token-diff entry — and fall back to the fitting `replaceRange` for
|
|
430
|
-
* whole-region entries whose paste-fitted boundaries no longer match the
|
|
431
|
-
* slice's open depths. A throwing `replace` adds no step, so the fallback
|
|
432
|
-
* runs against an unchanged transaction.
|
|
433
|
-
*/
|
|
434
|
-
const applyRestore = (tr, entry) => {
|
|
435
|
-
if (entry.kind === 'insert') {
|
|
436
|
-
tr.delete(entry.from, entry.to);
|
|
437
|
-
return;
|
|
438
|
-
}
|
|
439
|
-
try {
|
|
440
|
-
tr.replace(entry.from, entry.to, entry.inverted);
|
|
441
|
-
}
|
|
442
|
-
catch {
|
|
443
|
-
tr.replaceRange(entry.from, entry.to, entry.inverted);
|
|
444
|
-
}
|
|
445
|
-
};
|
|
446
|
-
/**
|
|
447
|
-
* Applies an AI replacement for `[from, to]` as reviewable tracked changes —
|
|
448
|
-
* the Phase 2 suggestion engine (storage option B of the design spec).
|
|
449
|
-
*
|
|
450
|
-
* Framework-free: only the editor instance and the replacement Markdown are
|
|
451
|
-
* needed — no Angular, no provider token, no network.
|
|
452
|
-
*
|
|
453
|
-
* Diff granularity (pragmatic, documented rule): when the original region is
|
|
454
|
-
* a single textblock of plain, unmarked text **and** the parsed replacement
|
|
455
|
-
* is a single paragraph of plain text, the two are diffed token-by-token
|
|
456
|
-
* (tokens are whitespace or word runs) over a longest common subsequence,
|
|
457
|
-
* producing individual insert/delete/replace suggestions. Any structural or
|
|
458
|
-
* formatted content on either side falls back to **one** whole-region
|
|
459
|
-
* replace suggestion.
|
|
460
|
-
*
|
|
461
|
-
* Representation: inserted text enters the document immediately and is
|
|
462
|
-
* highlighted by an inline decoration
|
|
463
|
-
* ({@link MLV_EDITOR_AI_SUGGESTION_INSERT_CLASS}); deleted text is removed
|
|
464
|
-
* from the document and re-rendered at its position by a strikethrough
|
|
465
|
-
* `aria-hidden` widget decoration
|
|
466
|
-
* ({@link MLV_EDITOR_AI_SUGGESTION_DELETE_CLASS}). Because both are
|
|
467
|
-
* decorations, `getHTML`/`getMarkdown`/`getJSON` during review serialize the
|
|
468
|
-
* document as-is — inserted text present, removed text absent — the spec's
|
|
469
|
-
* accepted-by-default rule. Hosts gate saves on pending suggestions.
|
|
470
|
-
*
|
|
471
|
-
* The whole application is exactly **one** history-visible transaction:
|
|
472
|
-
* one undo removes every suggested change at once — and, because the undo
|
|
473
|
-
* intersects every suggestion range, it also drops the whole session.
|
|
474
|
-
*
|
|
475
|
-
* Concurrent edits: user transactions remap all suggestion ranges through
|
|
476
|
-
* their step maps; an edit that intersects a suggestion's range drops that
|
|
477
|
-
* one suggestion (decorations removed, the current document text stands,
|
|
478
|
-
* nothing is restored). The session ends when every suggestion is resolved
|
|
479
|
-
* or dropped, and editor destruction cleans up listeners and plugin state.
|
|
480
|
-
* Starting a new session on an editor first ends the previous one, leaving
|
|
481
|
-
* its remaining suggestions applied (accepted-by-default).
|
|
482
|
-
*
|
|
483
|
-
* Undo-step semantics of the returned session are documented on
|
|
484
|
-
* {@link MlvEditorAiSuggestionsSession}.
|
|
485
|
-
*
|
|
486
|
-
* @param editor Live Tiptap editor the suggestions are applied to.
|
|
487
|
-
* @param options Region to rewrite and the replacement Markdown.
|
|
488
|
-
* @returns The review session, or `null` when nothing reviewable results —
|
|
489
|
-
* destroyed editor, out-of-range positions, unusable replacement, or a
|
|
490
|
-
* replacement identical to the region.
|
|
491
|
-
*/
|
|
492
|
-
export function applyMlvEditorAiSuggestions(editor, options) {
|
|
493
|
-
const { from, to, replacementMarkdown } = options;
|
|
494
|
-
if (editor.isDestroyed)
|
|
495
|
-
return null;
|
|
496
|
-
if (!Number.isInteger(from) ||
|
|
497
|
-
!Number.isInteger(to) ||
|
|
498
|
-
from < 0 ||
|
|
499
|
-
to < from ||
|
|
500
|
-
to > editor.state.doc.content.size) {
|
|
501
|
-
return null;
|
|
502
|
-
}
|
|
503
|
-
if (replacementMarkdown.trim().length === 0)
|
|
504
|
-
return null;
|
|
505
|
-
const blocks = parseReplacement(editor, replacementMarkdown);
|
|
506
|
-
if (!blocks)
|
|
507
|
-
return null;
|
|
508
|
-
const doc = editor.state.doc;
|
|
509
|
-
const plainReplacement = plainParagraphText(blocks);
|
|
510
|
-
const wordDiff = plainReplacement !== null && regionIsPlainInline(doc, from, to)
|
|
511
|
-
? mlvEditorAiWordDiff(doc.textBetween(from, to), plainReplacement)
|
|
512
|
-
: null;
|
|
513
|
-
// A word-diffable replacement identical to the region changes nothing.
|
|
514
|
-
if (wordDiff !== null && wordDiff.length === 0)
|
|
515
|
-
return null;
|
|
516
|
-
// One session per editor: the shared plugin key must never register twice.
|
|
517
|
-
activeSessions.get(editor)?.();
|
|
518
|
-
let active = true;
|
|
519
|
-
/** @internal Ends the session exactly once and detaches everything. */
|
|
520
|
-
const teardown = () => {
|
|
521
|
-
if (!active)
|
|
522
|
-
return;
|
|
523
|
-
active = false;
|
|
524
|
-
editor.off('transaction', onTransaction);
|
|
525
|
-
editor.off('destroy', onDestroy);
|
|
526
|
-
if (!editor.isDestroyed) {
|
|
527
|
-
try {
|
|
528
|
-
editor.unregisterPlugin(MLV_EDITOR_AI_SUGGESTIONS_KEY);
|
|
529
|
-
}
|
|
530
|
-
catch {
|
|
531
|
-
// A failing reconfigure must not break teardown; the listeners are
|
|
532
|
-
// already detached and the session is inert either way.
|
|
533
|
-
}
|
|
534
|
-
}
|
|
535
|
-
if (activeSessions.get(editor) === teardown)
|
|
536
|
-
activeSessions.delete(editor);
|
|
537
|
-
};
|
|
538
|
-
/**
|
|
539
|
-
* @internal Ends the session once every suggestion is resolved or dropped —
|
|
540
|
-
* including the drop-all that undoing the original application causes.
|
|
541
|
-
*/
|
|
542
|
-
const onTransaction = () => {
|
|
543
|
-
if (!active)
|
|
544
|
-
return;
|
|
545
|
-
const state = MLV_EDITOR_AI_SUGGESTIONS_KEY.getState(editor.state);
|
|
546
|
-
if (!state || state.entries.length === 0)
|
|
547
|
-
teardown();
|
|
548
|
-
};
|
|
549
|
-
/** @internal A destroyed editor leaves nothing to unregister. */
|
|
550
|
-
const onDestroy = () => teardown();
|
|
551
|
-
editor.registerPlugin(createSuggestionsPlugin());
|
|
552
|
-
editor.on('transaction', onTransaction);
|
|
553
|
-
editor.on('destroy', onDestroy);
|
|
554
|
-
activeSessions.set(editor, teardown);
|
|
555
|
-
try {
|
|
556
|
-
const tr = editor.state.tr;
|
|
557
|
-
const entries = [];
|
|
558
|
-
if (wordDiff !== null) {
|
|
559
|
-
// Token path: every run edits plain inline text of one textblock, so a
|
|
560
|
-
// character offset is a document offset and a running delta keeps the
|
|
561
|
-
// positions of later runs exact.
|
|
562
|
-
let delta = 0;
|
|
563
|
-
for (const run of wordDiff) {
|
|
564
|
-
const runFrom = from + run.oldFrom + delta;
|
|
565
|
-
const runTo = from + run.oldTo + delta;
|
|
566
|
-
const inverted = run.kind === 'insert'
|
|
567
|
-
? Slice.empty
|
|
568
|
-
: doc.slice(from + run.oldFrom, from + run.oldTo);
|
|
569
|
-
if (run.kind === 'delete') {
|
|
570
|
-
tr.delete(runFrom, runTo);
|
|
571
|
-
}
|
|
572
|
-
else {
|
|
573
|
-
tr.replaceWith(runFrom, runTo, editor.schema.text(run.newText));
|
|
574
|
-
}
|
|
575
|
-
entries.push({
|
|
576
|
-
id: nextSuggestionId(),
|
|
577
|
-
kind: run.kind,
|
|
578
|
-
from: runFrom,
|
|
579
|
-
to: run.kind === 'delete' ? runFrom : runFrom + run.newText.length,
|
|
580
|
-
oldText: run.oldText,
|
|
581
|
-
newText: run.newText,
|
|
582
|
-
inverted,
|
|
583
|
-
});
|
|
584
|
-
delta += run.newText.length - run.oldText.length;
|
|
585
|
-
}
|
|
586
|
-
}
|
|
587
|
-
else {
|
|
588
|
-
// Fallback: one whole-region replace. A single-paragraph replacement
|
|
589
|
-
// lands as inline content so a one-line result never splits the
|
|
590
|
-
// surrounding block (streaming-commit precedent); everything else lands
|
|
591
|
-
// as the block structure the Markdown describes, paste-fitted.
|
|
592
|
-
const inline = blocks.length === 1 && blocks[0].type === 'paragraph'
|
|
593
|
-
? (blocks[0].content ?? [])
|
|
594
|
-
: null;
|
|
595
|
-
const blockReplacement = !inline || inline.length === 0;
|
|
596
|
-
const fragment = Fragment.fromJSON(editor.schema, blockReplacement ? blocks : inline);
|
|
597
|
-
// Block content replacing a region whose edge sits at a textblock edge
|
|
598
|
-
// grows to that block's boundary; otherwise `replaceRange`'s fitting
|
|
599
|
-
// strands the emptied source block as a shell next to the inserted
|
|
600
|
-
// structure, and that shell would serialize (accepted-by-default) and
|
|
601
|
-
// survive a reject.
|
|
602
|
-
let replaceFrom = from;
|
|
603
|
-
let replaceTo = to;
|
|
604
|
-
if (blockReplacement && from < to) {
|
|
605
|
-
const $from = doc.resolve(from);
|
|
606
|
-
const $to = doc.resolve(to);
|
|
607
|
-
if ($from.depth > 0 &&
|
|
608
|
-
$from.parent.isTextblock &&
|
|
609
|
-
$from.parentOffset === 0) {
|
|
610
|
-
replaceFrom = $from.before();
|
|
611
|
-
}
|
|
612
|
-
if ($to.depth > 0 &&
|
|
613
|
-
$to.parent.isTextblock &&
|
|
614
|
-
$to.parentOffset === $to.parent.content.size) {
|
|
615
|
-
replaceTo = $to.after();
|
|
616
|
-
}
|
|
617
|
-
}
|
|
618
|
-
const inverted = doc.slice(replaceFrom, replaceTo);
|
|
619
|
-
tr.replaceRange(replaceFrom, replaceTo, new Slice(fragment, 0, 0));
|
|
620
|
-
// A replacement that reproduces the document is the no-changes case —
|
|
621
|
-
// the whole-region counterpart of the empty word diff above. Without
|
|
622
|
-
// this check an echoed formatted or structural region would land as one
|
|
623
|
-
// bogus no-op replace suggestion (and a no-op undo step).
|
|
624
|
-
if (tr.doc.eq(doc)) {
|
|
625
|
-
teardown();
|
|
626
|
-
return null;
|
|
627
|
-
}
|
|
628
|
-
const mappedFrom = tr.mapping.map(replaceFrom, -1);
|
|
629
|
-
const mappedTo = Math.max(mappedFrom, tr.mapping.map(replaceTo, 1));
|
|
630
|
-
entries.push({
|
|
631
|
-
id: nextSuggestionId(),
|
|
632
|
-
kind: 'replace',
|
|
633
|
-
from: mappedFrom,
|
|
634
|
-
to: mappedTo,
|
|
635
|
-
oldText: doc.textBetween(replaceFrom, replaceTo, ' '),
|
|
636
|
-
newText: tr.doc.textBetween(mappedFrom, mappedTo, ' '),
|
|
637
|
-
inverted,
|
|
638
|
-
});
|
|
639
|
-
}
|
|
640
|
-
tr.setMeta(MLV_EDITOR_AI_SUGGESTIONS_KEY, {
|
|
641
|
-
type: 'init',
|
|
642
|
-
entries,
|
|
643
|
-
});
|
|
644
|
-
closeHistory(tr);
|
|
645
|
-
editor.view.dispatch(tr);
|
|
646
|
-
}
|
|
647
|
-
catch {
|
|
648
|
-
teardown();
|
|
649
|
-
return null;
|
|
650
|
-
}
|
|
651
|
-
/** @internal Reads the current entries; empty once the session ended. */
|
|
652
|
-
const currentEntries = () => active
|
|
653
|
-
? (MLV_EDITOR_AI_SUGGESTIONS_KEY.getState(editor.state)?.entries ?? [])
|
|
654
|
-
: [];
|
|
655
|
-
return {
|
|
656
|
-
suggestions: () => currentEntries().map((entry) => ({
|
|
657
|
-
id: entry.id,
|
|
658
|
-
kind: entry.kind,
|
|
659
|
-
range: { from: entry.from, to: entry.to },
|
|
660
|
-
oldText: entry.oldText,
|
|
661
|
-
newText: entry.newText,
|
|
662
|
-
})),
|
|
663
|
-
accept: (id) => {
|
|
664
|
-
const entry = currentEntries().find((candidate) => candidate.id === id);
|
|
665
|
-
if (!entry)
|
|
666
|
-
return false;
|
|
667
|
-
// No document step: the applied change already is the accepted state,
|
|
668
|
-
// so this is a pure plugin-state update and creates no history entry.
|
|
669
|
-
const tr = editor.state.tr;
|
|
670
|
-
tr.setMeta(MLV_EDITOR_AI_SUGGESTIONS_KEY, {
|
|
671
|
-
type: 'drop',
|
|
672
|
-
ids: [id],
|
|
673
|
-
});
|
|
674
|
-
editor.view.dispatch(tr);
|
|
675
|
-
return true;
|
|
676
|
-
},
|
|
677
|
-
reject: (id) => {
|
|
678
|
-
const entry = currentEntries().find((candidate) => candidate.id === id);
|
|
679
|
-
if (!entry)
|
|
680
|
-
return false;
|
|
681
|
-
try {
|
|
682
|
-
const tr = editor.state.tr;
|
|
683
|
-
applyRestore(tr, entry);
|
|
684
|
-
tr.setMeta(MLV_EDITOR_AI_SUGGESTIONS_KEY, {
|
|
685
|
-
type: 'drop',
|
|
686
|
-
ids: [id],
|
|
687
|
-
});
|
|
688
|
-
closeHistory(tr);
|
|
689
|
-
editor.view.dispatch(tr);
|
|
690
|
-
return true;
|
|
691
|
-
}
|
|
692
|
-
catch {
|
|
693
|
-
return false;
|
|
694
|
-
}
|
|
695
|
-
},
|
|
696
|
-
acceptAll: () => {
|
|
697
|
-
if (currentEntries().length === 0)
|
|
698
|
-
return false;
|
|
699
|
-
const tr = editor.state.tr;
|
|
700
|
-
tr.setMeta(MLV_EDITOR_AI_SUGGESTIONS_KEY, {
|
|
701
|
-
type: 'clear',
|
|
702
|
-
});
|
|
703
|
-
editor.view.dispatch(tr);
|
|
704
|
-
return true;
|
|
705
|
-
},
|
|
706
|
-
setCurrent: (id) => {
|
|
707
|
-
if (!active)
|
|
708
|
-
return false;
|
|
709
|
-
const state = MLV_EDITOR_AI_SUGGESTIONS_KEY.getState(editor.state);
|
|
710
|
-
if (!state)
|
|
711
|
-
return false;
|
|
712
|
-
if (id !== null && !state.entries.some((entry) => entry.id === id)) {
|
|
713
|
-
return false;
|
|
714
|
-
}
|
|
715
|
-
if (state.currentId === id)
|
|
716
|
-
return true;
|
|
717
|
-
// No document step: the marker lives in plugin state and decorations
|
|
718
|
-
// only, so this dispatch can never create a history entry.
|
|
719
|
-
const tr = editor.state.tr;
|
|
720
|
-
tr.setMeta(MLV_EDITOR_AI_SUGGESTIONS_KEY, {
|
|
721
|
-
type: 'current',
|
|
722
|
-
id,
|
|
723
|
-
});
|
|
724
|
-
editor.view.dispatch(tr);
|
|
725
|
-
return true;
|
|
726
|
-
},
|
|
727
|
-
rejectAll: () => {
|
|
728
|
-
const entries = currentEntries();
|
|
729
|
-
if (entries.length === 0)
|
|
730
|
-
return false;
|
|
731
|
-
try {
|
|
732
|
-
const tr = editor.state.tr;
|
|
733
|
-
// Descending order keeps every earlier entry's positions valid while
|
|
734
|
-
// later ones are restored.
|
|
735
|
-
for (const entry of [...entries].sort((a, b) => b.from - a.from)) {
|
|
736
|
-
applyRestore(tr, entry);
|
|
737
|
-
}
|
|
738
|
-
tr.setMeta(MLV_EDITOR_AI_SUGGESTIONS_KEY, {
|
|
739
|
-
type: 'clear',
|
|
740
|
-
});
|
|
741
|
-
closeHistory(tr);
|
|
742
|
-
editor.view.dispatch(tr);
|
|
743
|
-
return true;
|
|
744
|
-
}
|
|
745
|
-
catch {
|
|
746
|
-
return false;
|
|
747
|
-
}
|
|
748
|
-
},
|
|
749
|
-
};
|
|
750
|
-
}
|
|
751
|
-
//# sourceMappingURL=editor-ai-suggestions.js.map
|