@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.
Files changed (137) hide show
  1. package/dist/server.js +1 -1
  2. package/dist/ui/file-preview/preview-runtime.js +204 -153
  3. package/dist/ui/file-preview/src/markdown/controller.d.ts +7 -1
  4. package/dist/ui/file-preview/src/markdown/controller.js +135 -16
  5. package/dist/ui/file-preview/src/markdown/editor.d.ts +97 -1
  6. package/dist/ui/file-preview/src/markdown/editor.js +814 -26
  7. package/dist/ui/file-preview/src/model.d.ts +2 -1
  8. package/dist/utils/capture.js +1 -1
  9. package/dist/utils/toolHistory.d.ts +13 -0
  10. package/dist/utils/toolHistory.js +65 -0
  11. package/dist/version.d.ts +1 -1
  12. package/dist/version.js +1 -1
  13. package/package.json +7 -1
  14. package/dist/ui/config-editor/app.js +0 -840
  15. package/dist/ui/config-editor/array-modal.d.ts +0 -19
  16. package/dist/ui/config-editor/array-modal.js +0 -185
  17. package/dist/ui/config-editor/main.d.ts +0 -1
  18. package/dist/ui/config-editor/main.js +0 -2
  19. package/dist/ui/config-editor/src/App.d.ts +0 -43
  20. package/dist/ui/config-editor/src/components/layout.d.ts +0 -4
  21. package/dist/ui/config-editor/src/components/layout.js +0 -83
  22. package/dist/ui/config-editor/src/components/toolbar.d.ts +0 -1
  23. package/dist/ui/config-editor/src/components/toolbar.js +0 -21
  24. package/dist/ui/config-editor/src/config-values.d.ts +0 -6
  25. package/dist/ui/config-editor/src/config-values.js +0 -61
  26. package/dist/ui/config-editor/src/contracts.d.ts +0 -14
  27. package/dist/ui/config-editor/src/contracts.js +0 -3
  28. package/dist/ui/config-editor/src/directory-browser.d.ts +0 -6
  29. package/dist/ui/config-editor/src/directory-browser.js +0 -71
  30. package/dist/ui/config-editor/src/layout.d.ts +0 -5
  31. package/dist/ui/config-editor/src/layout.js +0 -90
  32. package/dist/ui/config-editor/src/parsing.d.ts +0 -5
  33. package/dist/ui/config-editor/src/parsing.js +0 -50
  34. package/dist/ui/config-editor/src/toolbar.d.ts +0 -1
  35. package/dist/ui/config-editor/src/toolbar.js +0 -18
  36. package/dist/ui/config-editor/src/types.d.ts +0 -17
  37. package/dist/ui/config-editor/src/types.js +0 -3
  38. package/dist/ui/config-editor/src/utils/config-values.d.ts +0 -9
  39. package/dist/ui/config-editor/src/utils/config-values.js +0 -61
  40. package/dist/ui/config-editor/src/utils/directory-browser.d.ts +0 -31
  41. package/dist/ui/config-editor/src/utils/directory-browser.js +0 -201
  42. package/dist/ui/config-editor/src/utils/parsing.d.ts +0 -8
  43. package/dist/ui/config-editor/src/utils/parsing.js +0 -50
  44. package/dist/ui/file-preview/app.d.ts +0 -8
  45. package/dist/ui/file-preview/app.js +0 -2020
  46. package/dist/ui/file-preview/components/code-viewer.d.ts +0 -6
  47. package/dist/ui/file-preview/components/code-viewer.js +0 -73
  48. package/dist/ui/file-preview/components/highlighting.d.ts +0 -2
  49. package/dist/ui/file-preview/components/highlighting.js +0 -54
  50. package/dist/ui/file-preview/components/html-renderer.d.ts +0 -5
  51. package/dist/ui/file-preview/components/html-renderer.js +0 -47
  52. package/dist/ui/file-preview/components/markdown-renderer.d.ts +0 -1
  53. package/dist/ui/file-preview/components/markdown-renderer.js +0 -67
  54. package/dist/ui/file-preview/components/toolbar.d.ts +0 -6
  55. package/dist/ui/file-preview/components/toolbar.js +0 -75
  56. package/dist/ui/file-preview/image-preview.d.ts +0 -3
  57. package/dist/ui/file-preview/image-preview.js +0 -21
  58. package/dist/ui/file-preview/main.d.ts +0 -1
  59. package/dist/ui/file-preview/main.js +0 -5
  60. package/dist/ui/file-preview/markdown/editor.d.ts +0 -36
  61. package/dist/ui/file-preview/markdown/editor.js +0 -643
  62. package/dist/ui/file-preview/markdown/linking.d.ts +0 -9
  63. package/dist/ui/file-preview/markdown/linking.js +0 -210
  64. package/dist/ui/file-preview/markdown/outline.d.ts +0 -7
  65. package/dist/ui/file-preview/markdown/outline.js +0 -40
  66. package/dist/ui/file-preview/markdown/preview.d.ts +0 -8
  67. package/dist/ui/file-preview/markdown/preview.js +0 -33
  68. package/dist/ui/file-preview/markdown/slugify.d.ts +0 -3
  69. package/dist/ui/file-preview/markdown/slugify.js +0 -31
  70. package/dist/ui/file-preview/markdown/toc.d.ts +0 -11
  71. package/dist/ui/file-preview/markdown/toc.js +0 -75
  72. package/dist/ui/file-preview/markdown/utils.d.ts +0 -1
  73. package/dist/ui/file-preview/markdown/utils.js +0 -15
  74. package/dist/ui/file-preview/markdown/workspace-controller.d.ts +0 -25
  75. package/dist/ui/file-preview/markdown/workspace-controller.js +0 -40
  76. package/dist/ui/file-preview/src/components/CodeViewer.d.ts +0 -6
  77. package/dist/ui/file-preview/src/components/CodeViewer.js +0 -60
  78. package/dist/ui/file-preview/src/components/HtmlRenderer.d.ts +0 -8
  79. package/dist/ui/file-preview/src/components/HtmlRenderer.js +0 -45
  80. package/dist/ui/file-preview/src/components/MarkdownRenderer.d.ts +0 -1
  81. package/dist/ui/file-preview/src/components/MarkdownRenderer.js +0 -15
  82. package/dist/ui/file-preview/src/components/Toolbar.d.ts +0 -6
  83. package/dist/ui/file-preview/src/components/Toolbar.js +0 -75
  84. package/dist/ui/file-preview/src/components/editor-toolbar.d.ts +0 -15
  85. package/dist/ui/file-preview/src/components/editor-toolbar.js +0 -384
  86. package/dist/ui/file-preview/src/components/markdown-editor.d.ts +0 -29
  87. package/dist/ui/file-preview/src/components/markdown-editor.js +0 -535
  88. package/dist/ui/file-preview/src/markdown/block-merge.d.ts +0 -25
  89. package/dist/ui/file-preview/src/markdown/block-merge.js +0 -86
  90. package/dist/ui/file-preview/src/markdown/link-modal.d.ts +0 -13
  91. package/dist/ui/file-preview/src/markdown/link-modal.js +0 -213
  92. package/dist/ui/file-preview/src/markdown/raw-editor.d.ts +0 -8
  93. package/dist/ui/file-preview/src/markdown/raw-editor.js +0 -61
  94. package/dist/ui/file-preview/src/markdown/selection-toolbar.d.ts +0 -14
  95. package/dist/ui/file-preview/src/markdown/selection-toolbar.js +0 -128
  96. package/dist/ui/file-preview/src/markdown/toc.d.ts +0 -11
  97. package/dist/ui/file-preview/src/markdown/toc.js +0 -75
  98. package/dist/ui/file-preview/src/markdown-workspace/editor.d.ts +0 -36
  99. package/dist/ui/file-preview/src/markdown-workspace/editor.js +0 -643
  100. package/dist/ui/file-preview/src/markdown-workspace/linking.d.ts +0 -9
  101. package/dist/ui/file-preview/src/markdown-workspace/linking.js +0 -210
  102. package/dist/ui/file-preview/src/markdown-workspace/outline.d.ts +0 -7
  103. package/dist/ui/file-preview/src/markdown-workspace/outline.js +0 -40
  104. package/dist/ui/file-preview/src/markdown-workspace/preview.d.ts +0 -8
  105. package/dist/ui/file-preview/src/markdown-workspace/preview.js +0 -33
  106. package/dist/ui/file-preview/src/markdown-workspace/slugify.d.ts +0 -3
  107. package/dist/ui/file-preview/src/markdown-workspace/slugify.js +0 -31
  108. package/dist/ui/file-preview/src/markdown-workspace/toc.d.ts +0 -11
  109. package/dist/ui/file-preview/src/markdown-workspace/toc.js +0 -75
  110. package/dist/ui/file-preview/src/markdown-workspace/utils.d.ts +0 -1
  111. package/dist/ui/file-preview/src/markdown-workspace/utils.js +0 -15
  112. package/dist/ui/file-preview/src/markdown-workspace/workspace-controller.d.ts +0 -25
  113. package/dist/ui/file-preview/src/markdown-workspace/workspace-controller.js +0 -40
  114. package/dist/ui/file-preview/types.d.ts +0 -1
  115. package/dist/ui/file-preview/types.js +0 -1
  116. package/dist/ui/server-integration.d.ts +0 -13
  117. package/dist/ui/server-integration.js +0 -31
  118. package/dist/ui/shared/ToolHeader.d.ts +0 -9
  119. package/dist/ui/shared/ToolHeader.js +0 -29
  120. package/dist/ui/shared/app-bootstrap.d.ts +0 -9
  121. package/dist/ui/shared/app-bootstrap.js +0 -15
  122. package/dist/ui/shared/guards.d.ts +0 -1
  123. package/dist/ui/shared/guards.js +0 -3
  124. package/dist/ui/shared/host-lifecycle.d.ts +0 -17
  125. package/dist/ui/shared/host-lifecycle.js +0 -41
  126. package/dist/ui/shared/rpc-client.d.ts +0 -14
  127. package/dist/ui/shared/rpc-client.js +0 -72
  128. package/dist/ui/shared/theme-adaptation.d.ts +0 -10
  129. package/dist/ui/shared/theme-adaptation.js +0 -118
  130. package/dist/ui/shared/tool-header.d.ts +0 -9
  131. package/dist/ui/shared/tool-header.js +0 -25
  132. package/dist/utils/ui-call-context.d.ts +0 -8
  133. package/dist/utils/ui-call-context.js +0 -72
  134. /package/dist/ui/config-editor/{app.d.ts → src/app.d.ts} +0 -0
  135. /package/dist/ui/config-editor/src/{App.js → app.js} +0 -0
  136. /package/dist/ui/file-preview/src/{App.d.ts → app.d.ts} +0 -0
  137. /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 computeEditBlocks(oldText, newText) {
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 = computeDiffHunks(oldLines, newLines);
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 merged = mergeCloseHunks(hunks, context * 2 + 1);
109
- const totalChanged = merged.reduce((sum, hunk) => sum + (hunk.oldEnd - hunk.oldStart), 0);
110
- if (totalChanged > oldLines.length * 0.7) {
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
- state.fullDocumentContent = state.draftContent;
610
- state.sourceContent = state.draftContent;
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;