From 2e92da500f4cd13951f1d46ed1d3cc6df9cab532 Mon Sep 17 00:00:00 2001 From: Andrew Date: Mon, 28 Oct 2024 03:19:40 -0700 Subject: [PATCH] lines sweep works! Need to polish it. --- extensions/void/src/DiffProvider.ts | 158 +++++++++-- extensions/void/src/common/ctrlL.ts | 243 ++++++---------- extensions/void/src/common/sendLLMMessage.ts | 52 ++-- extensions/void/src/common/shared_types.ts | 14 +- extensions/void/src/extension.ts | 43 +-- extensions/void/src/findDiffs.ts | 268 +++++++----------- extensions/void/src/sidebar/SidebarChat.tsx | 4 +- .../src/sidebar/markdown/MarkdownRender.tsx | 6 +- 8 files changed, 373 insertions(+), 415 deletions(-) diff --git a/extensions/void/src/DiffProvider.ts b/extensions/void/src/DiffProvider.ts index 0218984d..9191392e 100644 --- a/extensions/void/src/DiffProvider.ts +++ b/extensions/void/src/DiffProvider.ts @@ -10,6 +10,14 @@ const greenDecoration = vscode.window.createTextEditorDecorationType({ backgroundColor: 'rgba(0 255 51 / 0.2)', isWholeLine: false, // after: { contentText: ' [original]', color: 'rgba(0 255 60 / 0.5)' } // hoverMessage: originalText // this applies to hovering over after:... }) +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 { @@ -20,7 +28,6 @@ export class DiffProvider implements vscode.CodeLensProvider { private _diffareaidPool = 0 private _diffidPool = 0 - private _weAreEditing: boolean = false // used internally by vscode private _onDidChangeCodeLenses: vscode.EventEmitter = new vscode.EventEmitter(); // signals a UI refresh on .fire() events @@ -43,23 +50,21 @@ export class DiffProvider implements vscode.CodeLensProvider { const editor = vscode.window.activeTextEditor if (!editor) return - if (this._weAreEditing) 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.updateDiffAreasBasedOnChanges(docUriStr, changes, 'currentFile') + this.refreshDiffAreasModel(docUriStr, changes, 'currentFile') // refresh the diffAreas - this.refreshStyles(docUriStr) + this.refreshStylesAndDiffs(docUriStr) }) } - // used by us only - public createDiffArea(uri: vscode.Uri, diffArea: Omit, originalFile: string) { + public createDiffArea(uri: vscode.Uri, partialDiffArea: Omit, originalFile: string) { const uriStr = uri.toString() @@ -70,21 +75,26 @@ export class DiffProvider implements vscode.CodeLensProvider { // remove all diffAreas that the new `diffArea` is overlapping with this._diffAreasOfDocument[uriStr] = this._diffAreasOfDocument[uriStr].filter(da => { - const noOverlap = da.startLine > diffArea.endLine || da.endLine < diffArea.startLine + const noOverlap = da.startLine > partialDiffArea.endLine || da.endLine < partialDiffArea.startLine if (!noOverlap) return false return true }) // add `diffArea` to storage - this._diffAreasOfDocument[uriStr].push({ - ...diffArea, + const diffArea = { + ...partialDiffArea, diffareaid: this._diffareaidPool - }) + } + this._diffAreasOfDocument[uriStr].push(diffArea) this._diffareaidPool += 1 + + return diffArea } // used by us only - public updateDiffAreasBasedOnChanges(docUriStr: string, changes: { text: string, startLine: number, endLine: number }[], changesTo: 'originalFile' | 'currentFile') { + // 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 refreshDiffAreasModel(docUriStr: string, changes: { text: string, startLine: number, endLine: number }[], changesTo: 'originalFile' | 'currentFile') { const diffAreas = this._diffAreasOfDocument[docUriStr] || [] @@ -141,7 +151,8 @@ export class DiffProvider implements vscode.CodeLensProvider { // used by us only - public refreshStyles(docUriStr: string) { + // 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) { @@ -172,19 +183,19 @@ export class DiffProvider implements vscode.CodeLensProvider { // add the diffs to `this._diffsOfDocument[docUriStr]` this.createDiffs(editor.document.uri, diffs, diffArea) - // print diffs - console.log('!ORIGINAL FILE:', JSON.stringify(originalFile)) - console.log('!NEW FILE :', JSON.stringify(editor.document.getText().replace(/\r\n/g, '\n'))) - console.log('!AREA originalCode:', JSON.stringify(originalCode)) - console.log('!AREA currentCode :', JSON.stringify(currentCode)) - for (const diff of this._diffsOfDocument[docUriStr]) { - console.log('------------') - console.log('originalCode:', JSON.stringify(diff.originalCode)) - console.log('currentCode:', JSON.stringify(diff.code)) - console.log('originalRange:', diff.originalRange.start.line, diff.originalRange.end.line,) - console.log('currentRange:', diff.range.start.line, diff.range.end.line,) - } - console.log('DiffRepr: ', diffs.map(diff => diff.repr).join('\n')) + + // // print diffs + // console.log('!ORIGINAL FILE:', JSON.stringify(originalFile)) + // console.log('!NEW FILE :', JSON.stringify(editor.document.getText().replace(/\r\n/g, '\n'))) + // console.log('!AREA originalCode:', JSON.stringify(originalCode)) + // console.log('!AREA currentCode :', JSON.stringify(currentCode)) + // for (const diff of this._diffsOfDocument[docUriStr]) { + // console.log('------------') + // console.log('originalCode:', JSON.stringify(diff.originalCode)) + // console.log('currentCode:', JSON.stringify(diff.code)) + // console.log('originalRange:', diff.originalRange.start.line, diff.originalRange.end.line,) + // console.log('currentRange:', diff.range.start.line, diff.range.end.line,) + // } } @@ -197,6 +208,30 @@ export class DiffProvider implements vscode.CodeLensProvider { ) ); + + // 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) + }) + ) + ) + // TODO update red highlighting // this._diffsOfDocument[docUriStr].map(diff => diff.deletedCode) @@ -267,7 +302,7 @@ export class DiffProvider implements vscode.CodeLensProvider { this._originalFileOfDocument[docUriStr] = newOriginalLines.join('\n'); // Update diff areas based on the change - this.updateDiffAreasBasedOnChanges(docUriStr, [{ + this.refreshDiffAreasModel(docUriStr, [{ text: changedLines.join('\n'), startLine: diff.originalRange.start.line, endLine: diff.originalRange.end.line @@ -290,7 +325,7 @@ export class DiffProvider implements vscode.CodeLensProvider { this._diffAreasOfDocument[docUriStr].splice(index, 1) } - this.refreshStyles(docUriStr) + this.refreshStylesAndDiffs(docUriStr) } // called on void.rejectDiff @@ -310,11 +345,10 @@ export class DiffProvider implements vscode.CodeLensProvider { 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) - this._weAreEditing = true await vscode.workspace.applyEdit(workspaceEdit) - this._weAreEditing = false // Check if diffArea should be removed const originalFile = this._originalFileOfDocument[docUriStr] @@ -336,8 +370,70 @@ export class DiffProvider implements vscode.CodeLensProvider { this._diffAreasOfDocument[docUriStr].splice(index, 1) } - this.refreshStyles(docUriStr) + this.refreshStylesAndDiffs(docUriStr) } + + + + + // used by us only + public async updateStream(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) + } + } @@ -458,4 +554,4 @@ export class DiffProvider { } -*/ \ No newline at end of file +*/ diff --git a/extensions/void/src/common/ctrlL.ts b/extensions/void/src/common/ctrlL.ts index 393ba6b2..4056c304 100644 --- a/extensions/void/src/common/ctrlL.ts +++ b/extensions/void/src/common/ctrlL.ts @@ -1,46 +1,22 @@ import * as vscode from 'vscode'; -import { OnFinalMessage, OnText, sendLLMMessage, SetAbort } from "./sendLLMMessage" +import { AbortRef, OnFinalMessage, OnText, sendLLMMessage } from "./sendLLMMessage" import { VoidConfig } from '../sidebar/contextForConfig'; import { findDiffs } from '../findDiffs'; import { searchDiffChunkInstructions, writeFileWithDiffInstructions } from './systemPrompts'; import { throttle } from 'lodash'; import { readFileContentOfUri } from './readFileContentOfUri'; +import { DiffProvider } from '../DiffProvider'; +import { DiffArea } from './shared_types'; -type Res = ((value: T) => void) - -const THRTOTLE_TIME = 100 // minimum time between edits const LINES_PER_CHUNK = 20 // number of lines to search at a time -const applyCtrlLChangesToFile = throttle( - ({ fileUri, newCurrentLine, oldCurrentLine, fullCompletedStr, oldFileStr, debug }: { fileUri: vscode.Uri, newCurrentLine: number, oldCurrentLine: number, fullCompletedStr: string, oldFileStr: string, debug?: string }) => { +// const THRTOTLE_TIME = 100 // minimum time between edits +// throttle( +// THRTOTLE_TIME, { trailing: true } +// ) - console.log('DEBUG: ', debug) - console.log('oldNext: ', oldCurrentLine) - console.log('newNext: ', newCurrentLine) - console.log('WRITE_TO_FILE1: ', fullCompletedStr.split('\n').slice(0, newCurrentLine + 1).join('\n')) - console.log('WRITE_TO_FILE2: ', oldFileStr.split('\n').slice(oldCurrentLine + 1).join('\n')) - - // write the change to the file - const WRITE_TO_FILE = ( - fullCompletedStr.split('\n').slice(0, newCurrentLine + 1).join('\n') // newFile[:newCurrentLine+1] - + oldFileStr.split('\n').slice(oldCurrentLine + 1).join('\n') // oldFile[oldCurrentLine+1:] - ) - const workspaceEdit = new vscode.WorkspaceEdit() - workspaceEdit.replace(fileUri, new vscode.Range(0, 0, Number.MAX_SAFE_INTEGER, 0), WRITE_TO_FILE) - vscode.workspace.applyEdit(workspaceEdit) - - // highlight the `newCurrentLine` in white - // highlight the remaining part of the file in gray - - }, - THRTOTLE_TIME, { trailing: true } -) - - -// `next` is the line after the completed text -// `oldNext` is the same line but in the original file -type CompetedReturn = { isFinished: true, next?: undefined, oldNext?: undefined, } | { isFinished?: undefined, next: number, oldNext: number, } -const generateFileUsingDiffUntilMatchup = ({ fileUri, oldFileStr, completedStr, oldNext, next, diffStr, voidConfig, setAbort }: { fileUri: vscode.Uri, oldFileStr: string, completedStr: string, oldNext: number, next: number, diffStr: string, voidConfig: VoidConfig, setAbort: SetAbort }) => { +type CompetedReturn = { isFinished: true, } | { isFinished?: undefined, } +const streamChunk = ({ diffProvider, docUri, oldFileStr, completedStr, diffRepr, diffArea, voidConfig, abortRef }: { diffProvider: DiffProvider, docUri: vscode.Uri, oldFileStr: string, completedStr: string, diffRepr: string, voidConfig: VoidConfig, diffArea: DiffArea, abortRef: AbortRef }) => { const NUM_MATCHUP_TOKENS = 20 @@ -51,7 +27,7 @@ ${oldFileStr} DIFF \`\`\` -${diffStr} +${diffRepr} \`\`\` INSTRUCTIONS @@ -63,88 +39,61 @@ ${completedStr} \`\`\` ` // create a promise that can be awaited - let res: Res = () => { } - const promise = new Promise((resolve, reject) => { res = resolve }) + return new Promise((resolve, reject) => { - // get the abort method - let _abort = () => { } - let did_abort = false + let isAnyChangeSoFar = false - // make LLM complete the file to include the diff - sendLLMMessage({ - messages: [{ role: 'system', content: writeFileWithDiffInstructions, }, { role: 'user', content: promptContent, }], - onText: (tokenStr, deltaStr) => { + // make LLM complete the file to include the diff + sendLLMMessage({ + messages: [{ role: 'system', content: writeFileWithDiffInstructions, }, { role: 'user', content: promptContent, }], + onText: (newText, fullText) => { + const fullCompletedStr = completedStr + fullText - if (did_abort) return; + diffProvider.updateStream(docUri.toString(), diffArea, fullCompletedStr) - const fullCompletedStr = completedStr + deltaStr - - // diff `originalFileStr` and `newFileStr` - const diffs = findDiffs(oldFileStr, fullCompletedStr) - const lastDiff = diffs[diffs.length - 1] - const oldLineAfterLastDiff = lastDiff.originalRange.end.line + 1 - const newLineAfterLastDiff = lastDiff.range.end.line + 1 - - // check if we've generated a diff - const didGenerateDiff = newLineAfterLastDiff > next - - // get the line we are currently generating `newCurrentLine`; make sure it never goes past the last diff we've generated - // - if `deltaStr` contains a diff, then _next = newLineAfterLastDiff - 1 - // - if it does not contain a diff, then _next = next + deltaStr.split('\n').length - 1 - const newCurrentLine = didGenerateDiff ? newLineAfterLastDiff - 1 : next + deltaStr.split('\n').length - 1 - const oldCurrentLine = didGenerateDiff ? oldLineAfterLastDiff - 1 : oldNext + (newCurrentLine - next) - - // 1. Apply the changes and modify highlighting - - applyCtrlLChangesToFile({ fileUri, newCurrentLine, oldCurrentLine, fullCompletedStr, oldFileStr }) - - // 2. Check for early stopping - // the conditions for early stopping are: - // - we have generated a diff - // - there is matchup with the original file after the diff - const isMatchupAfterDiff = fullCompletedStr.split('\n').slice(newLineAfterLastDiff).join('\n').length > NUM_MATCHUP_TOKENS - if (didGenerateDiff && isMatchupAfterDiff) { - - // resolve the promise - res({ next: newCurrentLine + 1, oldNext: oldCurrentLine + 1, }); - - // abort the LLM call - _abort() - did_abort = true - - } else { - - } + // if there was any change from the original file + if (!oldFileStr.includes(fullCompletedStr)) { + isAnyChangeSoFar = true + } + const isRecentMatchup = false + // the final NUM_MATCHUP_TOKENS characters of fullCompletedStr are the same as the final NUM_MATCHUP_TOKENS characters of the last item in the diffs of oldFileStr that had 0 changes - }, - onFinalMessage: (deltaStr) => { + if (isAnyChangeSoFar && isRecentMatchup) { + diffProvider.updateStream(docUri.toString(), diffArea, fullCompletedStr) - const newCompletedStr = completedStr + deltaStr + // TODO resolve the promise + // resolve({ speculativeIndex: newCurrentLine + 1 }); - applyCtrlLChangesToFile({ fileUri, newCurrentLine: Number.MAX_SAFE_INTEGER, oldCurrentLine: Number.MAX_SAFE_INTEGER, fullCompletedStr: newCompletedStr, oldFileStr, debug: 'FINAL' }) + // abort the LLM call + abortRef.current?.() - res({ isFinished: true }); - }, - onError: (e) => { - res({ isFinished: true }); - console.error('Error rewriting file with diff', e); - }, - voidConfig, - setAbort: (a) => { setAbort(a); _abort = a; }, + } + + }, + + onFinalMessage: (fullText) => { + const newCompletedStr = completedStr + fullText + diffProvider.updateStream(docUri.toString(), diffArea, newCompletedStr) + resolve({ isFinished: true }); + }, + onError: (e) => { + resolve({ isFinished: true }); + console.error('Error rewriting file with diff', e); + }, + voidConfig, + abortRef, + }) }) - - return promise - } -const shouldApplyDiffFn = ({ diffStr, fileStr, speculationStr, voidConfig, setAbort }: { diffStr: string, fileStr: string, speculationStr: string, voidConfig: VoidConfig, setAbort: SetAbort }) => { +const shouldApplyDiff = ({ diffRepr, oldFileStr: fileStr, speculationStr, voidConfig, abortRef }: { diffRepr: string, oldFileStr: string, speculationStr: string, voidConfig: VoidConfig, abortRef: AbortRef }) => { const promptContent = `DIFF \`\`\` -${diffStr} +${diffRepr} \`\`\` FILES @@ -161,90 +110,76 @@ Return \`true\` if ANY part of the chunk should be modified, and \`false\` if it ` // create new promise - let res: Res = () => { } - const promise = new Promise((resolve, reject) => { res = resolve }) + return new Promise((resolve, reject) => { + // send message to LLM + sendLLMMessage({ + messages: [{ role: 'system', content: searchDiffChunkInstructions, }, { role: 'user', content: promptContent, }], + onFinalMessage: (finalMessage) => { - // send message to LLM - sendLLMMessage({ - messages: [{ role: 'system', content: searchDiffChunkInstructions, }, { role: 'user', content: promptContent, }], - onFinalMessage: (finalMessage) => { + const containsTrue = finalMessage + .slice(-10) // check for `true` in last 10 characters + .toLowerCase() + .includes('true') - const containsTrue = finalMessage - .slice(-10) // check for `true` in last 10 characters - .toLowerCase() - .includes('true') + resolve(containsTrue) + }, + onError: (e) => { + resolve(false); + console.error('Error in shouldApplyDiff: ', e) + }, + onText: () => { }, + voidConfig, + abortRef, + }) - res(containsTrue) - }, - onError: (e) => { - res(false); - console.error('Error in shouldApplyDiff: ', e) - }, - onText: () => { }, - voidConfig, - setAbort, }) - // return the promise - return promise - } // lazily applies the diff to the file // we chunk the text in the file, and ask an LLM whether it should edit each chunk -const applyDiffLazily = async ({ fileUri, oldFileStr, diffStr, voidConfig, setAbort }: { fileUri: vscode.Uri, oldFileStr: string, diffStr: string, voidConfig: VoidConfig, setAbort: SetAbort }) => { +const applyDiffLazily = async ({ docUri, oldFileStr, voidConfig, abortRef, diffRepr, diffProvider, diffArea }: { docUri: vscode.Uri, oldFileStr: string, diffRepr: string, voidConfig: VoidConfig, diffProvider: DiffProvider, diffArea: DiffArea, abortRef: AbortRef }) => { // stateful variables - let next = 0 - let oldNext = 0 + let speculativeIndex = 0 + let writtenTextSoFar: string[] = [] - while (next < oldFileStr.split('\n').length) { + while (speculativeIndex < oldFileStr.split('\n').length) { - console.log('next line: ', next) - - // get the chunk - const chunkStr = oldFileStr.split('\n').slice(next, next + LINES_PER_CHUNK).join('\n') + const chunkStr = oldFileStr.split('\n').slice(speculativeIndex, speculativeIndex + LINES_PER_CHUNK).join('\n') // ask LLM if we should apply the diff to the chunk - const __start = new Date().getTime() - - let shouldApplyDiff = await shouldApplyDiffFn({ fileStr: oldFileStr, speculationStr: chunkStr, diffStr, voidConfig, setAbort }) - - const __end = new Date().getTime() - - if (!shouldApplyDiff) { // should not change the chunk - console.log('KEEP CHUNK time: ', __end - __start) - - next += LINES_PER_CHUNK - oldNext += LINES_PER_CHUNK + const START = new Date().getTime() + let shouldApplyDiff_ = await shouldApplyDiff({ oldFileStr, speculationStr: chunkStr, diffRepr, voidConfig, abortRef }) + const END = new Date().getTime() + // if should not change the chunk + if (!shouldApplyDiff_) { + console.log('KEEP CHUNK time: ', END - START) + speculativeIndex += LINES_PER_CHUNK + writtenTextSoFar.push(chunkStr) + diffProvider.updateStream(docUri.toString(), diffArea, writtenTextSoFar.join('\n')) continue; } - // ask LLM to rewrite file with diff (if there is significant matchup with the original file, we stop rewriting) - // make vscode read uri = 'asdasd' + const START2 = new Date().getTime() + const completedStr = (await readFileContentOfUri(docUri)).split('\n').slice(0, speculativeIndex).join('\n'); + const result = await streamChunk({ diffProvider, docUri, oldFileStr, completedStr, diffRepr, voidConfig, diffArea, abortRef, }) + const END2 = new Date().getTime() - const ___start = new Date().getTime() - - - const completedStr = (await readFileContentOfUri(fileUri)).split('\n').slice(0, next).join('\n'); - const result = await generateFileUsingDiffUntilMatchup({ fileUri, oldFileStr, completedStr, oldNext, next, diffStr, voidConfig, setAbort, }) - - const ___end = new Date().getTime() - - console.log('EDIT CHUNK time: ', ___end - ___start); + console.log('EDIT CHUNK time: ', END2 - START2); // if we are finished, stop the loop if (result.isFinished) { break; } - next = result.next - oldNext = result.oldNext + // TODO + // speculativeIndex = result.speculativeIndex } @@ -253,4 +188,4 @@ const applyDiffLazily = async ({ fileUri, oldFileStr, diffStr, voidConfig, setAb -export { applyDiffLazily } \ No newline at end of file +export { applyDiffLazily } diff --git a/extensions/void/src/common/sendLLMMessage.ts b/extensions/void/src/common/sendLLMMessage.ts index 7111a6a2..3b9b15d4 100644 --- a/extensions/void/src/common/sendLLMMessage.ts +++ b/extensions/void/src/common/sendLLMMessage.ts @@ -3,15 +3,12 @@ import OpenAI from 'openai'; import { Ollama } from 'ollama/browser' import { VoidConfig } from '../sidebar/contextForConfig'; - - +export type AbortRef = { current: (() => void) | null } export type OnText = (newText: string, fullText: string) => void export type OnFinalMessage = (input: string) => void -export type SetAbort = (abort: () => void) => void - export type LLMMessageAnthropic = { role: 'user' | 'assistant', content: string, @@ -28,25 +25,23 @@ type SendLLMMessageFnTypeInternal = (params: { onFinalMessage: OnFinalMessage, onError: (error: string) => void, voidConfig: VoidConfig, - setAbort: SetAbort, + abortRef: AbortRef, }) => void type SendLLMMessageFnTypeExternal = (params: { messages: LLMMessage[], onText: OnText, - onFinalMessage: (input: string) => void, + onFinalMessage: (fullText: string) => void, onError: (error: string) => void, voidConfig: VoidConfig | null, - setAbort: SetAbort, - -}) - => void + abortRef: AbortRef, +}) => void // Anthropic -const sendAnthropicMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinalMessage, onError, voidConfig, setAbort }) => { +const sendAnthropicMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinalMessage, onError, voidConfig }) => { const anthropic = new Anthropic({ apiKey: voidConfig.anthropic.apikey, dangerouslyAllowBrowser: true }); // defaults to process.env["ANTHROPIC_API_KEY"] @@ -97,21 +92,21 @@ const sendAnthropicMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFi did_abort = true stream.controller.abort() // TODO need to test this to make sure it works, it might throw an error } - setAbort(abort) + return { abort } }; // OpenAI, OpenRouter, OpenAICompatible -const sendOpenAIMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinalMessage, onError, voidConfig, setAbort }) => { +const sendOpenAIMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinalMessage, onError, voidConfig, abortRef }) => { let didAbort = false let fullText = '' // if abort is called, onFinalMessage is NOT called, and no later onTexts are called either - let abort: () => void = () => { + abortRef.current = () => { didAbort = true; }; @@ -144,7 +139,7 @@ const sendOpenAIMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinal openai.chat.completions .create(options) .then(async response => { - abort = () => { + abortRef.current = () => { // response.controller.abort() didAbort = true; } @@ -172,18 +167,17 @@ const sendOpenAIMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinal } }) - setAbort(abort) }; // Ollama -export const sendOllamaMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinalMessage, onError, voidConfig, setAbort }) => { +export const sendOllamaMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinalMessage, onError, voidConfig, abortRef }) => { let didAbort = false let fullText = "" // if abort is called, onFinalMessage is NOT called, and no later onTexts are called either - let abort = () => { + abortRef.current = () => { didAbort = true; }; @@ -196,8 +190,8 @@ export const sendOllamaMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, options: { num_predict: parseInt(voidConfig.default.maxTokens) } // this is max_tokens }) .then(async stream => { - abort = () => { - // ollama.abort() + abortRef.current = () => { + // stream.abort() didAbort = true } // iterate through the stream @@ -215,7 +209,6 @@ export const sendOllamaMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onError(error) }) - setAbort(abort); }; @@ -224,13 +217,15 @@ export const sendOllamaMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, // https://docs.greptile.com/api-reference/query // https://docs.greptile.com/quickstart#sample-response-streamed -const sendGreptileMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinalMessage, onError, voidConfig, setAbort }) => { +const sendGreptileMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinalMessage, onError, voidConfig, abortRef }) => { let didAbort = false let fullText = '' // if abort is called, onFinalMessage is NOT called, and no later onTexts are called either - let abort: () => void = () => { didAbort = true } + abortRef.current = () => { + didAbort = true + } fetch('https://api.greptile.com/v2/query', { @@ -285,12 +280,11 @@ const sendGreptileMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFin onError(e) }); - setAbort(abort) } -export const sendLLMMessage: SendLLMMessageFnTypeExternal = ({ messages, onText, onFinalMessage, onError, voidConfig, setAbort }) => { +export const sendLLMMessage: SendLLMMessageFnTypeExternal = ({ messages, onText, onFinalMessage, onError, voidConfig, abortRef }) => { if (!voidConfig) return; // trim message content (Anthropic and other providers give an error if there is trailing whitespace) @@ -298,15 +292,15 @@ export const sendLLMMessage: SendLLMMessageFnTypeExternal = ({ messages, onText, switch (voidConfig.default.whichApi) { case 'anthropic': - return sendAnthropicMsg({ messages, onText, onFinalMessage, onError, voidConfig, setAbort }); + return sendAnthropicMsg({ messages, onText, onFinalMessage, onError, voidConfig, abortRef }); case 'openAI': case 'openRouter': case 'openAICompatible': - return sendOpenAIMsg({ messages, onText, onFinalMessage, onError, voidConfig, setAbort }); + return sendOpenAIMsg({ messages, onText, onFinalMessage, onError, voidConfig, abortRef }); case 'ollama': - return sendOllamaMsg({ messages, onText, onFinalMessage, onError, voidConfig, setAbort }); + return sendOllamaMsg({ messages, onText, onFinalMessage, onError, voidConfig, abortRef }); case 'greptile': - return sendGreptileMsg({ messages, onText, onFinalMessage, onError, voidConfig, setAbort }); + return sendGreptileMsg({ messages, onText, onFinalMessage, onError, voidConfig, abortRef }); default: onError(`Error: whichApi was ${voidConfig.default.whichApi}, which is not recognized!`) } diff --git a/extensions/void/src/common/shared_types.ts b/extensions/void/src/common/shared_types.ts index 10ad01dd..91cdd738 100644 --- a/extensions/void/src/common/shared_types.ts +++ b/extensions/void/src/common/shared_types.ts @@ -12,16 +12,20 @@ type File = { filepath: vscode.Uri, content: string } // an area that is currently being diffed type DiffArea = { diffareaid: number, - startLine: number, endLine: number, - originalStartLine: number, originalEndLine: number, + startLine: number, + endLine: number, + originalStartLine: number, + originalEndLine: number, + sweepIndex: number | null // null iff not sweeping } // the return type of diff creator type BaseDiff = { - repr: string; // representation of the diff in text + type: 'edit' | 'insertion' | 'deletion'; + // repr: string; // representation of the diff in text originalRange: vscode.Range; - range: vscode.Range; originalCode: string; + range: vscode.Range; code: string; } @@ -46,7 +50,7 @@ type MessageToSidebar = ( // sidebar -> editor type MessageFromSidebar = ( - | { type: 'applyChanges', code: string } // user clicks "apply" in the sidebar + | { type: 'applyChanges', diffRepr: string } // user clicks "apply" in the sidebar | { type: 'requestFiles', filepaths: vscode.Uri[] } | { type: 'getPartialVoidConfig' } | { type: 'persistPartialVoidConfig', partialVoidConfig: PartialVoidConfig } diff --git a/extensions/void/src/extension.ts b/extensions/void/src/extension.ts index c0f82a68..5bfac25c 100644 --- a/extensions/void/src/extension.ts +++ b/extensions/void/src/extension.ts @@ -6,6 +6,7 @@ import { v4 as uuidv4 } from 'uuid' import { applyDiffLazily } from './common/ctrlL'; import { getVoidConfig } from './sidebar/contextForConfig'; import { readFileContentOfUri } from './common/readFileContentOfUri'; +import { AbortRef } from './common/sendLLMMessage'; // this comes from vscode.proposed.editorInsets.d.ts declare module 'vscode' { @@ -91,15 +92,15 @@ export function activate(context: vscode.ExtensionContext) { ); // 3. Show an approve/reject codelens above each change - const displayChangesProvider = new DiffProvider(); - context.subscriptions.push(vscode.languages.registerCodeLensProvider('*', displayChangesProvider)); + const diffProvider = new DiffProvider(); + context.subscriptions.push(vscode.languages.registerCodeLensProvider('*', diffProvider)); // 4. Add approve/reject commands context.subscriptions.push(vscode.commands.registerCommand('void.acceptDiff', async (params) => { - displayChangesProvider.acceptDiff(params) + diffProvider.acceptDiff(params) })); context.subscriptions.push(vscode.commands.registerCommand('void.rejectDiff', async (params) => { - displayChangesProvider.rejectDiff(params) + diffProvider.rejectDiff(params) })); // 5. Receive messages from sidebar @@ -120,6 +121,8 @@ export function activate(context: vscode.ExtensionContext) { // Receive messages in the extension from the sidebar webview (messages are sent using `postMessage`) webview.onDidReceiveMessage(async (m: MessageFromSidebar) => { + const abortApplyRef: AbortRef = { current: null } + if (m.type === 'requestFiles') { // get contents of all file paths @@ -138,41 +141,21 @@ export function activate(context: vscode.ExtensionContext) { vscode.window.showInformationMessage('No active editor!') return } - - // create an area to show diffs - const diffArea: Omit = { + 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, } - displayChangesProvider.createDiffArea(editor.document.uri, diffArea, await readFileContentOfUri(editor.document.uri)) + const diffArea = diffProvider.createDiffArea(editor.document.uri, partialDiffArea, await readFileContentOfUri(editor.document.uri)) - - // write new code `m.code` to the document - // TODO update like this: - // this._weAreEditing = true - // await vscode.workspace.applyEdit(workspaceEdit) - // await vscode.workspace.save(docUri) - // this._weAreEditing = false - const fileUri = editor.document.uri - const fileStr = await readFileContentOfUri(fileUri) + const docUri = editor.document.uri + const fileStr = await readFileContentOfUri(docUri) const voidConfig = getVoidConfig(context.globalState.get('partialVoidConfig') ?? {}) - let abort = () => { } // TODO this is unused - - // apply the change - applyDiffLazily({ fileUri, oldFileStr: fileStr, diffStr: m.code, voidConfig, setAbort: (a) => { abort = a } }) - - // set the file equal to the change - // await editor.edit(editBuilder => { - // editBuilder.replace(new vscode.Range(diffArea.startLine, 0, diffArea.endLine, Number.MAX_SAFE_INTEGER), m.code); - // }); - - // rediff the changes based on the diffAreas - displayChangesProvider.refreshStyles(editor.document.uri.toString()) - + await applyDiffLazily({ docUri, oldFileStr: fileStr, diffRepr: m.diffRepr, voidConfig, diffProvider, diffArea, abortRef: abortApplyRef }) } else if (m.type === 'getPartialVoidConfig') { const partialVoidConfig = context.globalState.get('partialVoidConfig') ?? {} diff --git a/extensions/void/src/findDiffs.ts b/extensions/void/src/findDiffs.ts index a3d7f739..1953939d 100644 --- a/extensions/void/src/findDiffs.ts +++ b/extensions/void/src/findDiffs.ts @@ -1,183 +1,131 @@ -import * as vscode from 'vscode'; -// import { diffLines, Change } from 'diff'; -import { diff_match_patch } from 'diff-match-patch'; -import { diffLines } from 'diff'; +import { Range } from 'vscode'; +import { diffLines, Change } from 'diff'; import { BaseDiff } from './common/shared_types'; -// const diffLinesOld = (text1: string, text2: string) => { -// var dmp = new diff_match_patch(); -// var a = dmp.diff_linesToChars_(text1, text2); -// var lineText1 = a.chars1; -// var lineText2 = a.chars2; -// var lineArray = a.lineArray; -// var diffs = dmp.diff_main(lineText1, lineText2, false); -// dmp.diff_charsToLines_(diffs, lineArray); -// // dmp.diff_cleanupSemantic(diffs); -// return diffs; -// } - - -// // TODO use a better diff algorithm -// export const findDiffsOld = (oldText: string, newText: string): BaseDiff[] => { - -// const diffs = diffLinesOld(oldText, newText); - -// const blocks: BaseDiff[] = []; -// let reprBlock: string[] = []; -// let deletedBlock: string[] = []; -// let insertedBlock: string[] = []; -// let insertedLine = 0; -// let deletedLine = 0; -// let insertedStart = 0; -// let deletedStart = 0; - -// diffs.forEach(([operation, text]) => { - -// const lines = text.split('\n'); - -// switch (operation) { - -// // insertion -// case 1: -// if (reprBlock.length === 0) { reprBlock.push('@@@@'); } -// if (insertedBlock.length === 0) insertedStart = insertedLine; -// insertedLine += lines.length - 1; // Update only the line count for new text -// insertedBlock.push(text); -// reprBlock.push(lines.map(line => `+ ${line}`).join('\n')); -// break; - -// // deletion -// case -1: -// if (reprBlock.length === 0) { reprBlock.push('@@@@'); } -// if (deletedBlock.length === 0) deletedStart = deletedLine; -// deletedLine += lines.length - 1; // Update only the line count for old text -// deletedBlock.push(text); -// reprBlock.push(lines.map(line => `- ${line}`).join('\n')); -// break; - -// // no change -// case 0: -// // If we have a pending block, add it to the blocks array -// if (insertedBlock.length > 0 || deletedBlock.length > 0) { -// blocks.push({ -// code: reprBlock.join(''), -// deletedCode: deletedBlock.join(''), -// insertedCode: insertedBlock.join(''), -// deletedRange: new vscode.Range(deletedStart, 0, deletedLine, Number.MAX_SAFE_INTEGER), -// insertedRange: new vscode.Range(insertedStart, 0, insertedLine, Number.MAX_SAFE_INTEGER), -// }); -// } - -// // Reset the block variables -// reprBlock = []; -// deletedBlock = []; -// insertedBlock = []; - -// // Update line counts for unchanged text -// insertedLine += lines.length - 1; -// deletedLine += lines.length - 1; - -// break; -// } -// }); - -// // Add any remaining blocks after the loop ends -// if (insertedBlock.length > 0 || deletedBlock.length > 0) { -// blocks.push({ -// code: reprBlock.join(''), -// deletedCode: deletedBlock.join(''), -// insertedCode: insertedBlock.join(''), -// deletedRange: new vscode.Range(deletedStart, 0, deletedLine, Number.MAX_SAFE_INTEGER), -// insertedRange: new vscode.Range(insertedStart, 0, insertedLine, Number.MAX_SAFE_INTEGER), -// }); +// class Range { +// range: any; +// constructor(startLine, startCol, endLine, endCol) { +// const range = { +// startLine, +// startCol, +// endLine, +// endCol, +// }; +// this.range = range; // } - -// return blocks; -// }; +// } -export const findDiffs = (oldText: string, newText: string): BaseDiff[] => { - let diffs = diffLines(oldText, newText) - .map(diff => { - const operation = diff.added ? 1 : diff.removed ? -1 : 0; - const text = diff.value; - return [operation, text] as const; - }) +// 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, +} - const blocks: BaseDiff[] = []; - let reprBlock: string[] = []; - let deletedBlock: string[] = []; - let insertedBlock: string[] = []; - let newFileLine = 0; - let oldFileLine = 0; - let insertedStart = 0; - let deletedStart = 0; +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: '' }) // add a dummy so we flush any streaks we haven't yet at the very end (!line.added && !line.removed) - diffs.forEach(([operation, text]) => { + let oldFileLineNum: number = 0; + let newFileLineNum: number = 0; - const lines = text.split('\n'); + let streakStartInNewFile: number | undefined = undefined + let streakStartInOldFile: number | undefined = undefined - switch (operation) { + let oldStrLines = oldStr.split('\n') + let newStrLines = newStr.split('\n') - // insertion - case 1: - if (reprBlock.length === 0) { reprBlock.push('@@@@'); } - if (insertedBlock.length === 0) insertedStart = newFileLine; - newFileLine += lines.length - 1; // update the line count for new text - insertedBlock.push(text); - reprBlock.push(lines.map(line => `+ ${line}`).join('\n')); - break; + const replacements: BaseDiff[] = [] + for (let line of lineByLineChanges) { - // deletion - case -1: - if (reprBlock.length === 0) { reprBlock.push('@@@@'); } - if (deletedBlock.length === 0) deletedStart = oldFileLine; - oldFileLine += lines.length - 1; // update the line count for old text - deletedBlock.push(text); - reprBlock.push(lines.map(line => `- ${line}`).join('\n')); - break; + // no change on this line + if (!line.added && !line.removed) { - // no change - case 0: - // add pending block to the blocks array - if (insertedBlock.length > 0 || deletedBlock.length > 0) { - blocks.push({ - repr: reprBlock.join(''), - originalCode: deletedBlock.join(''), - code: insertedBlock.join(''), - originalRange: new vscode.Range(deletedStart, 0, oldFileLine, Number.MAX_SAFE_INTEGER), - range: new vscode.Range(insertedStart, 0, newFileLine, Number.MAX_SAFE_INTEGER), - }); + // 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' } - // update variables - reprBlock = []; - deletedBlock = []; - insertedBlock = []; - deletedStart += lines.length - 1; - insertedStart += lines.length - 1; - newFileLine += lines.length - 1; - oldFileLine += lines.length - 1; + // INSERTION + else if (originalEndLine === originalStartLine - 1) { + type = 'insertion' + originalEndLine = originalStartLine + originalStartCol = 0 + originalEndCol = 0 + } - break; + const replacement: BaseDiff = { + type, + range: new Range(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; } - }); - // Add any remaining blocks after the loop ends - if (insertedBlock.length > 0 || deletedBlock.length > 0) { - blocks.push({ - repr: reprBlock.join(''), - originalCode: deletedBlock.join(''), - code: insertedBlock.join(''), - originalRange: new vscode.Range(deletedStart, 0, oldFileLine, Number.MAX_SAFE_INTEGER), - range: new vscode.Range(insertedStart, 0, newFileLine, Number.MAX_SAFE_INTEGER), - }); - } + // 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 + } - return blocks; -}; + // 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/extensions/void/src/sidebar/SidebarChat.tsx b/extensions/void/src/sidebar/SidebarChat.tsx index f2271c60..a6fc1d55 100644 --- a/extensions/void/src/sidebar/SidebarChat.tsx +++ b/extensions/void/src/sidebar/SidebarChat.tsx @@ -263,10 +263,8 @@ export const SidebarChat = ({ chatInputRef }: { chatInputRef: React.RefObject { - abortFnRef.current = abort - }, voidConfig, + abortRef: abortFnRef, }) diff --git a/extensions/void/src/sidebar/markdown/MarkdownRender.tsx b/extensions/void/src/sidebar/markdown/MarkdownRender.tsx index bc02ebf4..a44fd48c 100644 --- a/extensions/void/src/sidebar/markdown/MarkdownRender.tsx +++ b/extensions/void/src/sidebar/markdown/MarkdownRender.tsx @@ -12,7 +12,7 @@ enum CopyButtonState { const COPY_FEEDBACK_TIMEOUT = 1000 // amount of time to say 'Copied!' -const CodeButtonsOnHover = ({ text }: { text: string }) => { +const CodeButtonsOnHover = ({ diffRepr: text }: { diffRepr: string }) => { const [copyButtonState, setCopyButtonState] = useState(CopyButtonState.Copy) useEffect(() => { @@ -44,7 +44,7 @@ const CodeButtonsOnHover = ({ text }: { text: string }) => {