@wonderwhy-er/desktop-commander 0.2.39 → 0.2.40
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/dist/server.js +1 -1
- package/dist/ui/file-preview/preview-runtime.js +204 -153
- package/dist/ui/file-preview/src/markdown/controller.d.ts +7 -1
- package/dist/ui/file-preview/src/markdown/controller.js +135 -16
- package/dist/ui/file-preview/src/markdown/editor.d.ts +97 -1
- package/dist/ui/file-preview/src/markdown/editor.js +814 -26
- package/dist/ui/file-preview/src/model.d.ts +2 -1
- package/dist/utils/capture.js +1 -1
- package/dist/utils/toolHistory.d.ts +13 -0
- package/dist/utils/toolHistory.js +65 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +7 -1
- package/dist/ui/config-editor/app.js +0 -840
- package/dist/ui/config-editor/array-modal.d.ts +0 -19
- package/dist/ui/config-editor/array-modal.js +0 -185
- package/dist/ui/config-editor/main.d.ts +0 -1
- package/dist/ui/config-editor/main.js +0 -2
- package/dist/ui/config-editor/src/App.d.ts +0 -43
- package/dist/ui/config-editor/src/components/layout.d.ts +0 -4
- package/dist/ui/config-editor/src/components/layout.js +0 -83
- package/dist/ui/config-editor/src/components/toolbar.d.ts +0 -1
- package/dist/ui/config-editor/src/components/toolbar.js +0 -21
- package/dist/ui/config-editor/src/config-values.d.ts +0 -6
- package/dist/ui/config-editor/src/config-values.js +0 -61
- package/dist/ui/config-editor/src/contracts.d.ts +0 -14
- package/dist/ui/config-editor/src/contracts.js +0 -3
- package/dist/ui/config-editor/src/directory-browser.d.ts +0 -6
- package/dist/ui/config-editor/src/directory-browser.js +0 -71
- package/dist/ui/config-editor/src/layout.d.ts +0 -5
- package/dist/ui/config-editor/src/layout.js +0 -90
- package/dist/ui/config-editor/src/parsing.d.ts +0 -5
- package/dist/ui/config-editor/src/parsing.js +0 -50
- package/dist/ui/config-editor/src/toolbar.d.ts +0 -1
- package/dist/ui/config-editor/src/toolbar.js +0 -18
- package/dist/ui/config-editor/src/types.d.ts +0 -17
- package/dist/ui/config-editor/src/types.js +0 -3
- package/dist/ui/config-editor/src/utils/config-values.d.ts +0 -9
- package/dist/ui/config-editor/src/utils/config-values.js +0 -61
- package/dist/ui/config-editor/src/utils/directory-browser.d.ts +0 -31
- package/dist/ui/config-editor/src/utils/directory-browser.js +0 -201
- package/dist/ui/config-editor/src/utils/parsing.d.ts +0 -8
- package/dist/ui/config-editor/src/utils/parsing.js +0 -50
- package/dist/ui/file-preview/app.d.ts +0 -8
- package/dist/ui/file-preview/app.js +0 -2020
- package/dist/ui/file-preview/components/code-viewer.d.ts +0 -6
- package/dist/ui/file-preview/components/code-viewer.js +0 -73
- package/dist/ui/file-preview/components/highlighting.d.ts +0 -2
- package/dist/ui/file-preview/components/highlighting.js +0 -54
- package/dist/ui/file-preview/components/html-renderer.d.ts +0 -5
- package/dist/ui/file-preview/components/html-renderer.js +0 -47
- package/dist/ui/file-preview/components/markdown-renderer.d.ts +0 -1
- package/dist/ui/file-preview/components/markdown-renderer.js +0 -67
- package/dist/ui/file-preview/components/toolbar.d.ts +0 -6
- package/dist/ui/file-preview/components/toolbar.js +0 -75
- package/dist/ui/file-preview/image-preview.d.ts +0 -3
- package/dist/ui/file-preview/image-preview.js +0 -21
- package/dist/ui/file-preview/main.d.ts +0 -1
- package/dist/ui/file-preview/main.js +0 -5
- package/dist/ui/file-preview/markdown/editor.d.ts +0 -36
- package/dist/ui/file-preview/markdown/editor.js +0 -643
- package/dist/ui/file-preview/markdown/linking.d.ts +0 -9
- package/dist/ui/file-preview/markdown/linking.js +0 -210
- package/dist/ui/file-preview/markdown/outline.d.ts +0 -7
- package/dist/ui/file-preview/markdown/outline.js +0 -40
- package/dist/ui/file-preview/markdown/preview.d.ts +0 -8
- package/dist/ui/file-preview/markdown/preview.js +0 -33
- package/dist/ui/file-preview/markdown/slugify.d.ts +0 -3
- package/dist/ui/file-preview/markdown/slugify.js +0 -31
- package/dist/ui/file-preview/markdown/toc.d.ts +0 -11
- package/dist/ui/file-preview/markdown/toc.js +0 -75
- package/dist/ui/file-preview/markdown/utils.d.ts +0 -1
- package/dist/ui/file-preview/markdown/utils.js +0 -15
- package/dist/ui/file-preview/markdown/workspace-controller.d.ts +0 -25
- package/dist/ui/file-preview/markdown/workspace-controller.js +0 -40
- package/dist/ui/file-preview/src/components/CodeViewer.d.ts +0 -6
- package/dist/ui/file-preview/src/components/CodeViewer.js +0 -60
- package/dist/ui/file-preview/src/components/HtmlRenderer.d.ts +0 -8
- package/dist/ui/file-preview/src/components/HtmlRenderer.js +0 -45
- package/dist/ui/file-preview/src/components/MarkdownRenderer.d.ts +0 -1
- package/dist/ui/file-preview/src/components/MarkdownRenderer.js +0 -15
- package/dist/ui/file-preview/src/components/Toolbar.d.ts +0 -6
- package/dist/ui/file-preview/src/components/Toolbar.js +0 -75
- package/dist/ui/file-preview/src/components/editor-toolbar.d.ts +0 -15
- package/dist/ui/file-preview/src/components/editor-toolbar.js +0 -384
- package/dist/ui/file-preview/src/components/markdown-editor.d.ts +0 -29
- package/dist/ui/file-preview/src/components/markdown-editor.js +0 -535
- package/dist/ui/file-preview/src/markdown/block-merge.d.ts +0 -25
- package/dist/ui/file-preview/src/markdown/block-merge.js +0 -86
- package/dist/ui/file-preview/src/markdown/link-modal.d.ts +0 -13
- package/dist/ui/file-preview/src/markdown/link-modal.js +0 -213
- package/dist/ui/file-preview/src/markdown/raw-editor.d.ts +0 -8
- package/dist/ui/file-preview/src/markdown/raw-editor.js +0 -61
- package/dist/ui/file-preview/src/markdown/selection-toolbar.d.ts +0 -14
- package/dist/ui/file-preview/src/markdown/selection-toolbar.js +0 -128
- package/dist/ui/file-preview/src/markdown/toc.d.ts +0 -11
- package/dist/ui/file-preview/src/markdown/toc.js +0 -75
- package/dist/ui/file-preview/src/markdown-workspace/editor.d.ts +0 -36
- package/dist/ui/file-preview/src/markdown-workspace/editor.js +0 -643
- package/dist/ui/file-preview/src/markdown-workspace/linking.d.ts +0 -9
- package/dist/ui/file-preview/src/markdown-workspace/linking.js +0 -210
- package/dist/ui/file-preview/src/markdown-workspace/outline.d.ts +0 -7
- package/dist/ui/file-preview/src/markdown-workspace/outline.js +0 -40
- package/dist/ui/file-preview/src/markdown-workspace/preview.d.ts +0 -8
- package/dist/ui/file-preview/src/markdown-workspace/preview.js +0 -33
- package/dist/ui/file-preview/src/markdown-workspace/slugify.d.ts +0 -3
- package/dist/ui/file-preview/src/markdown-workspace/slugify.js +0 -31
- package/dist/ui/file-preview/src/markdown-workspace/toc.d.ts +0 -11
- package/dist/ui/file-preview/src/markdown-workspace/toc.js +0 -75
- package/dist/ui/file-preview/src/markdown-workspace/utils.d.ts +0 -1
- package/dist/ui/file-preview/src/markdown-workspace/utils.js +0 -15
- package/dist/ui/file-preview/src/markdown-workspace/workspace-controller.d.ts +0 -25
- package/dist/ui/file-preview/src/markdown-workspace/workspace-controller.js +0 -40
- package/dist/ui/file-preview/types.d.ts +0 -1
- package/dist/ui/file-preview/types.js +0 -1
- package/dist/ui/server-integration.d.ts +0 -13
- package/dist/ui/server-integration.js +0 -31
- package/dist/ui/shared/ToolHeader.d.ts +0 -9
- package/dist/ui/shared/ToolHeader.js +0 -29
- package/dist/ui/shared/app-bootstrap.d.ts +0 -9
- package/dist/ui/shared/app-bootstrap.js +0 -15
- package/dist/ui/shared/guards.d.ts +0 -1
- package/dist/ui/shared/guards.js +0 -3
- package/dist/ui/shared/host-lifecycle.d.ts +0 -17
- package/dist/ui/shared/host-lifecycle.js +0 -41
- package/dist/ui/shared/rpc-client.d.ts +0 -14
- package/dist/ui/shared/rpc-client.js +0 -72
- package/dist/ui/shared/theme-adaptation.d.ts +0 -10
- package/dist/ui/shared/theme-adaptation.js +0 -118
- package/dist/ui/shared/tool-header.d.ts +0 -9
- package/dist/ui/shared/tool-header.js +0 -25
- package/dist/utils/ui-call-context.d.ts +0 -8
- package/dist/utils/ui-call-context.js +0 -72
- /package/dist/ui/config-editor/{app.d.ts → src/app.d.ts} +0 -0
- /package/dist/ui/config-editor/src/{App.js → app.js} +0 -0
- /package/dist/ui/file-preview/src/{App.d.ts → app.d.ts} +0 -0
- /package/dist/ui/file-preview/src/{App.js → app.js} +0 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { MarkdownWorkspaceState, RenderBodyResult, RenderPayload } from '../model.js';
|
|
2
|
-
import { type MarkdownEditorView } from './editor.js';
|
|
2
|
+
import { type MarkdownEditRange, type MarkdownEditorView } from './editor.js';
|
|
3
3
|
import type { OpenConflictDialogOptions } from './conflict-dialog.js';
|
|
4
4
|
export interface MarkdownControllerDependencies {
|
|
5
5
|
callTool?: (name: string, args: Record<string, unknown>) => Promise<unknown | undefined>;
|
|
@@ -16,6 +16,11 @@ export interface MarkdownControllerDependencies {
|
|
|
16
16
|
trackUiEvent?: (event: string, params?: Record<string, unknown>) => void;
|
|
17
17
|
showConflictDialog?: (options: OpenConflictDialogOptions) => void;
|
|
18
18
|
}
|
|
19
|
+
interface EditBlock {
|
|
20
|
+
old_string: string;
|
|
21
|
+
new_string: string;
|
|
22
|
+
}
|
|
23
|
+
export declare function computeEditBlocks(oldText: string, newText: string, changedRanges?: MarkdownEditRange[]): EditBlock[];
|
|
19
24
|
export declare function createMarkdownController(dependencies: MarkdownControllerDependencies): {
|
|
20
25
|
attachHandlers: (payload: RenderPayload) => void;
|
|
21
26
|
buildBody: (payload: RenderPayload) => RenderBodyResult;
|
|
@@ -36,3 +41,4 @@ export declare function createMarkdownController(dependencies: MarkdownControlle
|
|
|
36
41
|
setEditorView: (payload: RenderPayload, view: MarkdownEditorView) => void;
|
|
37
42
|
};
|
|
38
43
|
export type MarkdownController = ReturnType<typeof createMarkdownController>;
|
|
44
|
+
export {};
|
|
@@ -8,6 +8,7 @@ import { extractMarkdownOutline } from './outline.js';
|
|
|
8
8
|
import { getRenderedMarkdownCopyText } from './preview.js';
|
|
9
9
|
import { slugifyMarkdownHeading } from './slugify.js';
|
|
10
10
|
import { getFileExtensionForAnalytics } from '../payload-utils.js';
|
|
11
|
+
const MAX_EDIT_BLOCK_LINES = 40;
|
|
11
12
|
function areOutlineItemsEqual(left, right) {
|
|
12
13
|
if (left.length !== right.length) {
|
|
13
14
|
return false;
|
|
@@ -35,9 +36,6 @@ function stripMarkdownExtension(filePath) {
|
|
|
35
36
|
function computeDiffHunks(oldLines, newLines) {
|
|
36
37
|
const oldLength = oldLines.length;
|
|
37
38
|
const newLength = newLines.length;
|
|
38
|
-
if (oldLength * newLength > 1000000) {
|
|
39
|
-
return [{ oldStart: 0, oldEnd: oldLength, newStart: 0, newEnd: newLength }];
|
|
40
|
-
}
|
|
41
39
|
const dp = Array.from({ length: oldLength + 1 }, () => Array(newLength + 1).fill(0));
|
|
42
40
|
for (let i = 1; i <= oldLength; i += 1) {
|
|
43
41
|
for (let j = 1; j <= newLength; j += 1) {
|
|
@@ -94,23 +92,113 @@ function mergeCloseHunks(hunks, minGap) {
|
|
|
94
92
|
}
|
|
95
93
|
return merged;
|
|
96
94
|
}
|
|
97
|
-
function
|
|
95
|
+
function mergeLineRanges(ranges) {
|
|
96
|
+
const sorted = ranges
|
|
97
|
+
.map((range) => ({ fromLine: Math.max(1, Math.floor(range.fromLine)), toLine: Math.max(1, Math.floor(range.toLine)) }))
|
|
98
|
+
.sort((left, right) => left.fromLine - right.fromLine || left.toLine - right.toLine);
|
|
99
|
+
const merged = [];
|
|
100
|
+
for (const range of sorted) {
|
|
101
|
+
const normalized = {
|
|
102
|
+
fromLine: Math.min(range.fromLine, range.toLine),
|
|
103
|
+
toLine: Math.max(range.fromLine, range.toLine),
|
|
104
|
+
};
|
|
105
|
+
const previous = merged[merged.length - 1];
|
|
106
|
+
if (previous && normalized.fromLine <= previous.toLine + 1) {
|
|
107
|
+
previous.toLine = Math.max(previous.toLine, normalized.toLine);
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
merged.push(normalized);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return merged;
|
|
114
|
+
}
|
|
115
|
+
function hunkIntersectsRanges(hunk, ranges) {
|
|
116
|
+
if (ranges.length === 0) {
|
|
117
|
+
return true;
|
|
118
|
+
}
|
|
119
|
+
const fromLine = Math.min(hunk.oldStart, hunk.newStart) + 1;
|
|
120
|
+
const toLine = Math.max(hunk.oldEnd, hunk.newEnd) + 1;
|
|
121
|
+
return ranges.some((range) => fromLine <= range.toLine && toLine >= range.fromLine);
|
|
122
|
+
}
|
|
123
|
+
function computeLineByLineHunks(oldLines, newLines) {
|
|
124
|
+
return computeAnchoredDiffHunks(oldLines, newLines, 0, oldLines.length, 0, newLines.length);
|
|
125
|
+
}
|
|
126
|
+
function computeAnchoredDiffHunks(oldLines, newLines, oldStart, oldEnd, newStart, newEnd) {
|
|
127
|
+
while (oldStart < oldEnd && newStart < newEnd && oldLines[oldStart] === newLines[newStart]) {
|
|
128
|
+
oldStart++;
|
|
129
|
+
newStart++;
|
|
130
|
+
}
|
|
131
|
+
while (oldStart < oldEnd && newStart < newEnd && oldLines[oldEnd - 1] === newLines[newEnd - 1]) {
|
|
132
|
+
oldEnd--;
|
|
133
|
+
newEnd--;
|
|
134
|
+
}
|
|
135
|
+
if (oldStart === oldEnd && newStart === newEnd) {
|
|
136
|
+
return [];
|
|
137
|
+
}
|
|
138
|
+
const oldLineCounts = new Map();
|
|
139
|
+
const newLineCounts = new Map();
|
|
140
|
+
for (let index = oldStart; index < oldEnd; index += 1) {
|
|
141
|
+
const current = oldLineCounts.get(oldLines[index]);
|
|
142
|
+
oldLineCounts.set(oldLines[index], { count: (current?.count ?? 0) + 1, index });
|
|
143
|
+
}
|
|
144
|
+
for (let index = newStart; index < newEnd; index += 1) {
|
|
145
|
+
const current = newLineCounts.get(newLines[index]);
|
|
146
|
+
newLineCounts.set(newLines[index], { count: (current?.count ?? 0) + 1, index });
|
|
147
|
+
}
|
|
148
|
+
for (let oldIndex = oldStart; oldIndex < oldEnd; oldIndex += 1) {
|
|
149
|
+
const oldEntry = oldLineCounts.get(oldLines[oldIndex]);
|
|
150
|
+
const newEntry = newLineCounts.get(oldLines[oldIndex]);
|
|
151
|
+
if (oldEntry?.count === 1 && newEntry?.count === 1) {
|
|
152
|
+
return [
|
|
153
|
+
...computeAnchoredDiffHunks(oldLines, newLines, oldStart, oldIndex, newStart, newEntry.index),
|
|
154
|
+
...computeAnchoredDiffHunks(oldLines, newLines, oldIndex + 1, oldEnd, newEntry.index + 1, newEnd),
|
|
155
|
+
];
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return [{ oldStart, oldEnd, newStart, newEnd }];
|
|
159
|
+
}
|
|
160
|
+
function splitOversizedEditBlock(oldText, newText) {
|
|
161
|
+
const oldLines = oldText.split('\n');
|
|
162
|
+
const newLines = newText.split('\n');
|
|
163
|
+
const blockCount = Math.ceil(Math.max(oldLines.length, newLines.length) / MAX_EDIT_BLOCK_LINES);
|
|
164
|
+
const blocks = [];
|
|
165
|
+
for (let blockIndex = 0; blockIndex < blockCount; blockIndex += 1) {
|
|
166
|
+
const oldStart = Math.floor((blockIndex * oldLines.length) / blockCount);
|
|
167
|
+
const oldEnd = Math.floor(((blockIndex + 1) * oldLines.length) / blockCount);
|
|
168
|
+
const newStart = Math.floor((blockIndex * newLines.length) / blockCount);
|
|
169
|
+
const newEnd = Math.floor(((blockIndex + 1) * newLines.length) / blockCount);
|
|
170
|
+
const old_string = oldLines.slice(oldStart, oldEnd).join('\n');
|
|
171
|
+
const new_string = newLines.slice(newStart, newEnd).join('\n');
|
|
172
|
+
if (old_string !== new_string) {
|
|
173
|
+
blocks.push({ old_string, new_string });
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return blocks;
|
|
177
|
+
}
|
|
178
|
+
function splitOversizedEditBlocks(blocks) {
|
|
179
|
+
return blocks.flatMap((block) => {
|
|
180
|
+
const lineCount = Math.max(block.old_string.split('\n').length, block.new_string.split('\n').length);
|
|
181
|
+
return lineCount > MAX_EDIT_BLOCK_LINES
|
|
182
|
+
? splitOversizedEditBlock(block.old_string, block.new_string)
|
|
183
|
+
: [block];
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
export function computeEditBlocks(oldText, newText, changedRanges = []) {
|
|
98
187
|
if (oldText === newText) {
|
|
99
188
|
return [];
|
|
100
189
|
}
|
|
101
190
|
const oldLines = oldText.split('\n');
|
|
102
191
|
const newLines = newText.split('\n');
|
|
103
|
-
const hunks =
|
|
192
|
+
const hunks = oldLines.length * newLines.length > 1000000
|
|
193
|
+
? computeLineByLineHunks(oldLines, newLines)
|
|
194
|
+
: computeDiffHunks(oldLines, newLines);
|
|
104
195
|
if (hunks.length === 0) {
|
|
105
196
|
return [];
|
|
106
197
|
}
|
|
107
198
|
const context = 3;
|
|
108
|
-
const
|
|
109
|
-
const
|
|
110
|
-
|
|
111
|
-
return [{ old_string: oldText, new_string: newText }];
|
|
112
|
-
}
|
|
113
|
-
return merged.map((hunk) => {
|
|
199
|
+
const normalizedRanges = mergeLineRanges(changedRanges);
|
|
200
|
+
const merged = mergeCloseHunks(hunks, context * 2 + 1).filter((hunk) => hunkIntersectsRanges(hunk, normalizedRanges));
|
|
201
|
+
const blocks = merged.map((hunk) => {
|
|
114
202
|
const contextBefore = Math.max(0, hunk.oldStart - context);
|
|
115
203
|
const contextAfter = Math.min(oldLines.length, hunk.oldEnd + context);
|
|
116
204
|
const oldBlock = oldLines.slice(contextBefore, contextAfter).join('\n');
|
|
@@ -121,6 +209,13 @@ function computeEditBlocks(oldText, newText) {
|
|
|
121
209
|
].join('\n');
|
|
122
210
|
return { old_string: oldBlock, new_string: newBlock };
|
|
123
211
|
}).filter((block) => block.old_string !== block.new_string);
|
|
212
|
+
if (blocks.length === 1 && blocks[0].old_string === oldText && blocks[0].new_string === newText) {
|
|
213
|
+
return splitOversizedEditBlock(oldText, newText);
|
|
214
|
+
}
|
|
215
|
+
return splitOversizedEditBlocks(blocks);
|
|
216
|
+
}
|
|
217
|
+
function applyEditBlocksToText(text, blocks) {
|
|
218
|
+
return blocks.reduce((current, block) => current.replace(block.old_string, block.new_string), text);
|
|
124
219
|
}
|
|
125
220
|
function isToolErrorResult(value) {
|
|
126
221
|
return typeof value === 'object' && value !== null;
|
|
@@ -176,6 +271,7 @@ export function createMarkdownController(dependencies) {
|
|
|
176
271
|
state.draftContent = nextDraftContent;
|
|
177
272
|
state.outline = extractMarkdownOutline(content);
|
|
178
273
|
state.dirty = nextDraftContent !== content;
|
|
274
|
+
state.dirtyLineRanges = [];
|
|
179
275
|
state.fileDeleted = false;
|
|
180
276
|
if (!state.outline.some((item) => item.id === state.activeHeadingId)) {
|
|
181
277
|
state.activeHeadingId = state.outline[0]?.id ?? null;
|
|
@@ -217,6 +313,7 @@ export function createMarkdownController(dependencies) {
|
|
|
217
313
|
outline,
|
|
218
314
|
mode: 'edit',
|
|
219
315
|
dirty: false,
|
|
316
|
+
dirtyLineRanges: [],
|
|
220
317
|
activeHeadingId: outline[0]?.id ?? null,
|
|
221
318
|
pendingAnchor: null,
|
|
222
319
|
notice: null,
|
|
@@ -541,6 +638,7 @@ export function createMarkdownController(dependencies) {
|
|
|
541
638
|
const filePath = workspaceState.filePath;
|
|
542
639
|
workspaceState.draftContent = workspaceState.fullDocumentContent;
|
|
543
640
|
workspaceState.dirty = false;
|
|
641
|
+
workspaceState.dirtyLineRanges = [];
|
|
544
642
|
workspaceState.error = null;
|
|
545
643
|
workspaceState.notice = null;
|
|
546
644
|
dependencies.rerender();
|
|
@@ -560,11 +658,12 @@ export function createMarkdownController(dependencies) {
|
|
|
560
658
|
state.error = null;
|
|
561
659
|
state.notice = null;
|
|
562
660
|
try {
|
|
563
|
-
const blocks = computeEditBlocks(state.fullDocumentContent, state.draftContent);
|
|
661
|
+
const blocks = computeEditBlocks(state.fullDocumentContent, state.draftContent, state.dirtyLineRanges);
|
|
564
662
|
if (blocks.length === 0) {
|
|
565
663
|
state.saving = false;
|
|
566
664
|
state.saveIndicator = 'idle';
|
|
567
665
|
state.dirty = false;
|
|
666
|
+
state.dirtyLineRanges = [];
|
|
568
667
|
return;
|
|
569
668
|
}
|
|
570
669
|
// Try each hunk independently. Previously the loop threw on the
|
|
@@ -606,16 +705,18 @@ export function createMarkdownController(dependencies) {
|
|
|
606
705
|
err.underlyingError = lastHardError;
|
|
607
706
|
throw err;
|
|
608
707
|
}
|
|
609
|
-
|
|
610
|
-
state.
|
|
708
|
+
const savedContent = applyEditBlocksToText(state.fullDocumentContent, blocks);
|
|
709
|
+
state.fullDocumentContent = savedContent;
|
|
710
|
+
state.sourceContent = savedContent;
|
|
711
|
+
state.draftContent = savedContent;
|
|
611
712
|
state.outline = extractMarkdownOutline(state.sourceContent);
|
|
612
713
|
state.dirty = false;
|
|
714
|
+
state.dirtyLineRanges = [];
|
|
613
715
|
state.saving = false;
|
|
614
716
|
state.saveIndicator = 'saved';
|
|
615
717
|
if (!state.outline.some((item) => item.id === state.activeHeadingId)) {
|
|
616
718
|
state.activeHeadingId = state.outline[0]?.id ?? null;
|
|
617
719
|
}
|
|
618
|
-
const savedContent = state.draftContent;
|
|
619
720
|
const currentPayload = dependencies.getCurrentPayload();
|
|
620
721
|
if (currentPayload) {
|
|
621
722
|
const statusLineMatch = currentPayload.content.match(/^(\[Reading [^\]]+\]\r?\n(?:\r?\n)?)/);
|
|
@@ -788,9 +889,24 @@ export function createMarkdownController(dependencies) {
|
|
|
788
889
|
currentFilePath: payload.filePath,
|
|
789
890
|
searchLinks: (query) => searchLinkTargets(payload.filePath, query),
|
|
790
891
|
loadHeadings: (targetPath) => loadLinkHeadings(payload.filePath, targetPath),
|
|
791
|
-
onChange: (value) => {
|
|
892
|
+
onChange: (value, editRanges) => {
|
|
893
|
+
if (value === state.draftContent) {
|
|
894
|
+
return;
|
|
895
|
+
}
|
|
792
896
|
state.draftContent = value;
|
|
793
897
|
state.dirty = value !== state.fullDocumentContent;
|
|
898
|
+
if (state.dirty) {
|
|
899
|
+
const nextRanges = editRanges && editRanges.length > 0
|
|
900
|
+
? editRanges
|
|
901
|
+
: [{ fromLine: 1, toLine: value.split('\n').length }];
|
|
902
|
+
state.dirtyLineRanges = mergeLineRanges([
|
|
903
|
+
...state.dirtyLineRanges,
|
|
904
|
+
...nextRanges,
|
|
905
|
+
]);
|
|
906
|
+
}
|
|
907
|
+
else {
|
|
908
|
+
state.dirtyLineRanges = [];
|
|
909
|
+
}
|
|
794
910
|
if (state.dirty && !editStartedFired) {
|
|
795
911
|
editStartedFired = true;
|
|
796
912
|
dependencies.trackUiEvent?.('markdown_edit_started', {
|
|
@@ -818,6 +934,9 @@ export function createMarkdownController(dependencies) {
|
|
|
818
934
|
}
|
|
819
935
|
},
|
|
820
936
|
onBlur: () => {
|
|
937
|
+
if (!state.dirty) {
|
|
938
|
+
return;
|
|
939
|
+
}
|
|
821
940
|
cancelAutosave();
|
|
822
941
|
void saveDocument();
|
|
823
942
|
},
|
|
@@ -1,4 +1,96 @@
|
|
|
1
|
+
import type { Extensions } from '@tiptap/core';
|
|
1
2
|
export type MarkdownEditorView = 'raw' | 'markdown';
|
|
3
|
+
/**
|
|
4
|
+
* Round-trip safety wrapper around Tiptap.
|
|
5
|
+
*
|
|
6
|
+
* Tiptap parses markdown into ProseMirror nodes and serializes back via
|
|
7
|
+
* tiptap-markdown. Both steps are inherently lossy — features like GFM
|
|
8
|
+
* tables, wikilinks, YAML frontmatter, escapable characters and exact
|
|
9
|
+
* whitespace can't be recovered exactly from the parsed tree. The wrappers
|
|
10
|
+
* below preserve those features by:
|
|
11
|
+
*
|
|
12
|
+
* 1. Stripping content the editor can't safely round-trip (YAML
|
|
13
|
+
* frontmatter, CRLF line endings) BEFORE handing markdown to Tiptap,
|
|
14
|
+
* and re-attaching it after serialization.
|
|
15
|
+
* 2. Calling existing helpers (rewriteWikiLinks / restoreWikiLinks) that
|
|
16
|
+
* replace `[[Page]]` with placeholder syntax Tiptap understands,
|
|
17
|
+
* then put it back on the way out.
|
|
18
|
+
* 3. Preserving a trailing newline if the original document ended with
|
|
19
|
+
* one — Tiptap's serializer always strips it.
|
|
20
|
+
*
|
|
21
|
+
* The shape of the safe region we save is captured in a `RoundTripContext`
|
|
22
|
+
* so post-processing can mirror it back. The test suite imports these
|
|
23
|
+
* helpers directly so the regression suite tests the EXACT same code path
|
|
24
|
+
* that production runs at autosave time.
|
|
25
|
+
*/
|
|
26
|
+
export interface RoundTripContext {
|
|
27
|
+
/** Original document text, retained for any final repair pass. */
|
|
28
|
+
originalInput: string;
|
|
29
|
+
/** YAML frontmatter prefix (`---\n…\n---\n`) stripped before editing. */
|
|
30
|
+
frontmatter: string;
|
|
31
|
+
/** Newlines between frontmatter end and first body line. Tiptap strips
|
|
32
|
+
* these; we put them back exactly. */
|
|
33
|
+
frontmatterGap: string;
|
|
34
|
+
/** Trailing newline that was on the original; restored after serialize. */
|
|
35
|
+
trailingNewline: string;
|
|
36
|
+
/** EOL convention of the original (`'\r\n'` or `'\n'`). */
|
|
37
|
+
eol: '\r\n' | '\n';
|
|
38
|
+
/** Code-text links (`[\`x\`](url)`) replaced with placeholders during
|
|
39
|
+
* preprocessing, restored after serialization. tiptap-markdown drops
|
|
40
|
+
* the URL when a link's text is purely inline code. */
|
|
41
|
+
codeLinks: Array<{
|
|
42
|
+
placeholder: string;
|
|
43
|
+
original: string;
|
|
44
|
+
}>;
|
|
45
|
+
/** `**...\`code\`...**` constructs replaced with placeholders. Tiptap's
|
|
46
|
+
* ProseMirror schema can't cleanly represent a bold mark wrapping
|
|
47
|
+
* inline code; it splits the bold around the code in non-obvious
|
|
48
|
+
* ways. */
|
|
49
|
+
boldCodeRuns: Array<{
|
|
50
|
+
placeholder: string;
|
|
51
|
+
original: string;
|
|
52
|
+
}>;
|
|
53
|
+
/** Count of `\|` escapes that were replaced with placeholders during
|
|
54
|
+
* preprocess. Each `\|` is replaced by a single ASCII token that
|
|
55
|
+
* restoration converts back to the literal `\|` in the output. */
|
|
56
|
+
pipeEscapeCount: number;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Pre-process a document before handing it to Tiptap. Returns a context
|
|
60
|
+
* object that `applyPostProcess` uses to restore stripped portions.
|
|
61
|
+
*/
|
|
62
|
+
export declare function preprocessForEditor(input: string): {
|
|
63
|
+
editorInput: string;
|
|
64
|
+
context: RoundTripContext;
|
|
65
|
+
};
|
|
66
|
+
/**
|
|
67
|
+
* Post-process the markdown Tiptap emits back into the user's expected
|
|
68
|
+
* form: re-attach frontmatter, restore wikilink syntax, restore trailing
|
|
69
|
+
* newline, undo unnecessary character escapes, and re-apply the original
|
|
70
|
+
* EOL convention.
|
|
71
|
+
*/
|
|
72
|
+
export declare function applyPostProcess(serialized: string, context: RoundTripContext): string;
|
|
73
|
+
/**
|
|
74
|
+
* Build the Tiptap extension array used by both production and the test
|
|
75
|
+
* suite. Centralising this means the regression tests exercise the exact
|
|
76
|
+
* configuration that ships, so any fix here flows through to autosave too.
|
|
77
|
+
*
|
|
78
|
+
* Notable choices:
|
|
79
|
+
* - StarterKit's strike extension is DISABLED. The default behaviour
|
|
80
|
+
* escapes literal `~` to `\~` (and breaks `~/path`) on serialize,
|
|
81
|
+
* because tiptap-markdown configures markdown-it with the strike
|
|
82
|
+
* plugin enabled, which in turn enables `~` as an escape target.
|
|
83
|
+
* Disabling strike costs us nothing visible (the editor never offered
|
|
84
|
+
* a strike button) and unblocks two #440 corruption modes.
|
|
85
|
+
*/
|
|
86
|
+
export declare function buildTiptapExtensions(): Extensions;
|
|
87
|
+
/**
|
|
88
|
+
* Convenience wrapper for tests and tools that want to mount the editor,
|
|
89
|
+
* call getMarkdown(), tear down, all in one shot. Production uses the
|
|
90
|
+
* pieces individually (preprocessForEditor at mount time, getMarkdown
|
|
91
|
+
* during autosave, applyPostProcess before writing to disk).
|
|
92
|
+
*/
|
|
93
|
+
export declare function roundTripMarkdown(input: string): string;
|
|
2
94
|
export interface MarkdownLinkSearchItem {
|
|
3
95
|
path: string;
|
|
4
96
|
title: string;
|
|
@@ -17,6 +109,10 @@ export interface MarkdownEditorHandle {
|
|
|
17
109
|
revealLine: (lineNumber: number, headingId?: string) => void;
|
|
18
110
|
setScrollTop: (scrollTop: number) => void;
|
|
19
111
|
}
|
|
112
|
+
export interface MarkdownEditRange {
|
|
113
|
+
fromLine: number;
|
|
114
|
+
toLine: number;
|
|
115
|
+
}
|
|
20
116
|
export declare function renderMarkdownCopyButton(): string;
|
|
21
117
|
export declare function renderMarkdownModeToggle(view: MarkdownEditorView): string;
|
|
22
118
|
export declare function renderMarkdownEditorShell(options: {
|
|
@@ -30,6 +126,6 @@ export declare function mountMarkdownEditor(options: {
|
|
|
30
126
|
currentFilePath: string;
|
|
31
127
|
searchLinks?: (query: string) => Promise<MarkdownLinkSearchItem[]>;
|
|
32
128
|
loadHeadings?: (filePath: string) => Promise<MarkdownLinkHeading[]>;
|
|
33
|
-
onChange: (value: string) => void;
|
|
129
|
+
onChange: (value: string, editRanges?: MarkdownEditRange[]) => void;
|
|
34
130
|
onBlur?: () => void;
|
|
35
131
|
}): MarkdownEditorHandle;
|