progress adding diffs

This commit is contained in:
Andrew Pareles 2024-11-12 03:28:53 -08:00
parent 4de3f1c7ef
commit a5912ba538
8 changed files with 932 additions and 482 deletions

36
package-lock.json generated
View file

@ -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",

View file

@ -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",

View file

@ -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;

View file

@ -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<void> = new vscode.EventEmitter<void>(); // signals a UI refresh on .fire() events
public readonly onDidChangeCodeLenses: vscode.Event<void> = this._onDidChangeCodeLenses.event;
// used internally by vscode
public provideCodeLenses(document: vscode.TextDocument, token: vscode.CancellationToken): vscode.ProviderResult<vscode.CodeLens[]> {
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<DiffArea, 'diffareaid'>, 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<void>((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)
}

View file

@ -0,0 +1,117 @@
import { Range } from 'vscode';
import { diffLines, Change } from 'diff';
import { Diff } from './registerInlineDiffs';
type BaseDiff = Omit<Diff, '_zone' | '_decorationId' | 'diffid' | 'diffareaid'>
// 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
}

View file

@ -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<void> = new vscode.EventEmitter<void>(); // signals a UI refresh on .fire() events
// public readonly onDidChangeCodeLenses: vscode.Event<void> = this._onDidChangeCodeLenses.event;
// // used internally by vscode
// public provideCodeLenses(document: vscode.TextDocument, token: vscode.CancellationToken): vscode.ProviderResult<vscode.CodeLens[]> {
// 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<DiffArea, 'diffareaid'>, 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<void>((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)
// }

View file

@ -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<DiffArea, 'diffareaid'> = {
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<DiffArea, 'diffareaid'> = {
// 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<IInlineDiffService>('inlineDif
class InlineDiffService extends Disposable implements IInlineDiffService {
private readonly _diffDecorations = new Map<ICodeEditor, string[]>();
private readonly _diffZones = new Map<ICodeEditor, string[]>();
_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

View file

@ -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<string, DiffArea>,
diffOfId: Map<string, Diff>,
} &
({
type: 'ctrl+k',
ctrlKText: string
} | {
type: 'ctrl+l',
})
export interface IInlineDiffsService {
readonly _serviceBrand: undefined;
}
export const IInlineDiffsService = createDecorator<IInlineDiffsService>('inlineDiffsService');
class InlineDiffsService extends Disposable implements IInlineDiffsService {
_serviceBrand: undefined;
diffAreaOfId: Map<string, DiffArea> = new Map();
diffOfId: Map<string, Diff> = new Map();
streamingState: {
type: 'streaming';
editGroup: UndoRedoGroup;
} | { type: 'idle' }
= { type: 'idle' }
private readonly _onDidFinishStreaming = new Emitter<void>();
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);