-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathdiff-preview.js
More file actions
201 lines (167 loc) · 5.84 KB
/
diff-preview.js
File metadata and controls
201 lines (167 loc) · 5.84 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
// src/diff-preview.js
// Visual diff preview using inline ghost decorations (track-changes style)
import { calculateCharDiff } from './diff-highlighter.js';
import { showDiffPreview, clearEditorHighlight, getMarkdownToPmMapping } from './editor.js';
import { getConflictState, restoreToPatch } from './timeline.js';
import { getConflictGroup } from './conflict-detection.js';
let previewState = {
active: false,
patchId: null,
oldText: '',
newText: '',
conflictGroup: null // Array of patch IDs in the same conflict group
};
/**
* Enter preview mode for a patch
* @param {number} patchId - Patch ID
* @param {string} oldText - Previous version text (current editor content)
* @param {string} newText - New version text (merged result)
*/
export function enterPreview(patchId, oldText, newText) {
// Get conflict group if this patch is in conflict
const conflictState = getConflictState();
const conflictGroup = getConflictGroup(patchId, conflictState.conflictGroups);
previewState = {
active: true,
patchId,
oldText,
newText,
conflictGroup
};
showPreviewBanner();
renderGhostPreview();
}
/**
* Exit preview mode
*/
export function exitPreview() {
previewState = {
active: false,
patchId: null,
oldText: '',
newText: '',
conflictGroup: null
};
hidePreviewBanner();
clearEditorHighlight();
}
/**
* Show the preview banner
*/
function showPreviewBanner() {
let banner = document.getElementById('diff-preview-banner');
if (!banner) {
banner = document.createElement('div');
banner.id = 'diff-preview-banner';
banner.innerHTML = `
<div class="preview-info">
<span class="preview-label">👁 Previewing Patch #<span id="preview-patch-id"></span></span>
<span class="preview-hint">(changes shown inline)</span>
</div>
<div class="preview-controls">
<button class="restore-btn" title="Restore document to this patch version">↺ Restore</button>
<button class="exit-btn">✕ Exit Preview</button>
</div>
`;
const editorContainer = document.getElementById('editor');
if (editorContainer) {
editorContainer.parentElement.insertBefore(banner, editorContainer);
}
// Wire up Exit Preview button
banner.querySelector('.exit-btn').addEventListener('click', () => {
exitPreview();
});
// Wire up Restore button
banner.querySelector('.restore-btn').addEventListener('click', async () => {
if (previewState.patchId) {
const success = await restoreToPatch(previewState.patchId);
if (success) {
exitPreview();
}
}
});
}
// Update patch ID
const patchIdEl = banner.querySelector('#preview-patch-id');
if (patchIdEl) {
patchIdEl.textContent = previewState.patchId;
}
banner.style.display = 'flex';
}
/**
* Hide the preview banner
*/
function hidePreviewBanner() {
const banner = document.getElementById('diff-preview-banner');
if (banner) {
banner.style.display = 'none';
}
}
/**
* Render the ghost preview using inline decorations
*/
function renderGhostPreview() {
if (!previewState.active) return;
// Get character-to-PM-position mapping
const { charToPm } = getMarkdownToPmMapping();
// For coordinate mapping, we don't need 'pmText' stripping.
// The diff is calculated against the original Markdown text (which we don't have here easily?)
// Wait, renderGhostPreview relies on `calculateCharDiff` between `oldText` and `newText`.
// oldText was `pmText`?
// In strict mode, previewState.oldText SHOULD be the Markdown Content?
// If we use getMarkdownToPmMapping, we assume `previewState.oldText` is the FULL MARKDOWN.
// And `previewState.newText` is the NEW FULL MARKDOWN.
// So `diff` is computed on Markdown chars.
// And `charToPm` maps Markdown chars to PM.
// This is correct.
const diff = calculateCharDiff(previewState.oldText, previewState.newText);
// Convert diff operations to PM-position-based operations
const operations = [];
let oldOffset = 0; // Track position in oldPlainText (which matches pmText)
for (const op of diff) {
if (op.type === 'equal') {
// Advance position in old text
oldOffset += op.text.length;
} else if (op.type === 'delete') {
// Text being removed - highlight in editor
const fromChar = oldOffset;
const toChar = oldOffset + op.text.length;
// Map character offsets to PM positions
const fromPm = charToPm(fromChar);
let toPm = charToPm(toChar);
// Debug
// Ensure minimum range of 1 for non-empty deletes
if (op.text.length > 0 && toPm <= fromPm) {
toPm = fromPm + 1;
}
if (fromPm < toPm) {
operations.push({
type: 'delete',
from: fromPm,
to: toPm
});
}
oldOffset += op.text.length;
} else if (op.type === 'add') {
// Text being added - show as ghost insert widget
const posPm = charToPm(oldOffset);
// Debug
operations.push({
type: 'add',
text: op.text,
pos: posPm
});
// Don't advance oldOffset for additions
}
}
// Debug final operations
// Apply the decorations to the editor
showDiffPreview(operations);
}
/**
* Check if preview mode is active
* @returns {boolean}
*/
export function isPreviewActive() {
return previewState.active;
}