vzcode 2.17.0 → 2.21.0
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/assets/{bindings_wasm_bg-D9vNLG9F.wasm → bindings_wasm_bg-9w8E-TmY.wasm} +0 -0
- package/dist/assets/buildWorker-Bi6wsfQk.js +695 -0
- package/dist/assets/index-D6-he0oi.js +476 -0
- package/dist/assets/{index-BvGPtSrr.css → index-fYq6iaqF.css} +1 -1
- package/dist/assets/worker-BSzbM0Uo.js +330 -0
- package/dist/assets/{worker-y5jhqCTR.js → worker-Cm3I3tnR.js} +57 -57
- package/dist/assets/{worker-CmHJK_do.js → worker-KiuLM-X4.js} +77 -77
- package/dist/index.html +2 -2
- package/dist/server/aiChatHandler/aiEditing.js +3 -34
- package/dist/server/aiChatHandler/chatOperations.js +5 -4
- package/dist/server/aiChatHandler/llmStreaming.js +67 -32
- package/dist/server/prettier.js +19 -8
- package/dist/utils/fileDiff.js +25 -1
- package/package.json +40 -40
- package/src/client/CodeEditor/getOrCreateEditor.tsx +263 -256
- package/src/client/CodeEditor/index.tsx +1 -4
- package/src/client/VZRight.tsx +4 -0
- package/src/client/VZSidebar/AIChat/ChatInput.tsx +1 -1
- package/src/client/VZSidebar/AIChat/DiffView.scss +37 -1
- package/src/client/VZSidebar/AIChat/DiffView.tsx +55 -16
- package/src/client/featureFlags.ts +7 -0
- package/src/server/aiChatHandler/aiEditing.ts +3 -52
- package/src/server/aiChatHandler/chatOperations.ts +5 -4
- package/src/server/aiChatHandler/llmStreaming.ts +108 -44
- package/src/server/prettier.ts +31 -16
- package/src/types.ts +5 -0
- package/src/utils/fileDiff.ts +36 -2
- package/dist/assets/buildWorker-DL4wXpRr.js +0 -695
- package/dist/assets/index-BiPmUreW.js +0 -474
- package/dist/assets/worker-RJ-cjbld.js +0 -327
|
@@ -9,6 +9,8 @@ import {
|
|
|
9
9
|
UnifiedFilesDiff,
|
|
10
10
|
parseUnifiedDiffStats,
|
|
11
11
|
combineUnifiedDiffs,
|
|
12
|
+
isFileDeletion,
|
|
13
|
+
getDeletedFileName,
|
|
12
14
|
} from '../../../utils/fileDiff';
|
|
13
15
|
import * as Diff2Html from 'diff2html';
|
|
14
16
|
import 'diff2html/bundles/css/diff2html.min.css';
|
|
@@ -41,21 +43,33 @@ export const DiffView = forwardRef<
|
|
|
41
43
|
useContext(VZCodeContext);
|
|
42
44
|
const diffContainerRef = useRef<HTMLDivElement>(null);
|
|
43
45
|
|
|
44
|
-
const
|
|
46
|
+
const allDiffs = Object.values(diffData).filter(
|
|
45
47
|
(diff) => diff.length > 0,
|
|
46
48
|
);
|
|
47
49
|
|
|
48
|
-
//
|
|
50
|
+
// Separate deleted files from regular diffs
|
|
51
|
+
const deletedFiles: string[] = [];
|
|
52
|
+
const regularDiffs: string[] = [];
|
|
53
|
+
|
|
54
|
+
for (const diff of allDiffs) {
|
|
55
|
+
if (isFileDeletion(diff)) {
|
|
56
|
+
deletedFiles.push(diff);
|
|
57
|
+
} else {
|
|
58
|
+
regularDiffs.push(diff);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Calculate statistics from regular unified diffs only
|
|
49
63
|
let totalAdditions = 0;
|
|
50
64
|
let totalDeletions = 0;
|
|
51
65
|
|
|
52
|
-
for (const unifiedDiff of
|
|
66
|
+
for (const unifiedDiff of regularDiffs) {
|
|
53
67
|
const stats = parseUnifiedDiffStats(unifiedDiff);
|
|
54
68
|
totalAdditions += stats.additions;
|
|
55
69
|
totalDeletions += stats.deletions;
|
|
56
70
|
}
|
|
57
71
|
|
|
58
|
-
// Combine
|
|
72
|
+
// Combine regular unified diffs and convert to HTML using diff2html
|
|
59
73
|
const combinedUnifiedDiff = combineUnifiedDiffs(diffData);
|
|
60
74
|
const diffHtml = Diff2Html.html(combinedUnifiedDiff, {
|
|
61
75
|
drawFileList: false,
|
|
@@ -155,7 +169,7 @@ export const DiffView = forwardRef<
|
|
|
155
169
|
};
|
|
156
170
|
}, [diffHtml, content, openTab, setIsAIChatOpen]);
|
|
157
171
|
|
|
158
|
-
if (
|
|
172
|
+
if (allDiffs.length === 0) {
|
|
159
173
|
return null;
|
|
160
174
|
}
|
|
161
175
|
|
|
@@ -164,8 +178,8 @@ export const DiffView = forwardRef<
|
|
|
164
178
|
<div className="diff-summary" id="diff-summary">
|
|
165
179
|
<div className="diff-stats">
|
|
166
180
|
<span className="files-changed">
|
|
167
|
-
{
|
|
168
|
-
{
|
|
181
|
+
{allDiffs.length} file
|
|
182
|
+
{allDiffs.length !== 1 ? 's' : ''} changed
|
|
169
183
|
</span>
|
|
170
184
|
{totalAdditions > 0 && (
|
|
171
185
|
<span className="additions">
|
|
@@ -177,18 +191,43 @@ export const DiffView = forwardRef<
|
|
|
177
191
|
-{totalDeletions}
|
|
178
192
|
</span>
|
|
179
193
|
)}
|
|
194
|
+
{deletedFiles.length > 0 && (
|
|
195
|
+
<span className="deletions">
|
|
196
|
+
{deletedFiles.length} deleted
|
|
197
|
+
</span>
|
|
198
|
+
)}
|
|
180
199
|
</div>
|
|
181
200
|
</div>
|
|
182
201
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
202
|
+
{/* Render deleted files */}
|
|
203
|
+
{deletedFiles.map((deletionMarker, index) => {
|
|
204
|
+
const fileName = getDeletedFileName(deletionMarker);
|
|
205
|
+
return (
|
|
206
|
+
<div key={index} className="deleted-file">
|
|
207
|
+
<div className="deleted-file-header">
|
|
208
|
+
<span className="deleted-file-name">
|
|
209
|
+
{fileName}
|
|
210
|
+
</span>
|
|
211
|
+
<span className="deleted-file-status">
|
|
212
|
+
File deleted
|
|
213
|
+
</span>
|
|
214
|
+
</div>
|
|
215
|
+
</div>
|
|
216
|
+
);
|
|
217
|
+
})}
|
|
218
|
+
|
|
219
|
+
{/* Render regular diffs */}
|
|
220
|
+
{regularDiffs.length > 0 && (
|
|
221
|
+
<div
|
|
222
|
+
className="diff-files"
|
|
223
|
+
ref={diffContainerRef}
|
|
224
|
+
tabIndex={-1}
|
|
225
|
+
role="region"
|
|
226
|
+
aria-label="Code diff content"
|
|
227
|
+
aria-describedby="diff-summary"
|
|
228
|
+
dangerouslySetInnerHTML={{ __html: diffHtml }}
|
|
229
|
+
/>
|
|
230
|
+
)}
|
|
192
231
|
</div>
|
|
193
232
|
);
|
|
194
233
|
});
|
|
@@ -9,3 +9,10 @@ export const enableAskMode = false;
|
|
|
9
9
|
|
|
10
10
|
// Phase 0: Feature flag for minimal AI edit flow
|
|
11
11
|
export const enableMinimalEditFlow = true;
|
|
12
|
+
|
|
13
|
+
// If true, only include the minimal set of extensions,
|
|
14
|
+
// namely only the JSON1 OT extension and that's it.
|
|
15
|
+
// This is useful for testing and debugging the OT functionality
|
|
16
|
+
// without any other extensions getting in the way,
|
|
17
|
+
// just to rule them out as the source of a bug.
|
|
18
|
+
export const MINIMAL_EXTENSIONS = false;
|
|
@@ -7,9 +7,6 @@ import {
|
|
|
7
7
|
createFilesSnapshot,
|
|
8
8
|
generateFilesUnifiedDiff,
|
|
9
9
|
} from '../../utils/fileDiff.js';
|
|
10
|
-
import { formatFiles } from '../prettier.js';
|
|
11
|
-
import { createSubmitOperation } from '../../submitOperation.js';
|
|
12
|
-
import { VizContent } from '@vizhub/viz-types';
|
|
13
10
|
|
|
14
11
|
// Dev flag for waiting 1 second before starting the LLM function.
|
|
15
12
|
// Useful for debugging and testing purposes, e.g. checking the typing indicator.
|
|
@@ -60,7 +57,7 @@ export const performAIEditing = async ({
|
|
|
60
57
|
llmFunction,
|
|
61
58
|
runCode,
|
|
62
59
|
}) => {
|
|
63
|
-
//
|
|
60
|
+
// Capture the current state of files before editing
|
|
64
61
|
const beforeFiles = createFilesSnapshot(
|
|
65
62
|
shareDBDoc.data.files,
|
|
66
63
|
);
|
|
@@ -70,7 +67,7 @@ export const performAIEditing = async ({
|
|
|
70
67
|
);
|
|
71
68
|
const filesContext = formatMarkdownFiles(files);
|
|
72
69
|
|
|
73
|
-
//
|
|
70
|
+
// Assemble the final prompt
|
|
74
71
|
const fullPrompt = assembleFullPrompt({
|
|
75
72
|
filesContext,
|
|
76
73
|
prompt,
|
|
@@ -83,55 +80,9 @@ export const performAIEditing = async ({
|
|
|
83
80
|
);
|
|
84
81
|
}
|
|
85
82
|
|
|
86
|
-
// Call the LLM function which will handle streaming
|
|
83
|
+
// Call the LLM function which will handle streaming, incremental file updates, and Prettier formatting
|
|
87
84
|
const result = await llmFunction(fullPrompt);
|
|
88
85
|
|
|
89
|
-
// 3. Run Prettier on changed files before running code
|
|
90
|
-
const afterLLMFiles = createFilesSnapshot(
|
|
91
|
-
shareDBDoc.data.files,
|
|
92
|
-
);
|
|
93
|
-
|
|
94
|
-
// Find files that were changed by the AI
|
|
95
|
-
const changedFileIds = Object.keys(afterLLMFiles).filter(
|
|
96
|
-
(fileId) =>
|
|
97
|
-
beforeFiles[fileId]?.text !==
|
|
98
|
-
afterLLMFiles[fileId]?.text,
|
|
99
|
-
);
|
|
100
|
-
|
|
101
|
-
if (changedFileIds.length > 0) {
|
|
102
|
-
// Format the changed files
|
|
103
|
-
const formattedFiles = await formatFiles(
|
|
104
|
-
shareDBDoc.data.files,
|
|
105
|
-
changedFileIds,
|
|
106
|
-
);
|
|
107
|
-
|
|
108
|
-
// Apply formatted versions to shareDBDoc if formatting succeeded
|
|
109
|
-
if (Object.keys(formattedFiles).length > 0) {
|
|
110
|
-
const submitOperation =
|
|
111
|
-
createSubmitOperation(shareDBDoc);
|
|
112
|
-
submitOperation((document: VizContent) => {
|
|
113
|
-
const updatedFiles = { ...document.files };
|
|
114
|
-
|
|
115
|
-
// Apply each formatted file
|
|
116
|
-
Object.entries(formattedFiles).forEach(
|
|
117
|
-
([fileId, formattedText]) => {
|
|
118
|
-
if (updatedFiles[fileId]) {
|
|
119
|
-
updatedFiles[fileId] = {
|
|
120
|
-
...updatedFiles[fileId],
|
|
121
|
-
text: formattedText,
|
|
122
|
-
};
|
|
123
|
-
}
|
|
124
|
-
},
|
|
125
|
-
);
|
|
126
|
-
|
|
127
|
-
return {
|
|
128
|
-
...document,
|
|
129
|
-
files: updatedFiles,
|
|
130
|
-
};
|
|
131
|
-
});
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
|
|
135
86
|
runCode();
|
|
136
87
|
|
|
137
88
|
// 4. Capture the state of files after editing and formatting, then generate diff
|
|
@@ -51,6 +51,7 @@ export const ensureChatExists = (
|
|
|
51
51
|
|
|
52
52
|
/**
|
|
53
53
|
* Adds a user message to the chat
|
|
54
|
+
* Clears old messages to reflect that each prompt is a self-contained code transformation
|
|
54
55
|
*/
|
|
55
56
|
export const addUserMessage = (
|
|
56
57
|
shareDBDoc: ShareDBDoc<VizContent>,
|
|
@@ -70,10 +71,7 @@ export const addUserMessage = (
|
|
|
70
71
|
...shareDBDoc.data.chats,
|
|
71
72
|
[chatId]: {
|
|
72
73
|
...shareDBDoc.data.chats[chatId],
|
|
73
|
-
messages: [
|
|
74
|
-
...shareDBDoc.data.chats[chatId].messages,
|
|
75
|
-
userMessage,
|
|
76
|
-
],
|
|
74
|
+
messages: [userMessage], // Replace old messages with just the new user message
|
|
77
75
|
updatedAt: dateToTimestamp(new Date()),
|
|
78
76
|
},
|
|
79
77
|
},
|
|
@@ -376,7 +374,10 @@ export const updateFiles = (
|
|
|
376
374
|
...shareDBDoc.data,
|
|
377
375
|
files,
|
|
378
376
|
});
|
|
377
|
+
DEBUG && console.log('updateFiles op:');
|
|
378
|
+
DEBUG && console.log(JSON.stringify(filesOp, null, 2));
|
|
379
379
|
shareDBDoc.submitOp(filesOp);
|
|
380
|
+
return filesOp;
|
|
380
381
|
};
|
|
381
382
|
|
|
382
383
|
/**
|
|
@@ -4,8 +4,11 @@ import {
|
|
|
4
4
|
StreamingMarkdownParser,
|
|
5
5
|
} from 'llm-code-format';
|
|
6
6
|
import { mergeFileChanges } from 'editcodewithai';
|
|
7
|
-
import {
|
|
8
|
-
|
|
7
|
+
import {
|
|
8
|
+
FileCollection,
|
|
9
|
+
VizChatId,
|
|
10
|
+
VizFiles,
|
|
11
|
+
} from '@vizhub/viz-types';
|
|
9
12
|
import {
|
|
10
13
|
updateFiles,
|
|
11
14
|
updateAIScratchpad,
|
|
@@ -14,17 +17,24 @@ import {
|
|
|
14
17
|
updateStreamingStatus,
|
|
15
18
|
finalizeStreamingMessage,
|
|
16
19
|
} from './chatOperations.js';
|
|
17
|
-
import { diff } from '../../ot.js';
|
|
18
20
|
import {
|
|
19
21
|
ShareDBDoc,
|
|
20
22
|
ExtendedVizContent,
|
|
21
23
|
} from '../../types.js';
|
|
24
|
+
import { formatFiles } from '../prettier.js';
|
|
22
25
|
|
|
26
|
+
// Verbose logs
|
|
23
27
|
const DEBUG = false;
|
|
24
28
|
|
|
25
29
|
// Useful for testing/debugging the streaming behavior
|
|
26
30
|
const slowMode = false;
|
|
27
31
|
|
|
32
|
+
// If the `EMIT_FIXTURES` variable is true,
|
|
33
|
+
// then an output file in the `test/fixtures` folder
|
|
34
|
+
// with the before and after file states for testing purposes.
|
|
35
|
+
// This feeds into tests in codemirror-ot.
|
|
36
|
+
const EMIT_FIXTURES = false;
|
|
37
|
+
|
|
28
38
|
/**
|
|
29
39
|
* Creates and configures the LLM function for streaming with reasoning tokens
|
|
30
40
|
*/
|
|
@@ -107,7 +117,7 @@ export const createLLMFunction = ({
|
|
|
107
117
|
const completeFileEditing = async (
|
|
108
118
|
fileName: string,
|
|
109
119
|
) => {
|
|
110
|
-
if (fileName
|
|
120
|
+
if (fileName) {
|
|
111
121
|
DEBUG &&
|
|
112
122
|
console.log(
|
|
113
123
|
`LLMStreaming: Completing file editing for ${fileName}`,
|
|
@@ -183,6 +193,38 @@ export const createLLMFunction = ({
|
|
|
183
193
|
}
|
|
184
194
|
}
|
|
185
195
|
},
|
|
196
|
+
onFileDelete: async (fileName: string) => {
|
|
197
|
+
DEBUG &&
|
|
198
|
+
console.log(
|
|
199
|
+
`LLMStreaming: File marked for deletion: ${fileName}`,
|
|
200
|
+
);
|
|
201
|
+
|
|
202
|
+
// Emit any accumulated text chunk first
|
|
203
|
+
await emitTextChunk();
|
|
204
|
+
|
|
205
|
+
// Complete previous file if any
|
|
206
|
+
if (currentEditingFileName) {
|
|
207
|
+
await completeFileEditing(currentEditingFileName);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Reset current editing state
|
|
211
|
+
currentEditingFileName = null;
|
|
212
|
+
currentFileContent = '';
|
|
213
|
+
|
|
214
|
+
// Emit file delete event
|
|
215
|
+
await addStreamingEvent(shareDBDoc, chatId, {
|
|
216
|
+
type: 'file_delete',
|
|
217
|
+
fileName,
|
|
218
|
+
timestamp: Date.now(),
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
// Update status
|
|
222
|
+
updateStreamingStatus(
|
|
223
|
+
shareDBDoc,
|
|
224
|
+
chatId,
|
|
225
|
+
`Deleting ${fileName}...`,
|
|
226
|
+
);
|
|
227
|
+
},
|
|
186
228
|
};
|
|
187
229
|
|
|
188
230
|
const parser = new StreamingMarkdownParser(callbacks);
|
|
@@ -285,51 +327,73 @@ export const createLLMFunction = ({
|
|
|
285
327
|
await completeFileEditing(currentEditingFileName);
|
|
286
328
|
}
|
|
287
329
|
|
|
330
|
+
// // Capture the current state of files before applying changes
|
|
331
|
+
// const beforeFiles = createFilesSnapshot(
|
|
332
|
+
// shareDBDoc.data.files,
|
|
333
|
+
// );
|
|
334
|
+
|
|
335
|
+
// Parse the full content to extract file changes
|
|
336
|
+
// export type FileCollection = Record<string, string>;
|
|
337
|
+
const newFilesUnformatted: FileCollection =
|
|
338
|
+
parseMarkdownFiles(fullContent, 'bold').files;
|
|
339
|
+
|
|
340
|
+
// Run Prettier on `newFiles` before applying them,
|
|
341
|
+
// preserving empty files as empty
|
|
342
|
+
// since that is the cue to delete a file.
|
|
343
|
+
const newFilesFormatted = await formatFiles(
|
|
344
|
+
newFilesUnformatted,
|
|
345
|
+
);
|
|
346
|
+
|
|
347
|
+
// Capture the current state of files before applying changes
|
|
348
|
+
let vizFilesBefore: VizFiles;
|
|
349
|
+
if (EMIT_FIXTURES) {
|
|
350
|
+
vizFilesBefore = JSON.parse(
|
|
351
|
+
JSON.stringify(shareDBDoc.data.files),
|
|
352
|
+
);
|
|
353
|
+
}
|
|
354
|
+
|
|
288
355
|
// Apply all the edits at once
|
|
289
|
-
|
|
290
|
-
shareDBDoc,
|
|
291
|
-
|
|
292
|
-
shareDBDoc.data.files,
|
|
293
|
-
parseMarkdownFiles(fullContent, 'bold').files,
|
|
294
|
-
),
|
|
356
|
+
const vizFilesAfter: VizFiles = mergeFileChanges(
|
|
357
|
+
shareDBDoc.data.files,
|
|
358
|
+
newFilesFormatted,
|
|
295
359
|
);
|
|
296
360
|
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
console.warn(
|
|
310
|
-
'Error setting runId after AI editing:',
|
|
311
|
-
error,
|
|
312
|
-
);
|
|
313
|
-
} else {
|
|
314
|
-
DEBUG &&
|
|
315
|
-
console.log(
|
|
316
|
-
'Set new runId after AI editing:',
|
|
317
|
-
newRunId,
|
|
318
|
-
);
|
|
361
|
+
const filesOp = updateFiles(shareDBDoc, vizFilesAfter);
|
|
362
|
+
|
|
363
|
+
if (EMIT_FIXTURES) {
|
|
364
|
+
const fs = await import('fs');
|
|
365
|
+
const path = await import('path');
|
|
366
|
+
const testCasesDir = path.resolve(
|
|
367
|
+
process.cwd(),
|
|
368
|
+
'../',
|
|
369
|
+
'fixtures',
|
|
370
|
+
);
|
|
371
|
+
if (!fs.existsSync(testCasesDir)) {
|
|
372
|
+
fs.mkdirSync(testCasesDir, { recursive: true });
|
|
319
373
|
}
|
|
320
|
-
|
|
374
|
+
const timestamp = new Date()
|
|
375
|
+
.toISOString()
|
|
376
|
+
.replace(/[:.]/g, '-');
|
|
377
|
+
const testCasePath = path.join(
|
|
378
|
+
testCasesDir,
|
|
379
|
+
`ai-chat-${timestamp}.json`,
|
|
380
|
+
);
|
|
381
|
+
const testCaseData = {
|
|
382
|
+
vizFilesBefore,
|
|
383
|
+
vizFilesAfter,
|
|
384
|
+
filesOp,
|
|
385
|
+
};
|
|
386
|
+
fs.writeFileSync(
|
|
387
|
+
testCasePath,
|
|
388
|
+
JSON.stringify(testCaseData, null, 2),
|
|
389
|
+
);
|
|
390
|
+
console.log(
|
|
391
|
+
`AI chat test case written to ${testCasePath}`,
|
|
392
|
+
);
|
|
393
|
+
}
|
|
321
394
|
|
|
322
|
-
//
|
|
323
|
-
|
|
324
|
-
// const chunksFileJSONpath = `./ai-chunks-${chatId}.json`;
|
|
325
|
-
// fs.writeFileSync(
|
|
326
|
-
// chunksFileJSONpath,
|
|
327
|
-
// JSON.stringify(chunks, null, 2),
|
|
328
|
-
// );
|
|
329
|
-
// console.log(
|
|
330
|
-
// `AI chunks written to ${chunksFileJSONpath}`,
|
|
331
|
-
// );
|
|
332
|
-
// }
|
|
395
|
+
// Finalize streaming message
|
|
396
|
+
finalizeStreamingMessage(shareDBDoc, chatId);
|
|
333
397
|
|
|
334
398
|
return {
|
|
335
399
|
content: fullContent,
|
package/src/server/prettier.ts
CHANGED
|
@@ -5,7 +5,7 @@ import * as prettierPluginHtml from 'prettier/plugins/html';
|
|
|
5
5
|
import * as prettierPluginMarkdown from 'prettier/plugins/markdown';
|
|
6
6
|
import * as prettierPluginCSS from 'prettier/plugins/postcss';
|
|
7
7
|
import * as prettierPluginTypescript from 'prettier/plugins/typescript';
|
|
8
|
-
import {
|
|
8
|
+
import { FileCollection } from '@vizhub/viz-types';
|
|
9
9
|
|
|
10
10
|
// Parser mappings - matches client-side implementation
|
|
11
11
|
const parsers = {
|
|
@@ -82,30 +82,45 @@ export const formatFile = async (
|
|
|
82
82
|
}
|
|
83
83
|
};
|
|
84
84
|
|
|
85
|
+
// Keys are file names, values are file text contents
|
|
86
|
+
//export type FileCollection = Record<string, string>;
|
|
87
|
+
|
|
85
88
|
/**
|
|
86
89
|
* Formats multiple files using Prettier
|
|
87
90
|
* Only formats files that have supported extensions
|
|
88
91
|
* Returns a map of fileId -> formatted text for successfully formatted files
|
|
89
92
|
*/
|
|
90
93
|
export const formatFiles = async (
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
const
|
|
95
|
-
const targetFileIds = fileIds || Object.keys(files);
|
|
94
|
+
fileCollection: FileCollection,
|
|
95
|
+
): Promise<FileCollection> => {
|
|
96
|
+
const results: FileCollection = {};
|
|
97
|
+
const targetFileNames = Object.keys(fileCollection);
|
|
96
98
|
|
|
97
99
|
// Process files in parallel for better performance
|
|
98
|
-
const formatPromises =
|
|
99
|
-
async (
|
|
100
|
-
const
|
|
101
|
-
|
|
100
|
+
const formatPromises = targetFileNames.map(
|
|
101
|
+
async (fileName: string) => {
|
|
102
|
+
const fileText = fileCollection[fileName];
|
|
103
|
+
results[fileName] = fileText;
|
|
104
|
+
|
|
105
|
+
// Preserve empty files as empty,
|
|
106
|
+
// since this is the cue to delete a file.
|
|
107
|
+
if (!fileText || fileText.trim() === '') {
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
102
110
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
111
|
+
try {
|
|
112
|
+
const formatted = await formatFile(
|
|
113
|
+
fileText,
|
|
114
|
+
fileName,
|
|
115
|
+
);
|
|
116
|
+
if (formatted !== null && formatted !== fileText) {
|
|
117
|
+
results[fileName] = formatted;
|
|
118
|
+
}
|
|
119
|
+
} catch (error) {
|
|
120
|
+
console.error(
|
|
121
|
+
`Error formatting ${fileName}:`,
|
|
122
|
+
error,
|
|
123
|
+
);
|
|
109
124
|
}
|
|
110
125
|
},
|
|
111
126
|
);
|
package/src/types.ts
CHANGED
package/src/utils/fileDiff.ts
CHANGED
|
@@ -2,7 +2,33 @@ import { createTwoFilesPatch } from 'diff';
|
|
|
2
2
|
import { VizFiles, VizFileId } from '@vizhub/viz-types';
|
|
3
3
|
|
|
4
4
|
export interface UnifiedFilesDiff {
|
|
5
|
-
[fileId: VizFileId]: string; // Unified diff string
|
|
5
|
+
[fileId: VizFileId]: string; // Unified diff string or deletion marker
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
// Special marker to indicate a file deletion
|
|
9
|
+
export const FILE_DELETION_MARKER = '__FILE_DELETED__';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Check if a diff string represents a file deletion
|
|
13
|
+
*/
|
|
14
|
+
export function isFileDeletion(
|
|
15
|
+
diffString: string,
|
|
16
|
+
): boolean {
|
|
17
|
+
return diffString.startsWith(FILE_DELETION_MARKER);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Extract file name from a deletion marker
|
|
22
|
+
*/
|
|
23
|
+
export function getDeletedFileName(
|
|
24
|
+
diffString: string,
|
|
25
|
+
): string {
|
|
26
|
+
if (!isFileDeletion(diffString)) {
|
|
27
|
+
return '';
|
|
28
|
+
}
|
|
29
|
+
return diffString.substring(
|
|
30
|
+
FILE_DELETION_MARKER.length + 1,
|
|
31
|
+
);
|
|
6
32
|
}
|
|
7
33
|
|
|
8
34
|
/**
|
|
@@ -19,6 +45,11 @@ export function generateFileUnifiedDiff(
|
|
|
19
45
|
return '';
|
|
20
46
|
}
|
|
21
47
|
|
|
48
|
+
// Handle file deletion case - when file existed before but is now empty/deleted
|
|
49
|
+
if (beforeContent && !afterContent) {
|
|
50
|
+
return `${FILE_DELETION_MARKER}:${fileName}`;
|
|
51
|
+
}
|
|
52
|
+
|
|
22
53
|
// Use the diff library's createTwoFilesPatch function to generate unified diff
|
|
23
54
|
const unifiedDiff = createTwoFilesPatch(
|
|
24
55
|
fileName,
|
|
@@ -109,9 +140,12 @@ export function parseUnifiedDiffStats(
|
|
|
109
140
|
|
|
110
141
|
/**
|
|
111
142
|
* Combine multiple unified diffs into a single diff string
|
|
143
|
+
* Note: File deletion markers are excluded from combination
|
|
112
144
|
*/
|
|
113
145
|
export function combineUnifiedDiffs(
|
|
114
146
|
unifiedDiffs: UnifiedFilesDiff,
|
|
115
147
|
): string {
|
|
116
|
-
return Object.values(unifiedDiffs)
|
|
148
|
+
return Object.values(unifiedDiffs)
|
|
149
|
+
.filter((diff) => !isFileDeletion(diff))
|
|
150
|
+
.join('\n');
|
|
117
151
|
}
|