From a5912ba538c5b3c4571d2d61788fa2a62b73d8d6 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Tue, 12 Nov 2024 03:28:53 -0800 Subject: [PATCH] progress adding diffs --- package-lock.json | 36 +- package.json | 2 + .../workbench/api/common/extHostInlineDiff.ts | 3 +- .../contrib/void/browser/DiffProvider.ts | 450 ++++++++++++++++++ .../contrib/void/browser/findDiffs.ts | 117 +++++ .../contrib/void/browser/misc/DiffProvider.ts | 450 ------------------ .../void/browser/registerInlineDiff.ts | 60 +-- .../void/browser/registerInlineDiffs.ts | 296 ++++++++++++ 8 files changed, 932 insertions(+), 482 deletions(-) create mode 100644 src/vs/workbench/contrib/void/browser/DiffProvider.ts create mode 100644 src/vs/workbench/contrib/void/browser/findDiffs.ts delete mode 100644 src/vs/workbench/contrib/void/browser/misc/DiffProvider.ts create mode 100644 src/vs/workbench/contrib/void/browser/registerInlineDiffs.ts diff --git a/package-lock.json b/package-lock.json index 96102735..b21c406d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -59,6 +59,7 @@ "@swc/core": "1.3.62", "@types/cookie": "^0.3.3", "@types/debug": "^4.1.5", + "@types/diff": "^6.0.0", "@types/gulp-svgmin": "^1.2.1", "@types/http-proxy-agent": "^2.0.1", "@types/kerberos": "^1.1.2", @@ -98,6 +99,7 @@ "cssnano": "^6.0.3", "debounce": "^1.0.0", "deemon": "^1.8.0", + "diff": "^7.0.0", "electron": "30.5.1", "eslint": "8.36.0", "eslint-plugin-header": "3.1.1", @@ -2928,6 +2930,13 @@ "@types/ms": "*" } }, + "node_modules/@types/diff": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@types/diff/-/diff-6.0.0.tgz", + "integrity": "sha512-dhVCYGv3ZSbzmQaBSagrv1WJ6rXCdkyTcDyoNu1MD8JohI7pR7k8wdZEm+mvdxRKXyHVwckFzWU1vJc+Z29MlA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/estree": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", @@ -7108,10 +7117,11 @@ "license": "Apache-2.0" }, "node_modules/diff": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz", - "integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-7.0.0.tgz", + "integrity": "sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw==", "dev": true, + "license": "BSD-3-Clause", "engines": { "node": ">=0.3.1" } @@ -14823,6 +14833,16 @@ "wrap-ansi": "^7.0.0" } }, + "node_modules/mocha/node_modules/diff": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz", + "integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, "node_modules/mocha/node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", @@ -19099,6 +19119,16 @@ "@sinonjs/commons": "^1.7.0" } }, + "node_modules/sinon/node_modules/diff": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz", + "integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, "node_modules/sinon/node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", diff --git a/package.json b/package.json index c363366e..b765c4fa 100644 --- a/package.json +++ b/package.json @@ -121,6 +121,7 @@ "@swc/core": "1.3.62", "@types/cookie": "^0.3.3", "@types/debug": "^4.1.5", + "@types/diff": "^6.0.0", "@types/gulp-svgmin": "^1.2.1", "@types/http-proxy-agent": "^2.0.1", "@types/kerberos": "^1.1.2", @@ -160,6 +161,7 @@ "cssnano": "^6.0.3", "debounce": "^1.0.0", "deemon": "^1.8.0", + "diff": "^7.0.0", "electron": "30.5.1", "eslint": "8.36.0", "eslint-plugin-header": "3.1.1", diff --git a/src/vs/workbench/api/common/extHostInlineDiff.ts b/src/vs/workbench/api/common/extHostInlineDiff.ts index cacfe8fa..033deedd 100644 --- a/src/vs/workbench/api/common/extHostInlineDiff.ts +++ b/src/vs/workbench/api/common/extHostInlineDiff.ts @@ -37,8 +37,9 @@ export class ExtHostInlineDiff implements ExtHostInlineDiffShape { } if (!apiEditor) { throw new Error('not a visible editor'); - } + } + // can't send over the editor, so just send over its id and reconstruct it. This is stupid but it's what VSCode's editorinset does - Andrew const id = apiEditor.id; // let uri = apiEditor.value.document.uri; diff --git a/src/vs/workbench/contrib/void/browser/DiffProvider.ts b/src/vs/workbench/contrib/void/browser/DiffProvider.ts new file mode 100644 index 00000000..032b0892 --- /dev/null +++ b/src/vs/workbench/contrib/void/browser/DiffProvider.ts @@ -0,0 +1,450 @@ +import * as vscode from 'vscode'; +import { findDiffs } from './src/extension/findDiffs'; +import { throttle } from 'lodash'; +import { DiffArea, BaseDiff, Diff } from '../common/shared_types'; +import { readFileContentOfUri } from './src/extension/extensionLib/readFileContentOfUri'; +import { AbortRef, sendLLMMessage } from '../common/sendLLMMessage'; +import { writeFileWithDiffInstructions } from '../common/systemPrompts'; +import { VoidConfig } from './src/webviews/common/contextForConfig'; + + +const THROTTLE_TIME = 100 + +// TODO in theory this should be disposed +const lightGrayDecoration = vscode.window.createTextEditorDecorationType({ + backgroundColor: 'rgba(218 218 218 / .2)', + isWholeLine: true, +}) +const darkGrayDecoration = vscode.window.createTextEditorDecorationType({ + backgroundColor: 'rgb(148 148 148 / .2)', + isWholeLine: true, +}) + +// responsible for displaying diffs and showing accept/reject buttons +export class DiffProvider implements vscode.CodeLensProvider { + + private _originalFileOfDocument: { [docUriStr: string]: string } = {} + private _diffAreasOfDocument: { [docUriStr: string]: DiffArea[] } = {} + private _diffsOfDocument: { [docUriStr: string]: Diff[] } = {} + + private _diffareaidPool = 0 + private _diffidPool = 0 + + private _extensionUri: vscode.Uri + + // used internally by vscode + private _onDidChangeCodeLenses: vscode.EventEmitter = new vscode.EventEmitter(); // signals a UI refresh on .fire() events + public readonly onDidChangeCodeLenses: vscode.Event = this._onDidChangeCodeLenses.event; + + // used internally by vscode + public provideCodeLenses(document: vscode.TextDocument, token: vscode.CancellationToken): vscode.ProviderResult { + const docUriStr = document.uri.toString() + return this._diffsOfDocument[docUriStr]?.flatMap(diff => diff.lenses) ?? [] + } + + // declared by us, registered with vscode.languages.registerCodeLensProvider() + constructor(context: vscode.ExtensionContext) { + this._extensionUri = context.extensionUri + + console.log('Creating DisplayChangesProvider') + + // this acts as a useEffect every time text changes + vscode.workspace.onDidChangeTextDocument((e) => { + + const editor = vscode.window.activeTextEditor + + if (!editor) return + + const docUriStr = editor.document.uri.toString() + const changes = e.contentChanges.map(c => ({ startLine: c.range.start.line, endLine: c.range.end.line, text: c.text, })) + + // on user change, grow/shrink/merge/delete diff areas + this.resizeDiffAreas(docUriStr, changes, 'currentFile') + + // refresh the diffAreas + this.refreshStylesAndDiffs(docUriStr) + + }) + } + + // used by us only + public createDiffArea(uri: vscode.Uri, partialDiffArea: Omit, originalFile: string) { + + const uriStr = uri.toString() + + this._originalFileOfDocument[uriStr] = originalFile + + // make sure array is defined + if (!this._diffAreasOfDocument[uriStr]) this._diffAreasOfDocument[uriStr] = [] + + // remove all diffAreas that the new `diffArea` is overlapping with + this._diffAreasOfDocument[uriStr] = this._diffAreasOfDocument[uriStr].filter(da => { + const noOverlap = da.startLine > partialDiffArea.endLine || da.endLine < partialDiffArea.startLine + if (!noOverlap) return false + return true + }) + + // add `diffArea` to storage + const diffArea = { + ...partialDiffArea, + diffareaid: this._diffareaidPool + } + this._diffAreasOfDocument[uriStr].push(diffArea) + this._diffareaidPool += 1 + + return diffArea + } + + // used by us only + // 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 + public resizeDiffAreas(docUriStr: string, changes: { text: string, startLine: number, endLine: number }[], changesTo: 'originalFile' | 'currentFile') { + + const diffAreas = this._diffAreasOfDocument[docUriStr] || [] + + let endLine: 'originalEndLine' | 'endLine' + let startLine: 'originalStartLine' | 'startLine' + + if (changesTo === 'originalFile') { + endLine = 'originalEndLine' as const + startLine = 'originalStartLine' as const + } else { + endLine = 'endLine' as const + startLine = 'startLine' as const + } + + for (const change of changes) { + + // here, `change.range` is the range of the original file that gets replaced with `change.text` + + + // 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 the diffArea accordingly + for (const diffArea of diffAreas) { + + // 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]) { + //remove it + const index = diffAreas.findIndex(da => da === diffArea) + diffAreas.splice(index, 1) + } + + // TODO handle other cases where eg. the change overlaps many diffAreas + } + + + // if a diffArea is below the last character of the change, shift the diffArea up/down by the delta amount of newlines + for (const diffArea of diffAreas) { + if (diffArea[startLine] > change.endLine) { + diffArea[startLine] += deltaNewlines + diffArea[endLine] += deltaNewlines + } + } + + // TODO merge any diffAreas if they overlap with each other as a result from the shift + + } + } + + + // used by us only + // refreshes all the diffs inside each diff area, and refreshes the styles + public refreshStylesAndDiffs(docUriStr: string) { + + const editor = vscode.window.activeTextEditor // TODO the editor should be that of `docUri` and not necessarily the current editor + if (!editor) { + console.log('Error: No active editor!') + return; + } + const originalFile = this._originalFileOfDocument[docUriStr] + if (!originalFile) { + console.log('Error: No original file!') + return; + } + + const diffAreas = this._diffAreasOfDocument[docUriStr] || [] + + // reset all diffs (we update them below) + this._diffsOfDocument[docUriStr] = [] + + // TODO!!!! + // vscode.languages.clearInlineDiffs(editor) + + // for each diffArea + for (const diffArea of diffAreas) { + + // get code inside of diffArea + const originalCode = originalFile.split('\n').slice(diffArea.originalStartLine, diffArea.originalEndLine + 1).join('\n') + const currentCode = editor.document.getText(new vscode.Range(diffArea.startLine, 0, diffArea.endLine, Number.MAX_SAFE_INTEGER)).replace(/\r\n/g, '\n') + + // compute the diffs + const diffs = findDiffs(originalCode, currentCode) + + // add the diffs to `this._diffsOfDocument[docUriStr]` + + // if no diffs, set diffs to [] + if (!this._diffsOfDocument[docUriStr]) + this._diffsOfDocument[docUriStr] = [] + + // add each diff and its codelens to the document + for (let i = diffs.length - 1; i > -1; i -= 1) { + let suggestedDiff = diffs[i] + + this._diffsOfDocument[docUriStr].push({ + ...suggestedDiff, + diffid: this._diffidPool, + // originalCode: suggestedDiff.deletedText, + lenses: [ + new vscode.CodeLens(suggestedDiff.range, { title: 'Accept', command: 'void.acceptDiff', arguments: [{ diffid: this._diffidPool, diffareaid: diffArea.diffareaid }] }), + new vscode.CodeLens(suggestedDiff.range, { title: 'Reject', command: 'void.rejectDiff', arguments: [{ diffid: this._diffidPool, diffareaid: diffArea.diffareaid }] }) + ] + }); + vscode.languages.addInlineDiff(editor, suggestedDiff.originalCode, suggestedDiff.range) + this._diffidPool += 1 + } + + } + + + // for each diffArea, highlight its sweepIndex in dark gray + editor.setDecorations( + darkGrayDecoration, + (this._diffAreasOfDocument[docUriStr] + .filter(diffArea => diffArea.sweepIndex !== null) + .map(diffArea => { + let s = diffArea.sweepIndex! + return new vscode.Range(s, 0, s, 0) + }) + ) + ) + + // for each diffArea, highlight sweepIndex+1...end in light gray + editor.setDecorations( + lightGrayDecoration, + (this._diffAreasOfDocument[docUriStr] + .filter(diffArea => diffArea.sweepIndex !== null) + .map(diffArea => { + return new vscode.Range(diffArea.sweepIndex! + 1, 0, diffArea.endLine, 0) + }) + ) + ) + + + // update code lenses + this._onDidChangeCodeLenses.fire() + + } + + + // called on void.acceptDiff + public async acceptDiff({ diffid, diffareaid }: { diffid: number, diffareaid: number }) { + const editor = vscode.window.activeTextEditor + if (!editor) + return + + const docUriStr = editor.document.uri.toString() + + const diffIdx = this._diffsOfDocument[docUriStr].findIndex(diff => diff.diffid === diffid); + if (diffIdx === -1) { console.error('Error: DiffID could not be found: ', diffid, diffareaid, this._diffsOfDocument[docUriStr], this._diffAreasOfDocument[docUriStr]); return; } + + const diffareaIdx = this._diffAreasOfDocument[docUriStr].findIndex(diff => diff.diffareaid === diffareaid); + if (diffareaIdx === -1) { console.error('Error: DiffAreaID could not be found: ', diffid, diffareaid, this._diffsOfDocument[docUriStr], this._diffAreasOfDocument[docUriStr]); return; } + + const diff = this._diffsOfDocument[docUriStr][diffIdx] + const originalFile = this._originalFileOfDocument[docUriStr] + const currentFile = await readFileContentOfUri(editor.document.uri) + + // Fixed: Handle newlines properly by splitting into lines and joining with proper newlines + const originalLines = originalFile.split('\n'); + const currentLines = currentFile.split('\n'); + + // Get the changed lines from current file + const changedLines = currentLines.slice(diff.range.start.line, diff.range.end.line + 1); + + // Create new original file content by replacing the affected lines + const newOriginalLines = [ + ...originalLines.slice(0, diff.originalRange.start.line), + ...changedLines, + ...originalLines.slice(diff.originalRange.end.line + 1) + ]; + + this._originalFileOfDocument[docUriStr] = newOriginalLines.join('\n'); + + // Update diff areas based on the change + this.resizeDiffAreas(docUriStr, [{ + text: changedLines.join('\n'), + startLine: diff.originalRange.start.line, + endLine: diff.originalRange.end.line + }], 'originalFile') + + // Check if diffArea should be removed + + const diffArea = this._diffAreasOfDocument[docUriStr][diffareaIdx] + + const currentArea = currentLines.slice(diffArea.startLine, diffArea.endLine + 1).join('\n') + const originalArea = newOriginalLines.slice(diffArea.originalStartLine, diffArea.originalEndLine + 1).join('\n') + + if (originalArea === currentArea) { + const index = this._diffAreasOfDocument[docUriStr].findIndex(da => da.diffareaid === diffArea.diffareaid) + this._diffAreasOfDocument[docUriStr].splice(index, 1) + } + + this.refreshStylesAndDiffs(docUriStr) + } + + // called on void.rejectDiff + public async rejectDiff({ diffid, diffareaid }: { diffid: number, diffareaid: number }) { + const editor = vscode.window.activeTextEditor + if (!editor) + return + + const docUriStr = editor.document.uri.toString() + + const diffIdx = this._diffsOfDocument[docUriStr].findIndex(diff => diff.diffid === diffid); + if (diffIdx === -1) { console.error('Error: DiffID could not be found: ', diffid, diffareaid, this._diffsOfDocument[docUriStr], this._diffAreasOfDocument[docUriStr]); return; } + + const diffareaIdx = this._diffAreasOfDocument[docUriStr].findIndex(diff => diff.diffareaid === diffareaid); + if (diffareaIdx === -1) { console.error('Error: DiffAreaID could not be found: ', diffid, diffareaid, this._diffsOfDocument[docUriStr], this._diffAreasOfDocument[docUriStr]); return; } + + const diff = this._diffsOfDocument[docUriStr][diffIdx] + + // Apply the rejection by replacing with original code + // we don't have to edit the original or final file; just do a workspace edit so the code equals the original code + const workspaceEdit = new vscode.WorkspaceEdit(); + workspaceEdit.replace(editor.document.uri, diff.range, diff.originalCode) + await vscode.workspace.applyEdit(workspaceEdit) + + // Check if diffArea should be removed + const originalFile = this._originalFileOfDocument[docUriStr] + const currentFile = await readFileContentOfUri(editor.document.uri) + const diffArea = this._diffAreasOfDocument[docUriStr][diffareaIdx] + const currentLines = currentFile.split('\n'); + const originalLines = originalFile.split('\n'); + + const currentArea = currentLines.slice(diffArea.startLine, diffArea.endLine + 1).join('\n') + const originalArea = originalLines.slice(diffArea.originalStartLine, diffArea.originalEndLine + 1).join('\n') + + if (originalArea === currentArea) { + const index = this._diffAreasOfDocument[docUriStr].findIndex(da => da.diffareaid === diffArea.diffareaid) + this._diffAreasOfDocument[docUriStr].splice(index, 1) + } + + this.refreshStylesAndDiffs(docUriStr) + } + + async startStreamingInDiffArea({ docUri, oldFileStr, diffRepr, diffArea, voidConfig, abortRef }: { docUri: vscode.Uri, oldFileStr: string, diffRepr: string, voidConfig: VoidConfig, diffArea: DiffArea, abortRef: AbortRef }) { + + + const promptContent = `\ +ORIGINAL_FILE +\`\`\` +${oldFileStr} +\`\`\` + +DIFF +\`\`\` +${diffRepr} +\`\`\` + +INSTRUCTIONS +Please finish writing the new file by applying the diff to the original file. Return ONLY the completion of the file, without any explanation. + +` + // make LLM complete the file to include the diff + await new Promise((resolve, reject) => { + sendLLMMessage({ + logging: { loggingName: 'streamChunk' }, + messages: [ + { role: 'system', content: writeFileWithDiffInstructions, }, + // TODO include more context too + { role: 'user', content: promptContent, } + ], + onText: (newText, fullText) => { + this._updateStream(docUri.toString(), diffArea, fullText) + }, + onFinalMessage: (fullText) => { + this._updateStream(docUri.toString(), diffArea, fullText) + resolve(); + }, + onError: (e) => { + console.error('Error rewriting file with diff', e); + resolve(); + }, + voidConfig, + abortRef, + }) + }) + + } + + + // used by us only + private _updateStream = throttle(async (docUriStr: string, diffArea: DiffArea, newDiffAreaCode: string) => { + + const editor = vscode.window.activeTextEditor // TODO the editor should be that of `docUri` and not necessarily the current editor + if (!editor) { + console.log('Error: No active editor!') + return; + } + + // original code all diffs are based on in the code + const originalDiffAreaCode = (this._originalFileOfDocument[docUriStr] || '').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 in findDiffs to figure that out + const diffs = findDiffs(originalDiffAreaCode, newDiffAreaCode) + const lastDiff = diffs?.[diffs.length - 1] ?? null + + // 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 + } + else { + if (lastDiff.type === 'insertion') { + newFileEndLine = lastDiff.range.end.line + oldFileStartLine = lastDiff.originalRange.start.line + } + else if (lastDiff.type === 'deletion') { + newFileEndLine = lastDiff.range.start.line + oldFileStartLine = lastDiff.originalRange.start.line + } + else if (lastDiff.type === 'edit') { + newFileEndLine = lastDiff.range.end.line + oldFileStartLine = lastDiff.originalRange.start.line + } + else { + throw new Error(`updateStream: diff.type not recognized: ${lastDiff.type}`) + } + } + + // display + const newFileTop = newDiffAreaCode.split('\n').slice(0, newFileEndLine + 1).join('\n') + const oldFileBottom = originalDiffAreaCode.split('\n').slice(oldFileStartLine + 1, Infinity).join('\n') + + let newCode = `${newFileTop}\n${oldFileBottom}` + diffArea.sweepIndex = newFileEndLine + // replace oldDACode with newDACode with a vscode edit + + const workspaceEdit = new vscode.WorkspaceEdit(); + + const diffareaRange = new vscode.Range(diffArea.startLine, 0, diffArea.endLine, Number.MAX_SAFE_INTEGER) + workspaceEdit.replace(editor.document.uri, diffareaRange, newCode) + await vscode.workspace.applyEdit(workspaceEdit) + }, THROTTLE_TIME) + +} + + + diff --git a/src/vs/workbench/contrib/void/browser/findDiffs.ts b/src/vs/workbench/contrib/void/browser/findDiffs.ts new file mode 100644 index 00000000..a823b0d0 --- /dev/null +++ b/src/vs/workbench/contrib/void/browser/findDiffs.ts @@ -0,0 +1,117 @@ + +import { Range } from 'vscode'; +import { diffLines, Change } from 'diff'; +import { Diff } from './registerInlineDiffs'; + +type BaseDiff = Omit + +// Andrew diff algo: +export type SuggestedEdit = { + // start/end of current file + newRange: Range; + + // start/end of original file + originalRange: Range; + type: 'insertion' | 'deletion' | 'edit', + originalContent: string, // original content (originalfile[originalStart...originalEnd]) + newContent: string, +} + +export function findDiffs(oldStr: string, newStr: string) { + // an ordered list of every original line, line added to the new file, and line removed from the old file (order is unambiguous, think about it) + const lineByLineChanges: Change[] = 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 streakStartInNewFile: number | undefined = undefined + let streakStartInOldFile: number | undefined = undefined + + let oldStrLines = oldStr.split('\n') + let newStrLines = newStr.split('\n') + + const replacements: BaseDiff[] = [] + for (let line of lineByLineChanges) { + + // no change on this line + if (!line.added && !line.removed) { + + // do nothing + + // if we were on a streak of +s and -s, end it + if (streakStartInNewFile !== undefined) { + let type: 'edit' | 'insertion' | 'deletion' = 'edit' + + 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 endCol = Number.MAX_SAFE_INTEGER + + let originalStartLine = streakStartInOldFile! + let originalEndLine = oldFileLineNum - 1 // don't include current line, the edit was up to this line but not including it + let originalStartCol = 0 + // let originalEndCol = Number.MAX_SAFE_INTEGER + + 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) + // DELETION + if (endLine === startLine - 1) { + type = 'deletion' + endLine = startLine + startCol = 0 + endCol = 0 + newContent += '\n' + } + + // INSERTION + else if (originalEndLine === originalStartLine - 1) { + type = 'insertion' + originalEndLine = originalStartLine + originalStartCol = 0 + // originalEndCol = 0 + } + + const replacement: BaseDiff = { + type, + startLine, startCol, endLine, endCol, + // code: newContent, + // originalRange: new Range(originalStartLine, originalStartCol, originalEndLine, originalEndCol), + originalCode: originalContent, + } + + replacements.push(replacement) + + streakStartInNewFile = undefined + streakStartInOldFile = undefined + } + oldFileLineNum += line.count ?? 0; + newFileLineNum += line.count ?? 0; + } + + // line was removed from old file + else if (line.removed) { + // if we weren't on a streak, start one on this current line num + if (streakStartInNewFile === undefined) { + streakStartInNewFile = newFileLineNum + streakStartInOldFile = oldFileLineNum + } + oldFileLineNum += line.count ?? 0 // we processed the line so add 1 + } + + // line was added to new file + else if (line.added) { + // if we weren't on a streak, start one on this current line num + if (streakStartInNewFile === undefined) { + streakStartInNewFile = newFileLineNum + streakStartInOldFile = oldFileLineNum + } + newFileLineNum += line.count ?? 0; // we processed the line so add 1 + } + } // end for + + // console.debug('Replacements', replacements) + return replacements +} diff --git a/src/vs/workbench/contrib/void/browser/misc/DiffProvider.ts b/src/vs/workbench/contrib/void/browser/misc/DiffProvider.ts deleted file mode 100644 index a361002b..00000000 --- a/src/vs/workbench/contrib/void/browser/misc/DiffProvider.ts +++ /dev/null @@ -1,450 +0,0 @@ -// import * as vscode from 'vscode'; -// import { findDiffs } from './src/extension/findDiffs'; -// import { throttle } from 'lodash'; -// import { DiffArea, BaseDiff, Diff } from '../common/shared_types'; -// import { readFileContentOfUri } from './src/extension/extensionLib/readFileContentOfUri'; -// import { AbortRef, sendLLMMessage } from '../common/sendLLMMessage'; -// import { writeFileWithDiffInstructions } from '../common/systemPrompts'; -// import { VoidConfig } from './src/webviews/common/contextForConfig'; - - -// const THROTTLE_TIME = 100 - -// // TODO in theory this should be disposed -// const lightGrayDecoration = vscode.window.createTextEditorDecorationType({ -// backgroundColor: 'rgba(218 218 218 / .2)', -// isWholeLine: true, -// }) -// const darkGrayDecoration = vscode.window.createTextEditorDecorationType({ -// backgroundColor: 'rgb(148 148 148 / .2)', -// isWholeLine: true, -// }) - -// // responsible for displaying diffs and showing accept/reject buttons -// export class DiffProvider implements vscode.CodeLensProvider { - -// private _originalFileOfDocument: { [docUriStr: string]: string } = {} -// private _diffAreasOfDocument: { [docUriStr: string]: DiffArea[] } = {} -// private _diffsOfDocument: { [docUriStr: string]: Diff[] } = {} - -// private _diffareaidPool = 0 -// private _diffidPool = 0 - -// private _extensionUri: vscode.Uri - -// // used internally by vscode -// private _onDidChangeCodeLenses: vscode.EventEmitter = new vscode.EventEmitter(); // signals a UI refresh on .fire() events -// public readonly onDidChangeCodeLenses: vscode.Event = this._onDidChangeCodeLenses.event; - -// // used internally by vscode -// public provideCodeLenses(document: vscode.TextDocument, token: vscode.CancellationToken): vscode.ProviderResult { -// const docUriStr = document.uri.toString() -// return this._diffsOfDocument[docUriStr]?.flatMap(diff => diff.lenses) ?? [] -// } - -// // declared by us, registered with vscode.languages.registerCodeLensProvider() -// constructor(context: vscode.ExtensionContext) { -// this._extensionUri = context.extensionUri - -// console.log('Creating DisplayChangesProvider') - -// // this acts as a useEffect every time text changes -// vscode.workspace.onDidChangeTextDocument((e) => { - -// const editor = vscode.window.activeTextEditor - -// if (!editor) return - -// const docUriStr = editor.document.uri.toString() -// const changes = e.contentChanges.map(c => ({ startLine: c.range.start.line, endLine: c.range.end.line, text: c.text, })) - -// // on user change, grow/shrink/merge/delete diff areas -// this.resizeDiffAreas(docUriStr, changes, 'currentFile') - -// // refresh the diffAreas -// this.refreshStylesAndDiffs(docUriStr) - -// }) -// } - -// // used by us only -// public createDiffArea(uri: vscode.Uri, partialDiffArea: Omit, originalFile: string) { - -// const uriStr = uri.toString() - -// this._originalFileOfDocument[uriStr] = originalFile - -// // make sure array is defined -// if (!this._diffAreasOfDocument[uriStr]) this._diffAreasOfDocument[uriStr] = [] - -// // remove all diffAreas that the new `diffArea` is overlapping with -// this._diffAreasOfDocument[uriStr] = this._diffAreasOfDocument[uriStr].filter(da => { -// const noOverlap = da.startLine > partialDiffArea.endLine || da.endLine < partialDiffArea.startLine -// if (!noOverlap) return false -// return true -// }) - -// // add `diffArea` to storage -// const diffArea = { -// ...partialDiffArea, -// diffareaid: this._diffareaidPool -// } -// this._diffAreasOfDocument[uriStr].push(diffArea) -// this._diffareaidPool += 1 - -// return diffArea -// } - -// // used by us only -// // 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 -// public resizeDiffAreas(docUriStr: string, changes: { text: string, startLine: number, endLine: number }[], changesTo: 'originalFile' | 'currentFile') { - -// const diffAreas = this._diffAreasOfDocument[docUriStr] || [] - -// let endLine: 'originalEndLine' | 'endLine' -// let startLine: 'originalStartLine' | 'startLine' - -// if (changesTo === 'originalFile') { -// endLine = 'originalEndLine' as const -// startLine = 'originalStartLine' as const -// } else { -// endLine = 'endLine' as const -// startLine = 'startLine' as const -// } - -// for (const change of changes) { - -// // here, `change.range` is the range of the original file that gets replaced with `change.text` - - -// // 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 the diffArea accordingly -// for (const diffArea of diffAreas) { - -// // 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]) { -// //remove it -// const index = diffAreas.findIndex(da => da === diffArea) -// diffAreas.splice(index, 1) -// } - -// // TODO handle other cases where eg. the change overlaps many diffAreas -// } - - -// // if a diffArea is below the last character of the change, shift the diffArea up/down by the delta amount of newlines -// for (const diffArea of diffAreas) { -// if (diffArea[startLine] > change.endLine) { -// diffArea[startLine] += deltaNewlines -// diffArea[endLine] += deltaNewlines -// } -// } - -// // TODO merge any diffAreas if they overlap with each other as a result from the shift - -// } -// } - - -// // used by us only -// // refreshes all the diffs inside each diff area, and refreshes the styles -// public refreshStylesAndDiffs(docUriStr: string) { - -// const editor = vscode.window.activeTextEditor // TODO the editor should be that of `docUri` and not necessarily the current editor -// if (!editor) { -// console.log('Error: No active editor!') -// return; -// } -// const originalFile = this._originalFileOfDocument[docUriStr] -// if (!originalFile) { -// console.log('Error: No original file!') -// return; -// } - -// const diffAreas = this._diffAreasOfDocument[docUriStr] || [] - -// // reset all diffs (we update them below) -// this._diffsOfDocument[docUriStr] = [] - -// // TODO!!!! -// // vscode.languages.clearInlineDiffs(editor) - -// // for each diffArea -// for (const diffArea of diffAreas) { - -// // get code inside of diffArea -// const originalCode = originalFile.split('\n').slice(diffArea.originalStartLine, diffArea.originalEndLine + 1).join('\n') -// const currentCode = editor.document.getText(new vscode.Range(diffArea.startLine, 0, diffArea.endLine, Number.MAX_SAFE_INTEGER)).replace(/\r\n/g, '\n') - -// // compute the diffs -// const diffs = findDiffs(originalCode, currentCode) - -// // add the diffs to `this._diffsOfDocument[docUriStr]` - -// // if no diffs, set diffs to [] -// if (!this._diffsOfDocument[docUriStr]) -// this._diffsOfDocument[docUriStr] = [] - -// // add each diff and its codelens to the document -// for (let i = diffs.length - 1; i > -1; i -= 1) { -// let suggestedDiff = diffs[i] - -// this._diffsOfDocument[docUriStr].push({ -// ...suggestedDiff, -// diffid: this._diffidPool, -// // originalCode: suggestedDiff.deletedText, -// lenses: [ -// new vscode.CodeLens(suggestedDiff.range, { title: 'Accept', command: 'void.acceptDiff', arguments: [{ diffid: this._diffidPool, diffareaid: diffArea.diffareaid }] }), -// new vscode.CodeLens(suggestedDiff.range, { title: 'Reject', command: 'void.rejectDiff', arguments: [{ diffid: this._diffidPool, diffareaid: diffArea.diffareaid }] }) -// ] -// }); -// vscode.languages.addInlineDiff(editor, suggestedDiff.originalCode, suggestedDiff.range) -// this._diffidPool += 1 -// } - -// } - - -// // for each diffArea, highlight its sweepIndex in dark gray -// editor.setDecorations( -// darkGrayDecoration, -// (this._diffAreasOfDocument[docUriStr] -// .filter(diffArea => diffArea.sweepIndex !== null) -// .map(diffArea => { -// let s = diffArea.sweepIndex! -// return new vscode.Range(s, 0, s, 0) -// }) -// ) -// ) - -// // for each diffArea, highlight sweepIndex+1...end in light gray -// editor.setDecorations( -// lightGrayDecoration, -// (this._diffAreasOfDocument[docUriStr] -// .filter(diffArea => diffArea.sweepIndex !== null) -// .map(diffArea => { -// return new vscode.Range(diffArea.sweepIndex! + 1, 0, diffArea.endLine, 0) -// }) -// ) -// ) - - -// // update code lenses -// this._onDidChangeCodeLenses.fire() - -// } - - -// // called on void.acceptDiff -// public async acceptDiff({ diffid, diffareaid }: { diffid: number, diffareaid: number }) { -// const editor = vscode.window.activeTextEditor -// if (!editor) -// return - -// const docUriStr = editor.document.uri.toString() - -// const diffIdx = this._diffsOfDocument[docUriStr].findIndex(diff => diff.diffid === diffid); -// if (diffIdx === -1) { console.error('Error: DiffID could not be found: ', diffid, diffareaid, this._diffsOfDocument[docUriStr], this._diffAreasOfDocument[docUriStr]); return; } - -// const diffareaIdx = this._diffAreasOfDocument[docUriStr].findIndex(diff => diff.diffareaid === diffareaid); -// if (diffareaIdx === -1) { console.error('Error: DiffAreaID could not be found: ', diffid, diffareaid, this._diffsOfDocument[docUriStr], this._diffAreasOfDocument[docUriStr]); return; } - -// const diff = this._diffsOfDocument[docUriStr][diffIdx] -// const originalFile = this._originalFileOfDocument[docUriStr] -// const currentFile = await readFileContentOfUri(editor.document.uri) - -// // Fixed: Handle newlines properly by splitting into lines and joining with proper newlines -// const originalLines = originalFile.split('\n'); -// const currentLines = currentFile.split('\n'); - -// // Get the changed lines from current file -// const changedLines = currentLines.slice(diff.range.start.line, diff.range.end.line + 1); - -// // Create new original file content by replacing the affected lines -// const newOriginalLines = [ -// ...originalLines.slice(0, diff.originalRange.start.line), -// ...changedLines, -// ...originalLines.slice(diff.originalRange.end.line + 1) -// ]; - -// this._originalFileOfDocument[docUriStr] = newOriginalLines.join('\n'); - -// // Update diff areas based on the change -// this.resizeDiffAreas(docUriStr, [{ -// text: changedLines.join('\n'), -// startLine: diff.originalRange.start.line, -// endLine: diff.originalRange.end.line -// }], 'originalFile') - -// // Check if diffArea should be removed - -// const diffArea = this._diffAreasOfDocument[docUriStr][diffareaIdx] - -// const currentArea = currentLines.slice(diffArea.startLine, diffArea.endLine + 1).join('\n') -// const originalArea = newOriginalLines.slice(diffArea.originalStartLine, diffArea.originalEndLine + 1).join('\n') - -// if (originalArea === currentArea) { -// const index = this._diffAreasOfDocument[docUriStr].findIndex(da => da.diffareaid === diffArea.diffareaid) -// this._diffAreasOfDocument[docUriStr].splice(index, 1) -// } - -// this.refreshStylesAndDiffs(docUriStr) -// } - -// // called on void.rejectDiff -// public async rejectDiff({ diffid, diffareaid }: { diffid: number, diffareaid: number }) { -// const editor = vscode.window.activeTextEditor -// if (!editor) -// return - -// const docUriStr = editor.document.uri.toString() - -// const diffIdx = this._diffsOfDocument[docUriStr].findIndex(diff => diff.diffid === diffid); -// if (diffIdx === -1) { console.error('Error: DiffID could not be found: ', diffid, diffareaid, this._diffsOfDocument[docUriStr], this._diffAreasOfDocument[docUriStr]); return; } - -// const diffareaIdx = this._diffAreasOfDocument[docUriStr].findIndex(diff => diff.diffareaid === diffareaid); -// if (diffareaIdx === -1) { console.error('Error: DiffAreaID could not be found: ', diffid, diffareaid, this._diffsOfDocument[docUriStr], this._diffAreasOfDocument[docUriStr]); return; } - -// const diff = this._diffsOfDocument[docUriStr][diffIdx] - -// // Apply the rejection by replacing with original code -// // we don't have to edit the original or final file; just do a workspace edit so the code equals the original code -// const workspaceEdit = new vscode.WorkspaceEdit(); -// workspaceEdit.replace(editor.document.uri, diff.range, diff.originalCode) -// await vscode.workspace.applyEdit(workspaceEdit) - -// // Check if diffArea should be removed -// const originalFile = this._originalFileOfDocument[docUriStr] -// const currentFile = await readFileContentOfUri(editor.document.uri) -// const diffArea = this._diffAreasOfDocument[docUriStr][diffareaIdx] -// const currentLines = currentFile.split('\n'); -// const originalLines = originalFile.split('\n'); - -// const currentArea = currentLines.slice(diffArea.startLine, diffArea.endLine + 1).join('\n') -// const originalArea = originalLines.slice(diffArea.originalStartLine, diffArea.originalEndLine + 1).join('\n') - -// if (originalArea === currentArea) { -// const index = this._diffAreasOfDocument[docUriStr].findIndex(da => da.diffareaid === diffArea.diffareaid) -// this._diffAreasOfDocument[docUriStr].splice(index, 1) -// } - -// this.refreshStylesAndDiffs(docUriStr) -// } - -// async startStreamingInDiffArea({ docUri, oldFileStr, diffRepr, diffArea, voidConfig, abortRef }: { docUri: vscode.Uri, oldFileStr: string, diffRepr: string, voidConfig: VoidConfig, diffArea: DiffArea, abortRef: AbortRef }) { - - -// const promptContent = `\ -// ORIGINAL_FILE -// \`\`\` -// ${oldFileStr} -// \`\`\` - -// DIFF -// \`\`\` -// ${diffRepr} -// \`\`\` - -// INSTRUCTIONS -// Please finish writing the new file by applying the diff to the original file. Return ONLY the completion of the file, without any explanation. - -// ` -// // make LLM complete the file to include the diff -// await new Promise((resolve, reject) => { -// sendLLMMessage({ -// logging: { loggingName: 'streamChunk' }, -// messages: [ -// { role: 'system', content: writeFileWithDiffInstructions, }, -// // TODO include more context too -// { role: 'user', content: promptContent, } -// ], -// onText: (newText, fullText) => { -// this._updateStream(docUri.toString(), diffArea, fullText) -// }, -// onFinalMessage: (fullText) => { -// this._updateStream(docUri.toString(), diffArea, fullText) -// resolve(); -// }, -// onError: (e) => { -// console.error('Error rewriting file with diff', e); -// resolve(); -// }, -// voidConfig, -// abortRef, -// }) -// }) - -// } - - -// // used by us only -// private _updateStream = throttle(async (docUriStr: string, diffArea: DiffArea, newDiffAreaCode: string) => { - -// const editor = vscode.window.activeTextEditor // TODO the editor should be that of `docUri` and not necessarily the current editor -// if (!editor) { -// console.log('Error: No active editor!') -// return; -// } - -// // original code all diffs are based on in the code -// const originalDiffAreaCode = (this._originalFileOfDocument[docUriStr] || '').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 in findDiffs to figure that out -// const diffs = findDiffs(originalDiffAreaCode, newDiffAreaCode) -// const lastDiff = diffs?.[diffs.length - 1] ?? null - -// // 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 -// } -// else { -// if (lastDiff.type === 'insertion') { -// newFileEndLine = lastDiff.range.end.line -// oldFileStartLine = lastDiff.originalRange.start.line -// } -// else if (lastDiff.type === 'deletion') { -// newFileEndLine = lastDiff.range.start.line -// oldFileStartLine = lastDiff.originalRange.start.line -// } -// else if (lastDiff.type === 'edit') { -// newFileEndLine = lastDiff.range.end.line -// oldFileStartLine = lastDiff.originalRange.start.line -// } -// else { -// throw new Error(`updateStream: diff.type not recognized: ${lastDiff.type}`) -// } -// } - -// // display -// const newFileTop = newDiffAreaCode.split('\n').slice(0, newFileEndLine + 1).join('\n') -// const oldFileBottom = originalDiffAreaCode.split('\n').slice(oldFileStartLine + 1, Infinity).join('\n') - -// let newCode = `${newFileTop}\n${oldFileBottom}` -// diffArea.sweepIndex = newFileEndLine -// // replace oldDACode with newDACode with a vscode edit - -// const workspaceEdit = new vscode.WorkspaceEdit(); - -// const diffareaRange = new vscode.Range(diffArea.startLine, 0, diffArea.endLine, Number.MAX_SAFE_INTEGER) -// workspaceEdit.replace(editor.document.uri, diffareaRange, newCode) -// await vscode.workspace.applyEdit(workspaceEdit) -// }, THROTTLE_TIME) - -// } - - - diff --git a/src/vs/workbench/contrib/void/browser/registerInlineDiff.ts b/src/vs/workbench/contrib/void/browser/registerInlineDiff.ts index 41df60de..c26168cf 100644 --- a/src/vs/workbench/contrib/void/browser/registerInlineDiff.ts +++ b/src/vs/workbench/contrib/void/browser/registerInlineDiff.ts @@ -4,35 +4,38 @@ import { registerSingleton, InstantiationType } from '../../../../platform/insta import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; import { ICodeEditor, IViewZone } from '../../../../editor/browser/editorBrowser.js'; -import { IModelDeltaDecoration } from '../../../common/model.js'; -import { IRange } from '../../../common/core/range.js'; -import { EditorOption } from '../../../common/config/editorOptions.js'; -import { UndoRedoGroup } from '../../../../platform/undoRedo/common/undoRedo.js'; +import { IUndoRedoElement, IUndoRedoService, UndoRedoElementType, UndoRedoGroup } from '../../../../platform/undoRedo/common/undoRedo.js'; +import { ICodeEditorService } from '../../../../editor/browser/services/codeEditorService.js'; +import { IBulkEditService } from '../../../../editor/browser/services/bulkEditService.js'; +import { WorkspaceEdit } from 'vscode'; +import { IModelDeltaDecoration } from '../../../../editor/common/model.js'; +import { IRange } from '../../../../editor/common/core/range.js'; +import { EditorOption } from '../../../../editor/common/config/editorOptions.js'; -if (m.type === 'applyChanges') { +// if (m.type === 'applyChanges') { - const editor = vscode.window.activeTextEditor - if (!editor) { - vscode.window.showInformationMessage('No active editor!') - return - } - // create an area to show diffs - const partialDiffArea: Omit = { - startLine: 0, // in ctrl+L the start and end lines are the full document - endLine: editor.document.lineCount, - originalStartLine: 0, - originalEndLine: editor.document.lineCount, - sweepIndex: null, - } - const diffArea = diffProvider.createDiffArea(editor.document.uri, partialDiffArea, await readFileContentOfUri(editor.document.uri)) +// const editor = vscode.window.activeTextEditor +// if (!editor) { +// vscode.window.showInformationMessage('No active editor!') +// return +// } +// // create an area to show diffs +// const partialDiffArea: Omit = { +// startLine: 0, // in ctrl+L the start and end lines are the full document +// endLine: editor.document.lineCount, +// originalStartLine: 0, +// originalEndLine: editor.document.lineCount, +// sweepIndex: null, +// } +// const diffArea = diffProvider.createDiffArea(editor.document.uri, partialDiffArea, await readFileContentOfUri(editor.document.uri)) - const docUri = editor.document.uri - const fileStr = await readFileContentOfUri(docUri) - const voidConfig = getVoidConfigFromPartial(context.globalState.get('partialVoidConfig') ?? {}) +// const docUri = editor.document.uri +// const fileStr = await readFileContentOfUri(docUri) +// const voidConfig = getVoidConfigFromPartial(context.globalState.get('partialVoidConfig') ?? {}) - await diffProvider.startStreamingInDiffArea({ docUri, oldFileStr: fileStr, diffRepr: m.diffRepr, voidConfig, diffArea, abortRef: abortApplyRef }) -} +// await diffProvider.startStreamingInDiffArea({ docUri, oldFileStr: fileStr, diffRepr: m.diffRepr, voidConfig, diffArea, abortRef: abortApplyRef }) +// } @@ -50,16 +53,16 @@ type DiffArea = { type BaseDiff = { type: 'edit' | 'insertion' | 'deletion'; // repr: string; // representation of the diff in text - originalRange: vscode.Range; + originalRange: IRange; originalCode: string; - range: vscode.Range; + range: IRange; code: string; } // each diff on the user's screen type Diff = BaseDiff & { diffid: number, - lenses: vscode.CodeLens[], + lenses: CodeLens[], } @@ -75,6 +78,8 @@ export const IInlineDiffService = createDecorator('inlineDif class InlineDiffService extends Disposable implements IInlineDiffService { private readonly _diffDecorations = new Map(); private readonly _diffZones = new Map(); + + _serviceBrand: undefined; constructor() { @@ -200,7 +205,6 @@ class StreamManager extends Disposable { constructor( - context: IExtHostContext, @IInlineDiffService private readonly _inlineDiff: IInlineDiffService, @ICodeEditorService private readonly _editorService: ICodeEditorService, // @IHistoryService private readonly _historyService: IHistoryService, // history service is the history of pressing alt left/right diff --git a/src/vs/workbench/contrib/void/browser/registerInlineDiffs.ts b/src/vs/workbench/contrib/void/browser/registerInlineDiffs.ts new file mode 100644 index 00000000..36b3d829 --- /dev/null +++ b/src/vs/workbench/contrib/void/browser/registerInlineDiffs.ts @@ -0,0 +1,296 @@ + +import { Disposable } from '../../../../base/common/lifecycle.js'; +import { registerSingleton, InstantiationType } from '../../../../platform/instantiation/common/extensions.js'; +import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; +import { ICodeEditor, IViewZone } from '../../../../editor/browser/editorBrowser.js'; + +import { IUndoRedoElement, IUndoRedoService, UndoRedoElementType, UndoRedoGroup } from '../../../../platform/undoRedo/common/undoRedo.js'; +import { ICodeEditorService } from '../../../../editor/browser/services/codeEditorService.js'; +import { IBulkEditService } from '../../../../editor/browser/services/bulkEditService.js'; +import { WorkspaceEdit } from 'vscode'; +import { EditorOption } from '../../../../editor/common/config/editorOptions.js'; +import { Emitter } from '../../../../base/common/event.js'; + + + + +// red in a view zone +editor.changeViewZones(accessor => { + // Get the editor's font info + const fontInfo = editor.getOption(EditorOption.fontInfo); + + const domNode = document.createElement('div'); + domNode.className = 'monaco-editor view-zones line-delete monaco-mouse-cursor-text'; + domNode.style.fontSize = `${fontInfo.fontSize}px`; + domNode.style.fontFamily = fontInfo.fontFamily; + domNode.style.lineHeight = `${fontInfo.lineHeight}px`; + + // div + const lineContent = document.createElement('div'); + lineContent.className = 'view-line'; // .monaco-editor .inline-deleted-text + + // span + const contentSpan = document.createElement('span'); + + // span + const codeSpan = document.createElement('span'); + codeSpan.className = 'mtk1'; // char-delete + codeSpan.textContent = originalText; + + // Mount + contentSpan.appendChild(codeSpan); + lineContent.appendChild(contentSpan); + domNode.appendChild(lineContent); + + // gutter element + const gutterDiv = document.createElement('div'); + gutterDiv.className = 'inline-diff-gutter'; + const minusDiv = document.createElement('div'); + minusDiv.className = 'inline-diff-deleted-gutter'; + // minusDiv.textContent = '-'; + gutterDiv.appendChild(minusDiv); + + const viewZone: IViewZone = { + afterLineNumber: modifiedRange.startLineNumber - 1, + heightInLines: originalText.split('\n').length + 1, + domNode: domNode, + suppressMouseDown: true, + marginDomNode: gutterDiv + }; + + const zoneId = accessor.addZone(viewZone); + // editor.layout(); + this._diffZones.set(editor, [zoneId]); +}); + + + + + + + + +// // green decoration and gutter decoration +// const greenDecoration: IModelDeltaDecoration[] = [{ +// range: modifiedRange, +// options: { +// className: 'line-insert', // .monaco-editor .line-insert +// description: 'line-insert', +// isWholeLine: true, +// minimap: { +// color: { id: 'minimapGutter.addedBackground' }, +// position: 2 +// }, +// overviewRuler: { +// color: { id: 'editorOverviewRuler.addedForeground' }, +// position: 7 +// } +// } +// }]; + +// this._diffDecorations.set(editor, editor.deltaDecorations([], greenDecoration)); + + + + + +// override dispose(): void { +// super.dispose(); +// this._diffDecorations.clear(); +// this._diffZones.clear(); +// } + + + + + + +public removeAllDiffs(editor: ICodeEditor): void { + const decorationIds = this._diffDecorations.get(editor) || []; + editor.deltaDecorations(decorationIds, []); + this._diffDecorations.delete(editor); + + editor.changeViewZones(accessor => { + const zoneIds = this._diffZones.get(editor) || []; + zoneIds.forEach(id => accessor.removeZone(id)); + }); + this._diffZones.delete(editor); +} + + + + + + +// _ means computed / temporary +type DiffArea = { + diffareaid: string, + startLine: number, + endLine: number, + + _diffIds: string[], + _sweepIdx: number | null, +} + + +export type Diff = { + diffid: string, + diffareaid: string, // the diff area this diff belongs to, "computed" + type: 'edit' | 'insertion' | 'deletion'; + originalCode: string; + startLine: number; + endLine: number; + + startCol: number; + endCol: number; + + _zone: IViewZone | null, + _decorationId: string | null, +} + + + + +type HistorySnapshot = { + diffAreaOfId: Map, + diffOfId: Map, +} & + ({ + type: 'ctrl+k', + ctrlKText: string + } | { + type: 'ctrl+l', + }) + + + + +export interface IInlineDiffsService { + readonly _serviceBrand: undefined; +} + +export const IInlineDiffsService = createDecorator('inlineDiffsService'); + +class InlineDiffsService extends Disposable implements IInlineDiffsService { + _serviceBrand: undefined; + + diffAreaOfId: Map = new Map(); + diffOfId: Map = new Map(); + + + streamingState: { + type: 'streaming'; + editGroup: UndoRedoGroup; + } | { type: 'idle' } + = { type: 'idle' } + + + private readonly _onDidFinishStreaming = new Emitter(); + + + constructor( + // @IHistoryService private readonly _historyService: IHistoryService, // history service is the history of pressing alt left/right + @IInlineDiffsService private readonly _inlineDiff: IInlineDiffsService, + @ICodeEditorService private readonly _editorService: ICodeEditorService, + @IUndoRedoService private readonly _undoRedoService: IUndoRedoService, // undoRedo service is the history of pressing ctrl+z + @IBulkEditService private readonly _bulkEditService: IBulkEditService, + ) { + super(); + } + + + startStreaming() { + const editor = this._editorService.getActiveCodeEditor() + if (!editor) return + + const model = editor.getModel() + if (!model) return + + // all changes made by us when streaming should be a part of the group so we can undo them all together + this.streamingState = { + type: 'streaming', + editGroup: new UndoRedoGroup(), + } + + const beforeSnapshot: HistorySnapshot = { + diffAreaOfId: new Map(this.diffAreaOfId), + diffOfId: new Map(this.diffOfId), + type: 'ctrl+l', + } + + let afterSnapshot: HistorySnapshot | null = null + this._register( + this._onDidFinishStreaming.event(() => { + if (afterSnapshot !== null) return + afterSnapshot = { + diffAreaOfId: new Map(this.diffAreaOfId), + diffOfId: new Map(this.diffOfId), + type: 'ctrl+l', + } + }) + ) + + const elt: IUndoRedoElement = { + type: UndoRedoElementType.Resource, + resource: model.uri, + label: 'Add Diffs', + code: 'undoredo.inlineDiffs', + // called when undoing this state + undo: () => { + // when the user undoes this element, revert to oldSnapshot + this.diffAreaOfId = new Map(beforeSnapshot.diffAreaOfId) + this.diffOfId = new Map(beforeSnapshot.diffOfId) + // TODO refresh diffs + }, + // called when restoring this state + redo: () => { + if (afterSnapshot === null) return + this.diffAreaOfId = new Map(afterSnapshot.diffAreaOfId) + this.diffOfId = new Map(afterSnapshot.diffOfId) + } + } + + this._undoRedoService.pushElement(elt, this.streamingState.editGroup) + + + + // ---------- START ---------- + editor.updateOptions({ readOnly: true }) + + + // ---------- WHEN DONE ---------- + editor.updateOptions({ readOnly: false }) + + } + + + + + private _streamChange(editor: ICodeEditor, edit: WorkspaceEdit) { + + // count all changes towards the group + this._bulkEditService.apply(edit, { undoRedoGroupId: this._streamingState.editGroup.id, }) + + } + + + + endStreaming() { + + this._onDidFinishStreaming.fire() + + } + + + + +} + +registerSingleton(IInlineDiffsService, InlineDiffsService, InstantiationType.Eager); + + + + + + +