From 2ad063eb309e7433db12c8afad98f49ee08d98f9 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Fri, 15 Nov 2024 03:42:38 -0800 Subject: [PATCH] simplify sweep + refresh --- .../contrib/void/browser/findDiffs.ts | 148 +++++- .../contrib/void/browser/registerActions.ts | 5 +- .../void/browser/registerInlineDiffs.ts | 479 +++++++++--------- 3 files changed, 375 insertions(+), 257 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/findDiffs.ts b/src/vs/workbench/contrib/void/browser/findDiffs.ts index e83ffc14..c3f80601 100644 --- a/src/vs/workbench/contrib/void/browser/findDiffs.ts +++ b/src/vs/workbench/contrib/void/browser/findDiffs.ts @@ -12,7 +12,7 @@ type Fields = | 'startCol' | 'endCol' -type BaseDiff = Pick +export type BaseDiff = Pick // Andrew diff algo: export type SuggestedEdit = { @@ -31,14 +31,14 @@ export function findDiffs(oldStr: string, newStr: string) { const lineByLineChanges = diffLines(oldStr, newStr); lineByLineChanges.push({ value: '', added: false, removed: false }) // add a dummy so we flush any streaks we haven't yet at the very end (!line.added && !line.removed) - let oldFileLineNum: number = 0; - let newFileLineNum: number = 0; + let oldFileLineNum: number = 1; + let newFileLineNum: number = 1; let streakStartInNewFile: number | undefined = undefined let streakStartInOldFile: number | undefined = undefined - let oldStrLines = oldStr.split('\n') - let newStrLines = newStr.split('\n') + let oldStrLines = ('\n' + oldStr).split('\n') // add newline so indexing starts at 1 + // let newStrLines = ('\n' + newStr).split('\n') const replacements: BaseDiff[] = [] for (let line of lineByLineChanges) { @@ -54,7 +54,7 @@ export function findDiffs(oldStr: string, newStr: string) { let startLine = streakStartInNewFile let endLine = newFileLineNum - 1 // don't include current line, the edit was up to this line but not including it - let startCol = 0 + let startCol = 1 let endCol = Number.MAX_SAFE_INTEGER let originalStartLine = streakStartInOldFile! @@ -62,7 +62,7 @@ export function findDiffs(oldStr: string, newStr: string) { // let originalStartCol = 0 // let originalEndCol = Number.MAX_SAFE_INTEGER - let newContent = newStrLines.slice(startLine, endLine + 1).join('\n') + // let newContent = newStrLines.slice(startLine, endLine + 1).join('\n') let originalContent = oldStrLines.slice(originalStartLine, originalEndLine + 1).join('\n') // if the range is empty, mark it as a deletion / insertion (both won't be true at once) @@ -70,9 +70,9 @@ export function findDiffs(oldStr: string, newStr: string) { if (endLine === startLine - 1) { type = 'deletion' endLine = startLine - startCol = 0 - endCol = 0 - newContent += '\n' + startCol = 1 + endCol = 1 + // newContent += '\n' } // INSERTION @@ -109,7 +109,7 @@ export function findDiffs(oldStr: string, newStr: string) { streakStartInNewFile = newFileLineNum streakStartInOldFile = oldFileLineNum } - oldFileLineNum += line.count ?? 0 // we processed the line so add 1 + oldFileLineNum += line.count ?? 0 // we processed the line so add 1 (or "count") } // line was added to new file @@ -119,10 +119,134 @@ export function findDiffs(oldStr: string, newStr: string) { streakStartInNewFile = newFileLineNum streakStartInOldFile = oldFileLineNum } - newFileLineNum += line.count ?? 0; // we processed the line so add 1 + newFileLineNum += line.count ?? 0; // we processed the line so add 1 (or "count") } } // end for // console.debug('Replacements', replacements) return replacements } + + + + + + + + + + + + + + + + + + + + +// uncomment this to test + +// let name_ = '' +// let testsFailed = 0 +// const assertEqual = (a: { [s: string]: any }, b: { [s: string]: any }) => { +// let keys = new Set([...Object.keys(a), ...Object.keys(b)]) +// for (let k of keys) { +// if (a[k] !== b[k]) { +// console.error('Void Test Error:', name_, '\n', `${k}=`, `${JSON.stringify(a[k])}, ${JSON.stringify(b[k])}`) +// // console.error(JSON.stringify(a, null, 4)) +// // console.error(JSON.stringify(b, null, 4)) +// testsFailed += 1 +// } +// } +// } +// const test = (name: string, fn: () => void) => { +// name_ = name +// fn() +// } + +// const originalCode = `\ +// A +// B +// C + +// E +// ` +// const insertedCode = `\ +// A +// B +// C +// F +// D +// E +// ` + +// const modifiedCode = `\ +// A +// B +// C +// F +// E +// ` + + +// test('Diffs Insertion', () => { +// const diffs = findDiffs(originalCode, insertedCode) + +// const expected: BaseDiff = { +// type: 'insertion', +// originalCode: '', +// originalStartLine: 4, // empty range where the insertion happened +// originalEndLine: 4, + +// startLine: 4, +// startCol: 1, +// endLine: 4, +// endCol: Number.MAX_SAFE_INTEGER, +// } +// assertEqual(diffs[0], expected) +// }) + +// test('Diffs Deletion', () => { +// const diffs = findDiffs(insertedCode, originalCode) +// assertEqual({ length: diffs.length }, { length: 1 }) +// const expected: BaseDiff = { +// type: 'deletion', +// originalCode: 'F', +// originalStartLine: 4, +// originalEndLine: 4, + +// startLine: 4, +// startCol: 1, // empty range where the deletion happened +// endLine: 4, +// endCol: 1, +// } +// assertEqual(diffs[0], expected) +// }) + +// test('Diffs Modification', () => { +// const diffs = findDiffs(originalCode, modifiedCode) +// assertEqual({ length: diffs.length }, { length: 1 }) +// const expected: BaseDiff = { +// type: 'edit', +// originalCode: 'D', +// originalStartLine: 4, +// originalEndLine: 4, + +// startLine: 4, +// startCol: 1, +// endLine: 4, +// endCol: Number.MAX_SAFE_INTEGER, +// } +// assertEqual(diffs[0], expected) +// }) + + + +// if (testsFailed === 0) { +// console.log('✅ Void - All tests passed') +// } +// else { +// console.log('❌ Void - At least one test failed') +// } diff --git a/src/vs/workbench/contrib/void/browser/registerActions.ts b/src/vs/workbench/contrib/void/browser/registerActions.ts index f3df8b33..9a1ebb35 100644 --- a/src/vs/workbench/contrib/void/browser/registerActions.ts +++ b/src/vs/workbench/contrib/void/browser/registerActions.ts @@ -24,10 +24,11 @@ import { IVoidSidebarStateService, VOID_VIEW_ID } from './registerSidebar.js'; const roundRangeToLines = (range: IRange | null | undefined) => { if (!range) return null - let endLine = range.endColumn === 0 ? range.endLineNumber - 1 : range.endLineNumber // e.g. if the user triple clicks, it selects column=0, line=line -> column=0, line=line+1 + // IRange is 1-indexed + let endLine = range.endColumn === 1 ? range.endLineNumber - 1 : range.endLineNumber // e.g. if the user triple clicks, it selects column=0, line=line -> column=0, line=line+1 const newRange: IRange = { startLineNumber: range.startLineNumber, - startColumn: 0, + startColumn: 1, endLineNumber: endLine, endColumn: Number.MAX_SAFE_INTEGER } diff --git a/src/vs/workbench/contrib/void/browser/registerInlineDiffs.ts b/src/vs/workbench/contrib/void/browser/registerInlineDiffs.ts index d782db82..2587a0f0 100644 --- a/src/vs/workbench/contrib/void/browser/registerInlineDiffs.ts +++ b/src/vs/workbench/contrib/void/browser/registerInlineDiffs.ts @@ -13,7 +13,7 @@ import { URI } from '../../../../base/common/uri.js'; import { IVoidConfigStateService } from './registerConfig.js'; import { writeFileWithDiffInstructions } from './prompt/systemPrompts.js'; import { findDiffs } from './findDiffs.js'; -import { EndOfLinePreference, IModelDeltaDecoration, ITextModel } from '../../../../editor/common/model.js'; +import { IModelDecorationOptions, IModelDeltaDecoration, ITextModel } from '../../../../editor/common/model.js'; import { IRange } from '../../../../editor/common/core/range.js'; import { EditorOption } from '../../../../editor/common/config/editorOptions.js'; // import { IModelService } from '../../../../editor/common/services/model.js'; @@ -47,7 +47,7 @@ export type Diff = { startCol: number; endCol: number; - _disposeStyles: (() => void) | null; + _disposeDiff: (() => void) | null; // _zone: IViewZone | null, // _decorationId: string | null, @@ -63,10 +63,10 @@ type DiffArea = { startLine: number, endLine: number, - _model: ITextModel, // the model, computed by modelUriComponents - _sweepLine: number | null, - _sweepCol: number | null, + _model: ITextModel, // the model (if we clone it, the function keeps track of the model id) + _isStreaming: boolean, _diffs: Diff[], + _disposeSweepStyles: (() => void) | null, // _generationid: number, } @@ -76,7 +76,7 @@ const diffAreaSnapshotKeys = [ 'originalEndLine', 'startLine', 'endLine', -] as const +] as const satisfies (keyof DiffArea)[] type DiffAreaSnapshot = Pick @@ -94,14 +94,6 @@ type HistorySnapshot = { }) -type StreamingState = { - type: 'streaming'; - // editGroup: UndoRedoGroup; // all changes made by us when streaming should be a part of the group so we can undo them all together -} | { - type: 'idle'; -} - - export interface IInlineDiffsService { readonly _serviceBrand: undefined; startStreaming(type: 'ctrl+k' | 'ctrl+l', userMessage: string): void; @@ -125,7 +117,6 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService { // state of each document originalFileStrOfModelId: Record = {} // modelid -> originalFile diffAreasOfModelId: Record> = {} // modelid -> Set(diffAreaId) - streamingStateOfModelId: Record = {} // modelid -> state diffAreaOfId: Record = {}; diffOfId: Record = {}; // redundant with diffArea._diffs @@ -149,9 +140,53 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService { } + private _addSweepStyles = (model: ITextModel, sweepLine: number, endLine: number) => { - private _addInlineDiffZone = (model: ITextModel, originalText: string, greenRange: IRange) => { + const decorationIds: (string | null)[] = [] + // sweepLine ... sweepLine + const lineRange = { + startLineNumber: sweepLine, + startColumn: 1, + endLineNumber: sweepLine, + endColumn: Number.MAX_SAFE_INTEGER + } + const darkGrayDecoration: IModelDecorationOptions = { + className: 'sweep-dark-gray', + description: 'sweep-dark-gray', + isWholeLine: true + } + decorationIds.push( + model.changeDecorations(accessor => accessor.addDecoration(lineRange, darkGrayDecoration)) + ) + + // sweepline+1 ... end + const bulkRange = { + startLineNumber: sweepLine + 1, + startColumn: 1, + endLineNumber: endLine, + endColumn: Number.MAX_SAFE_INTEGER + } + const lightGrayDecoration: IModelDecorationOptions = { + className: 'sweep-light-gray', + description: 'sweep-light-gray', + isWholeLine: true + } + decorationIds.push( + model.changeDecorations(accessor => accessor.addDecoration(bulkRange, lightGrayDecoration)) + ) + + const dispose = () => { + for (let id of decorationIds) { + if (id) model.changeDecorations(accessor => accessor.removeDecoration(id)) + } + } + return dispose + } + + + + private _addInlineDiffZone = (model: ITextModel, redText: string, greenRange: IRange, diffid: number) => { const _addInlineDiffZoneToEditor = (editor: ICodeEditor) => { // green decoration and gutter decoration const greenDecoration: IModelDeltaDecoration[] = [{ @@ -184,6 +219,8 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService { domNode.style.fontFamily = fontInfo.fontFamily; domNode.style.lineHeight = `${fontInfo.lineHeight}px`; + domNode.style.whiteSpace = `pre`; + // div const lineContent = document.createElement('div'); lineContent.className = 'view-line'; // .monaco-editor .inline-deleted-text @@ -194,7 +231,8 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService { // span const codeSpan = document.createElement('span'); codeSpan.className = 'mtk1'; // char-delete - codeSpan.textContent = originalText; + codeSpan.textContent = redText; + console.log('originalText', redText.replace('\n', '\\n')); // Mount contentSpan.appendChild(codeSpan); @@ -211,9 +249,9 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService { const viewZone: IViewZone = { afterLineNumber: greenRange.startLineNumber - 1, - heightInLines: originalText.split('\n').length + 1, + heightInLines: redText.split('\n').length, domNode: domNode, - suppressMouseDown: true, + // suppressMouseDown: true, marginDomNode: gutterDiv }; @@ -222,12 +260,9 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService { // this._diffZones.set(editor, [zoneId]); }); - const dispose = () => { decorationsCollection.clear() - editor.changeViewZones(accessor => { - if (zoneId) accessor.removeZone(zoneId); - }); + editor.changeViewZones(accessor => { if (zoneId) accessor.removeZone(zoneId); }); } return dispose } @@ -235,11 +270,11 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService { const editors = this._editorService.listCodeEditors().filter(editor => editor.getModel()?.id === model.id) const disposeFns = editors.map(editor => _addInlineDiffZoneToEditor(editor)) - const dispose = () => { + const disposeDiff = () => { disposeFns.forEach(fn => fn()) } - return dispose + return disposeDiff } @@ -274,6 +309,13 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService { const restoreDiffAreas = (snapshot: HistorySnapshot) => { const { snapshottedDiffAreaOfId, snapshottedOriginalFileStr } = structuredClone(snapshot) // don't want to destroy the snapshot + // delete all current decorations (diffs, sweep styles) so we don't have any unwanted leftover decorations + for (let diffareaid in this.diffAreaOfId) { + const diffArea = this.diffAreaOfId[diffareaid] + this._deleteDiffs(diffArea) + this._deleteSweepStyles(diffArea) + } + // restore diffAreaOfId this.diffAreaOfId = {} for (let diffareaid in snapshottedDiffAreaOfId) { @@ -281,8 +323,8 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService { ...snapshottedDiffAreaOfId[diffareaid], _diffs: [], _model: model, - _sweepCol: null, - _sweepLine: null, + _isStreaming: false, + _disposeSweepStyles: null, } } // use it to restore diffAreasOfModelId @@ -293,8 +335,10 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService { // restore originalFileStr of this model this.originalFileStrOfModelId[model.id] = snapshottedOriginalFileStr - this._refreshAllDiffsAndStyles(model) - this._refreshSweepStyles(model) + // restore all the decorations + for (let diffareaid in this.diffAreaOfId) { + this._onGetNewDiffAreaText(this.diffAreaOfId[diffareaid], snapshottedOriginalFileStr, new UndoRedoGroup()) + } } const elt: IUndoRedoElement = { @@ -324,11 +368,14 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService { } - + private _deleteSweepStyles(diffArea: DiffArea) { + diffArea._disposeSweepStyles?.() + diffArea._disposeSweepStyles = null + } private _deleteDiffs(diffArea: DiffArea) { for (const diff of diffArea._diffs) { - diff._disposeStyles?.() + diff._disposeDiff?.() delete this.diffOfId[diff.diffid] } diffArea._diffs = [] @@ -344,245 +391,202 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService { - // for every diffarea in this document, recompute its diffs and restyle it (the two are coupled) - private _refreshAllDiffsAndStyles(model: ITextModel) { - - const originalFile = this.originalFileStrOfModelId[model.id] - if (originalFile === undefined) return - - // ------------ recompute all diffs in each diffarea ------------ - // for each diffArea - for (const diffareaid of this.diffAreasOfModelId[model.id]) { - - const diffArea = this.diffAreaOfId[diffareaid] - - // clear its diffs - this._deleteDiffs(diffArea) - - // recompute diffs: - const originalCode = originalFile.split('\n').slice(diffArea.originalStartLine, diffArea.originalEndLine + 1).join('\n') - const currentCode = model.getValue(EndOfLinePreference.LF).split('\n').slice(diffArea.startLine, diffArea.endLine + 1).join('\n') - - const computedDiffs = findDiffs(originalCode, currentCode) - console.log('Length of computed:', computedDiffs.length) - - for (let computedDiff of computedDiffs) { - // add the view zone - const greenRange: IRange = { startLineNumber: computedDiff.startLine + 1, startColumn: 0, endLineNumber: computedDiff.endLine + 1, endColumn: Number.MAX_SAFE_INTEGER, } - const dispose = this._addInlineDiffZone(model, computedDiff.originalCode, greenRange) - - // create a Diff of it - const diffid = this._diffidPool++ - const newDiff: Diff = { - diffid: diffid, - diffareaid: diffArea.diffareaid, - _disposeStyles: dispose, - ...computedDiff, - } - - this.diffOfId[diffid] = newDiff - diffArea._diffs.push(newDiff) - } - } - } - - private _refreshSweepStyles(model: ITextModel) { - const modelid = model.id; - - // Create decorations for each diffArea - for (const diffareaid of this.diffAreasOfModelId[modelid] || new Set()) { - const diffArea = this.diffAreaOfId[diffareaid]; - if (!diffArea._sweepLine) continue; - - const lightGrayDecoration: IModelDeltaDecoration[] = [{ - range: { - startLineNumber: diffArea._sweepLine + 1, - startColumn: 0, - endLineNumber: diffArea.endLine, - endColumn: Number.MAX_SAFE_INTEGER - }, - options: { - className: 'sweep-light-gray', - description: 'sweep-light-gray', - isWholeLine: true - } - }]; - - const darkGrayDecoration: IModelDeltaDecoration[] = [{ - range: { - startLineNumber: diffArea._sweepLine, - startColumn: 0, - endLineNumber: diffArea._sweepLine, - endColumn: Number.MAX_SAFE_INTEGER - }, - options: { - className: 'sweep-dark-gray', - description: 'sweep-dark-gray', - isWholeLine: true - } - }]; - - model.deltaDecorations([], [...lightGrayDecoration, ...darkGrayDecoration]); - } - } - // changes the start/line locations based on the changes that were recently made. does not change any of the diffs in the diff areas - // changes tells us how many lines were inserted/deleted so we can grow/shrink the diffAreas accordingly - private _resizeOnTextChange(modelid: string, changes: { text: string, startLine: number, endLine: number }[], changesTo: 'originalFile' | 'currentFile') { - // resize all diffareas on page (adjust their start/end based on the change) + // private _registeredListeners = new Set() // set of model IDs + // private _registerTextChangeListener(model: ITextModel) { - let endLine: 'originalEndLine' | 'endLine' - let startLine: 'originalStartLine' | 'startLine' + // if (this._registeredListeners.has(model.id)) return - if (changesTo === 'originalFile') { - endLine = 'originalEndLine' as const - startLine = 'originalStartLine' as const - } else { - endLine = 'endLine' as const - startLine = 'startLine' as const - } + // this._registeredListeners.add(model.id) + // // listen for text changes + // this._register( + // model.onDidChangeContent(e => { + // const changes = e.changes.map(c => ({ startLine: c.range.startLineNumber, endLine: c.range.endLineNumber, text: c.text, })) + // this._resizeOnTextChange(model.id, changes, 'currentFile') + // this._refreshAllDiffs(model) + // }) + // ) - // here, `change.range` is the range of the original file that gets replaced with `change.text` - for (const change of changes) { + // this._register( + // model.onWillDispose(e => { + // this._registeredListeners.delete(model.id) + // }) + // ) + // } - // compute net number of newlines lines that were added/removed - const numNewLines = (change.text.match(/\n/g) || []).length - const numLineDeletions = change.endLine - change.startLine - const deltaNewlines = numNewLines - numLineDeletions + // // changes the start/line locations based on the changes that were recently made. does not change any of the diffs in the diff areas + // // changes tells us how many lines were inserted/deleted so we can grow/shrink the diffAreas accordingly + // private _resizeOnTextChange(modelid: string, changes: { text: string, startLine: number, endLine: number }[], changesTo: 'originalFile' | 'currentFile') { - // compute overlap with each diffArea and shrink/elongate each diffArea accordingly - for (const diffareaid of this.diffAreasOfModelId[modelid] || []) { - const diffArea = this.diffAreaOfId[diffareaid] + // // resize all diffareas on page (adjust their start/end based on the change) - // if the change is fully within the diffArea, elongate it by the delta amount of newlines - if (change.startLine >= diffArea[startLine] && change.endLine <= diffArea[endLine]) { - diffArea[endLine] += deltaNewlines - } - // check if the `diffArea` was fully deleted and remove it if so - if (diffArea[startLine] > diffArea[endLine]) { - this.diffAreasOfModelId[modelid].delete(diffareaid) - continue - } + // let endLine: 'originalEndLine' | 'endLine' + // let startLine: 'originalStartLine' | 'startLine' - // if a diffArea is below the last character of the change, shift the diffArea up/down by the delta amount of newlines - if (diffArea[startLine] > change.endLine) { - diffArea[startLine] += deltaNewlines - diffArea[endLine] += deltaNewlines - } + // if (changesTo === 'originalFile') { + // endLine = 'originalEndLine' as const + // startLine = 'originalStartLine' as const + // } else { + // endLine = 'endLine' as const + // startLine = 'startLine' as const + // } - // TODO handle other cases where eg. the change overlaps many diffAreas - } - // TODO merge any diffAreas if they overlap with each other as a result from the shift + // // here, `change.range` is the range of the original file that gets replaced with `change.text` + // for (const change of changes) { - } - } + // // compute net number of newlines lines that were added/removed + // const numNewLines = (change.text.match(/\n/g) || []).length + // const numLineDeletions = change.endLine - change.startLine + // const deltaNewlines = numNewLines - numLineDeletions + + // // compute overlap with each diffArea and shrink/elongate each diffArea accordingly + // for (const diffareaid of this.diffAreasOfModelId[modelid] || []) { + // const diffArea = this.diffAreaOfId[diffareaid] + + // // if the change is fully within the diffArea, elongate it by the delta amount of newlines + // if (change.startLine >= diffArea[startLine] && change.endLine <= diffArea[endLine]) { + // diffArea[endLine] += deltaNewlines + // } + // // check if the `diffArea` was fully deleted and remove it if so + // if (diffArea[startLine] > diffArea[endLine]) { + // this.diffAreasOfModelId[modelid].delete(diffareaid) + // continue + // } + + // // if a diffArea is below the last character of the change, shift the diffArea up/down by the delta amount of newlines + // if (diffArea[startLine] > change.endLine) { + // diffArea[startLine] += deltaNewlines + // diffArea[endLine] += deltaNewlines + // } + + // // TODO handle other cases where eg. the change overlaps many diffAreas + // } + // // TODO merge any diffAreas if they overlap with each other as a result from the shift + + // } + // } - private _registeredListeners = new Set() // set of model IDs - private _registerTextChangeListener(model: ITextModel) { - if (this._registeredListeners.has(model.id)) return - this._registeredListeners.add(model.id) - // listen for text changes - this._register( - model.onDidChangeContent(e => { - const changes = e.changes.map(c => ({ startLine: c.range.startLineNumber, endLine: c.range.endLineNumber, text: c.text, })) - this._resizeOnTextChange(model.id, changes, 'currentFile') - this._refreshAllDiffsAndStyles(model) - }) - ) - - this._register( - model.onWillDispose(e => { - this._registeredListeners.delete(model.id) - }) - ) - } private _writeToModel(model: ITextModel, text: string, range: IRange, editorGroup: UndoRedoGroup) { - if (!model.isDisposed()) - model.pushEditOperations(null, [{ range, text }], () => null, editorGroup) + // model.applyEdits([{ range, text }]) // applies edits without adding them to undo/redo stack + model.pushEditOperations(null, [{ range, text }], () => null, editorGroup) // applies edits in the group + + // this._bulkEditService.apply([new ResourceTextEdit(model.uri, { + // range: { startLineNumber: diffArea.startLine, startColumn: 0, endLineNumber: diffArea.endLine, endColumn: Number.MAX_SAFE_INTEGER, }, + // text: newCode + // })], { undoRedoGroupId: editorGroup.id }); // count all changes towards the group } // @throttle(100) - private _updateDiffAreaText(diffArea: DiffArea, llmCodeSoFar: string, editorGroup: UndoRedoGroup) { - // clear all diffs in this diffarea and recompute them + private _onGetNewDiffAreaText(diffArea: DiffArea, newCodeSoFar: string, editorGroup: UndoRedoGroup) { + const model = diffArea._model - - if (this.streamingStateOfModelId[model.id].type !== 'streaming') - return - // original code all diffs are based on const originalDiffAreaCode = (this.originalFileStrOfModelId[model.id] || '').split('\n').slice(diffArea.originalStartLine, diffArea.originalEndLine + 1).join('\n') + // figure out where to highlight based on where the AI is in the stream right now, use the last diff to figure that out + const computedDiffs = findDiffs(originalDiffAreaCode, newCodeSoFar) - // figure out where to highlight based on where the AI is in the stream right now, use the last diff in findDiffs to figure that out - const diffs = findDiffs(originalDiffAreaCode, llmCodeSoFar) - const lastDiff = diffs?.[diffs.length - 1] ?? null + // ----------- 0. Clear all current styles in the diffArea ----------- + this._deleteDiffs(diffArea) + this._deleteSweepStyles(diffArea) - // these are two different coordinate systems - new and old line number - let newFileEndLine: number // get new[0...newStoppingPoint] with line=newStoppingPoint highlighted - let oldFileStartLine: number // get original[oldStartingPoint...] - if (!lastDiff) { - // if the writing is identical so far, display no changes - newFileEndLine = 0 - oldFileStartLine = 0 + // ----------- 1. Write the new code to the document ----------- + + // if not streaming, just write the new code + if (!diffArea._isStreaming) { + this._writeToModel( + model, + newCodeSoFar, + { startLineNumber: diffArea.startLine, startColumn: 1, endLineNumber: diffArea.endLine, endColumn: Number.MAX_SAFE_INTEGER, }, + editorGroup + ) } + // if streaming, use diffs to figure out where to write new code else { - if (lastDiff.type === 'insertion') { - newFileEndLine = lastDiff.endLine - oldFileStartLine = lastDiff.originalStartLine - } - else if (lastDiff.type === 'deletion') { - newFileEndLine = lastDiff.startLine - oldFileStartLine = lastDiff.originalStartLine - } - else if (lastDiff.type === 'edit') { - newFileEndLine = lastDiff.endLine - oldFileStartLine = lastDiff.originalStartLine + // these are two different coordinate systems - new and old line number + let newFileEndLine: number // get new[0...newStoppingPoint] with line=newStoppingPoint highlighted + let oldFileStartLine: number // get original[oldStartingPoint...] + + // pop the last diff and use it to compute where the new code should be written + const lastDiff = computedDiffs.pop() + + if (!lastDiff) { + // if the writing is identical so far, display no changes + newFileEndLine = 0 + oldFileStartLine = 0 } else { - throw new Error(`updateStream: diff.type not recognized: ${lastDiff.type}`) + if (lastDiff.type === 'insertion') { + newFileEndLine = lastDiff.endLine + oldFileStartLine = lastDiff.originalStartLine + } + else if (lastDiff.type === 'deletion') { + newFileEndLine = lastDiff.startLine + oldFileStartLine = lastDiff.originalStartLine + } + else if (lastDiff.type === 'edit') { + newFileEndLine = lastDiff.endLine + oldFileStartLine = lastDiff.originalStartLine + } + else { + throw new Error(`Void: diff.type not recognized: ${lastDiff.type}`) + } } + + // lines are 1-indexed + const newFileTop = newCodeSoFar.split('\n').slice(0, newFileEndLine).join('\n') + const oldFileBottom = originalDiffAreaCode.split('\n').slice(oldFileStartLine, Infinity).join('\n') + + let newCode = `${newFileTop}\n${oldFileBottom}` + + this._writeToModel( + model, + newCode, + { startLineNumber: diffArea.startLine, startColumn: 1, endLineNumber: diffArea.endLine, endColumn: Number.MAX_SAFE_INTEGER, }, + editorGroup + ) + + // ----------- 2. Recompute sweep in the diffArea if streaming ----------- + const sweepLine = newFileEndLine + const disposeSweepStyles = this._addSweepStyles(model, sweepLine, diffArea.endLine) + diffArea._disposeSweepStyles = disposeSweepStyles } - // display - const newFileTop = llmCodeSoFar.split('\n').slice(0, newFileEndLine + 1).join('\n') - const oldFileBottom = originalDiffAreaCode.split('\n').slice(oldFileStartLine + 1, Infinity).join('\n') + // ----------- 3. Recompute all Diffs in the diffArea ----------- + // recompute + for (let computedDiff of computedDiffs) { + const diffid = this._diffidPool++ - let newCode = `${newFileTop}\n${oldFileBottom}` - diffArea._sweepLine = newFileEndLine + // add the view zone + const greenRange: IRange = { startLineNumber: computedDiff.startLine, startColumn: 1, endLineNumber: computedDiff.endLine, endColumn: Number.MAX_SAFE_INTEGER, } + const disposeDiff = this._addInlineDiffZone(diffArea._model, computedDiff.originalCode, greenRange, diffid) + + // create a Diff of it + const newDiff: Diff = { + diffid: diffid, + diffareaid: diffArea.diffareaid, + _disposeDiff: disposeDiff, + ...computedDiff, + } + + this.diffOfId[diffid] = newDiff + diffArea._diffs.push(newDiff) + } - // applies edits without adding them to undo/redo stack - // model.applyEdits([{ - // range: { startLineNumber: diffArea.startLine, startColumn: 0, endLineNumber: diffArea.endLine, endColumn: Number.MAX_SAFE_INTEGER, }, - // text: newCode - // }]) - // this._bulkEditService.apply([new ResourceTextEdit(model.uri, { - // range: { startLineNumber: diffArea.startLine, startColumn: 0, endLineNumber: diffArea.endLine, endColumn: Number.MAX_SAFE_INTEGER, }, - // text: newCode - // })], { undoRedoGroupId: editorGroup.id }); // count all changes towards the group - this._writeToModel( - model, - newCode, - { startLineNumber: diffArea.startLine, startColumn: 0, endLineNumber: diffArea.endLine, endColumn: Number.MAX_SAFE_INTEGER, }, - editorGroup - ) - - - - // TODO resize diffAreas?? Or is this handled already by the listener? } @@ -604,8 +608,8 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService { } } - // start listening for text changes - this._registerTextChangeListener(model) + // // start listening for text changes + // this._registerTextChangeListener(model) // add to history const { onFinishEdit, editGroup } = this._addToHistory(model) @@ -622,10 +626,10 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService { startLine: beginLine, endLine: endLine, // starts out the same as the current file _model: model, - _sweepLine: null, - _sweepCol: null, + _isStreaming: true, // _generationid: generationid, _diffs: [], // added later + _disposeSweepStyles: null, } this.diffAreasOfModelId[model.id].add(diffArea.diffareaid.toString()) @@ -656,14 +660,10 @@ Please finish writing the new file by applying the diff to the original file. Re { role: 'user', content: promptContent, } ], onText: (newText: string, fullText: string) => { - this._updateDiffAreaText(diffArea, fullText, editGroup) - // this._refreshAllDiffsAndStyles(model) - this._refreshSweepStyles(model) + this._onGetNewDiffAreaText(diffArea, fullText, editGroup) }, onFinalMessage: (fullText: string) => { - this._updateDiffAreaText(diffArea, fullText, editGroup) - // this._refreshAllDiffsAndStyles(model) - this._refreshSweepStyles(model) + this._onGetNewDiffAreaText(diffArea, fullText, editGroup) resolve(); }, onError: (e: any) => { @@ -699,8 +699,6 @@ Please finish writing the new file by applying the diff to the original file. Re if (!(model.id in this.diffAreasOfModelId)) this.diffAreasOfModelId[model.id] = new Set() - this.streamingStateOfModelId[model.id] = { type: 'streaming' } - // initialize stream await this._initializeStream(model, userMessage) @@ -770,7 +768,6 @@ Please finish writing the new file by applying the diff to the original file. Re const shouldDeleteDiffArea = originalArea === currentArea if (shouldDeleteDiffArea) { this._deleteDiffArea(diffArea) - this._refreshAllDiffsAndStyles(model) } onFinishEdit() @@ -819,10 +816,6 @@ Please finish writing the new file by applying the diff to the original file. Re if (shouldDeleteDiffArea) { this._deleteDiffArea(diffArea) } - const editor = this._editorService.getActiveCodeEditor() - if (editor?.getModel()?.id === modelid) { - this._refreshAllDiffsAndStyles(model) - } onFinishEdit()