From f4a407d80f87f638164d1ecbe163f3f4c5397471 Mon Sep 17 00:00:00 2001 From: Mathew P Date: Thu, 3 Oct 2024 00:33:17 -0700 Subject: [PATCH 01/10] Refactor and prepare to create `addNewChanges` --- extensions/void/package.json | 4 +- ...sProvider.ts => DisplayChangesProvider.ts} | 42 +++++++++++-------- extensions/void/src/common/sendLLMMessage.ts | 9 +++- extensions/void/src/extension.ts | 30 +++++++------ extensions/void/src/getDiffedLines.ts | 10 ++--- extensions/void/src/shared_types.ts | 2 +- .../void/src/sidebar/MarkdownRender.tsx | 2 +- extensions/void/src/sidebar/Sidebar.tsx | 17 ++++++-- extensions/void/src/sidebar/getVscodeApi.ts | 2 +- 9 files changed, 74 insertions(+), 44 deletions(-) rename extensions/void/src/{ApprovalCodeLensProvider.ts => DisplayChangesProvider.ts} (85%) diff --git a/extensions/void/package.json b/extensions/void/package.json index 0f9105b2..809329dd 100644 --- a/extensions/void/package.json +++ b/extensions/void/package.json @@ -72,11 +72,11 @@ "title": "Show Selection Lens" }, { - "command": "void.approveDiff", + "command": "void.acceptDiff", "title": "Approve Diff" }, { - "command": "void.discardDiff", + "command": "void.rejectDiff", "title": "Discard Diff" }, { diff --git a/extensions/void/src/ApprovalCodeLensProvider.ts b/extensions/void/src/DisplayChangesProvider.ts similarity index 85% rename from extensions/void/src/ApprovalCodeLensProvider.ts rename to extensions/void/src/DisplayChangesProvider.ts index 80386fa1..31c2a28a 100644 --- a/extensions/void/src/ApprovalCodeLensProvider.ts +++ b/extensions/void/src/DisplayChangesProvider.ts @@ -15,17 +15,17 @@ const greenDecoration = vscode.window.createTextEditorDecorationType({ isWholeLine: false, // after: { contentText: ' [original]', color: 'rgba(0 255 60 / 0.5)' } // hoverMessage: originalText // this applies to hovering over after:... }) -export class ApprovalCodeLensProvider implements vscode.CodeLensProvider { + +// responsible for displaying diffs and showing accept/reject buttons +export class ApplyChangesProvider implements vscode.CodeLensProvider { private _diffsOfDocument: { [docUriStr: string]: DiffType[] } = {}; private _computedLensesOfDocument: { [docUriStr: string]: vscode.CodeLens[] } = {} // computed from diffsOfDocument[docUriStr].lenses private _diffidPool = 0 - - private _onDidChangeCodeLenses: vscode.EventEmitter = new vscode.EventEmitter(); // signals a UI refresh on .fire() events - private _weAreEditing: boolean = false // used internally by vscode + private _onDidChangeCodeLenses: vscode.EventEmitter = new vscode.EventEmitter(); // signals a UI refresh on .fire() events public readonly onDidChangeCodeLenses: vscode.Event = this._onDidChangeCodeLenses.event; @@ -40,16 +40,19 @@ export class ApprovalCodeLensProvider implements vscode.CodeLensProvider { // this acts as a useEffect. Every time text changes, clear the diffs in this editor vscode.workspace.onDidChangeTextDocument((e) => { const editor = vscode.window.activeTextEditor + if (!editor) return if (this._weAreEditing) return + const docUri = editor.document.uri const docUriStr = docUri.toString() this._diffsOfDocument[docUriStr].splice(0) // clear diffs editor.setDecorations(greenDecoration, []) // clear decorations - this._computedLensesOfDocument[docUriStr] = this._diffsOfDocument[docUriStr].flatMap(diff => diff.lenses) // recompute - this._onDidChangeCodeLenses.fire() // refresh + + this._computedLensesOfDocument[docUriStr] = this._diffsOfDocument[docUriStr].flatMap(diff => diff.lenses) // recompute codelenses + this._onDidChangeCodeLenses.fire() // rerender codelenses }) } @@ -61,13 +64,15 @@ export class ApprovalCodeLensProvider implements vscode.CodeLensProvider { } // used by us only - public async addNewApprovals(editor: vscode.TextEditor, suggestedEdits: SuggestedEdit[]) { + public async addNewChanges(editor: vscode.TextEditor, suggestedEdits: SuggestedEdit[]) { const docUri = editor.document.uri const docUriStr = docUri.toString() + // if no diffs, set diffs to [] if (!this._diffsOfDocument[docUriStr]) this._diffsOfDocument[docUriStr] = [] + // if no codelenses, set codelenses to [] if (!this._computedLensesOfDocument[docUriStr]) this._computedLensesOfDocument[docUriStr] = [] @@ -84,7 +89,7 @@ export class ApprovalCodeLensProvider implements vscode.CodeLensProvider { // INSERTIONS (e.g. {originalStartLine: 0, originalEndLine: -1}) if (suggestedEdit.originalStartLine > suggestedEdit.originalEndLine) { const originalPosition = new vscode.Position(suggestedEdit.originalStartLine, 0) - workspaceEdit.insert(docUri, originalPosition, suggestedEdit.newContent + '\n') // add back in the line we deleted when we made the startline->endline range go negative + workspaceEdit.insert(docUri, originalPosition, suggestedEdit.afterCode + '\n') // add back in the line we deleted when we made the startline->endline range go negative greenRange = new vscode.Range(suggestedEdit.startLine, 0, suggestedEdit.endLine + 1, 0) } // DELETIONS @@ -92,22 +97,22 @@ export class ApprovalCodeLensProvider implements vscode.CodeLensProvider { const deleteRange = new vscode.Range(suggestedEdit.originalStartLine, 0, suggestedEdit.originalEndLine + 1, 0) workspaceEdit.delete(docUri, deleteRange) greenRange = new vscode.Range(suggestedEdit.startLine, 0, suggestedEdit.startLine, 0) - suggestedEdit.originalContent += '\n' // add back in the line we deleted when we made the startline->endline range go negative + suggestedEdit.beforeCode += '\n' // add back in the line we deleted when we made the startline->endline range go negative } // REPLACEMENTS else { const originalRange = new vscode.Range(suggestedEdit.originalStartLine, 0, suggestedEdit.originalEndLine, Number.MAX_SAFE_INTEGER) - workspaceEdit.replace(docUri, originalRange, suggestedEdit.newContent) + workspaceEdit.replace(docUri, originalRange, suggestedEdit.afterCode) greenRange = new vscode.Range(suggestedEdit.startLine, 0, suggestedEdit.endLine, Number.MAX_SAFE_INTEGER) } this._diffsOfDocument[docUriStr].push({ diffid: this._diffidPool, greenRange: greenRange, - originalCode: suggestedEdit.originalContent, + originalCode: suggestedEdit.beforeCode, lenses: [ - new vscode.CodeLens(greenRange, { title: 'Accept', command: 'void.approveDiff', arguments: [{ diffid: this._diffidPool }] }), - new vscode.CodeLens(greenRange, { title: 'Reject', command: 'void.discardDiff', arguments: [{ diffid: this._diffidPool }] }) + new vscode.CodeLens(greenRange, { title: 'Accept', command: 'void.acceptDiff', arguments: [{ diffid: this._diffidPool }] }), + new vscode.CodeLens(greenRange, { title: 'Reject', command: 'void.rejectDiff', arguments: [{ diffid: this._diffidPool }] }) ] }); this._diffidPool += 1 @@ -124,12 +129,13 @@ export class ApprovalCodeLensProvider implements vscode.CodeLensProvider { console.log('diffs after added:', this._diffsOfDocument[docUriStr]) } - // called on void.approveDiff - public async approveDiff({ diffid }: { diffid: number }) { + // called on void.acceptDiff + public async acceptDiff({ diffid }: { diffid: number }) { const editor = vscode.window.activeTextEditor if (!editor) return + // get document uri const docUri = editor.document.uri const docUriStr = docUri.toString() @@ -148,8 +154,8 @@ export class ApprovalCodeLensProvider implements vscode.CodeLensProvider { } - // called on void.discardDiff - public async discardDiff({ diffid }: { diffid: number }) { + // called on void.rejectDiff + public async rejectDiff({ diffid }: { diffid: number }) { const editor = vscode.window.activeTextEditor if (!editor) return @@ -172,7 +178,7 @@ export class ApprovalCodeLensProvider implements vscode.CodeLensProvider { // clear the decoration in this diffs range editor.setDecorations(greenDecoration, this._diffsOfDocument[docUriStr].map(diff => diff.greenRange)) - // REVERT THE CHANGE (this is the only part that's different from approveDiff) + // REVERT THE CHANGE (this is the only part that's different from acceptDiff) let workspaceEdit = new vscode.WorkspaceEdit(); workspaceEdit.replace(docUri, range, originalCode); this._weAreEditing = true diff --git a/extensions/void/src/common/sendLLMMessage.ts b/extensions/void/src/common/sendLLMMessage.ts index 9e47a80a..c0e1a8b4 100644 --- a/extensions/void/src/common/sendLLMMessage.ts +++ b/extensions/void/src/common/sendLLMMessage.ts @@ -64,9 +64,14 @@ const sendClaudeMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinal const anthropic = new Anthropic({ apiKey: apiConfig.anthropic.apikey, dangerouslyAllowBrowser: true }); // defaults to process.env["ANTHROPIC_API_KEY"] + console.log('max_tokens:' + apiConfig.anthropic.maxTokens) + + let max_tokens = parseInt(apiConfig.anthropic.maxTokens) + if (isNaN(max_tokens)) { max_tokens = 4000 } // TODO make a default max_tokens + const stream = anthropic.messages.stream({ model: apiConfig.anthropic.model, - max_tokens: parseInt(apiConfig.anthropic.maxTokens), + max_tokens: max_tokens, messages: messages, }); @@ -221,6 +226,8 @@ const sendGreptileMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFin export const sendLLMMessage: SendLLMMessageFnTypeExternal = ({ messages, onText, onFinalMessage, apiConfig }) => { if (!apiConfig) return { abort: () => { } } + console.log(`void: sending LLMMessage to ${apiConfig.whichApi}`) + const whichApi = apiConfig.whichApi if (whichApi === 'anthropic') { diff --git a/extensions/void/src/extension.ts b/extensions/void/src/extension.ts index 2d7e85a1..ab12b252 100644 --- a/extensions/void/src/extension.ts +++ b/extensions/void/src/extension.ts @@ -2,7 +2,7 @@ import * as vscode from 'vscode'; import { WebviewMessage } from './shared_types'; import { CtrlKCodeLensProvider } from './CtrlKCodeLensProvider'; import { getDiffedLines } from './getDiffedLines'; -import { ApprovalCodeLensProvider } from './ApprovalCodeLensProvider'; +import { ApplyChangesProvider as DisplayChangesProvider } from './DisplayChangesProvider'; import { SidebarWebviewProvider } from './SidebarWebviewProvider'; import { ApiConfig } from './common/sendLLMMessage'; @@ -71,22 +71,23 @@ export function activate(context: vscode.ExtensionContext) { ); // 3. Show an approve/reject codelens above each change - const approvalCodeLensProvider = new ApprovalCodeLensProvider(); - context.subscriptions.push(vscode.languages.registerCodeLensProvider('*', approvalCodeLensProvider)); + const displayChangesProvider = new DisplayChangesProvider(); + console.log(`void: Creating DisplayChangesProvider`) + context.subscriptions.push(vscode.languages.registerCodeLensProvider('*', displayChangesProvider)); // 4. Add approve/reject commands - context.subscriptions.push(vscode.commands.registerCommand('void.approveDiff', async (params) => { - approvalCodeLensProvider.approveDiff(params) + context.subscriptions.push(vscode.commands.registerCommand('void.acceptDiff', async (params) => { + displayChangesProvider.acceptDiff(params) })); - context.subscriptions.push(vscode.commands.registerCommand('void.discardDiff', async (params) => { - approvalCodeLensProvider.discardDiff(params) + context.subscriptions.push(vscode.commands.registerCommand('void.rejectDiff', async (params) => { + displayChangesProvider.rejectDiff(params) })); context.subscriptions.push(vscode.commands.registerCommand('void.openSettings', async () => { vscode.commands.executeCommand('workbench.action.openSettings', '@ext:void.void'); })); - // 5. + // 5. Receive messages from sidebar webviewProvider.webview.then( webview => { @@ -112,16 +113,21 @@ export function activate(context: vscode.ExtensionContext) { // send contents to webview webview.postMessage({ type: 'files', files, } satisfies WebviewMessage) - } else if (m.type === 'applyCode') { + } else if (m.type === 'applyChanges') { const editor = vscode.window.activeTextEditor if (!editor) { vscode.window.showInformationMessage('No active editor!') return } - const oldContents = await readFileContentOfUri(editor.document.uri) - const suggestedEdits = getDiffedLines(oldContents, m.code) - await approvalCodeLensProvider.addNewApprovals(editor, suggestedEdits) + + const beforeCode = await readFileContentOfUri(editor.document.uri) + + // TODO change this to be animated + const suggestedEdits = getDiffedLines(beforeCode, m.code) + + // when changes have been created + await displayChangesProvider.addNewChanges(editor, suggestedEdits) } else if (m.type === 'getApiConfig') { diff --git a/extensions/void/src/getDiffedLines.ts b/extensions/void/src/getDiffedLines.ts index 56fa119e..7533b20e 100644 --- a/extensions/void/src/getDiffedLines.ts +++ b/extensions/void/src/getDiffedLines.ts @@ -2,16 +2,16 @@ import { diffLines, Change } from 'diff'; export type SuggestedEdit = { // start/end of current file - startLine: number; - endLine: number; + startLine: number, + endLine: number, // start/end of original file originalStartLine: number, originalEndLine: number, // original content (originalfile[originalStart...originalEnd]) - originalContent: string; - newContent: string; + beforeCode: string; + afterCode: string; } export function getDiffedLines(oldStr: string, newStr: string) { @@ -46,7 +46,7 @@ export function getDiffedLines(oldStr: string, newStr: string) { const originalEndLine = oldFileLineNum - 1 // don't include current line, the edit was up to this line but not including it const originalContent = oldStrLines.slice(originalStartLine, originalEndLine + 1).join('\n') - const replacement: SuggestedEdit = { startLine, endLine, newContent, originalStartLine, originalEndLine, originalContent } + const replacement: SuggestedEdit = { beforeCode: originalContent, afterCode: newContent, startLine, endLine, originalStartLine, originalEndLine, } replacements.push(replacement) streakStartInNewFile = undefined diff --git a/extensions/void/src/shared_types.ts b/extensions/void/src/shared_types.ts index 4d7c0dc9..81efbe71 100644 --- a/extensions/void/src/shared_types.ts +++ b/extensions/void/src/shared_types.ts @@ -13,7 +13,7 @@ type WebviewMessage = ( | { type: 'ctrl+l', selection: Selection } // user presses ctrl+l in the editor // sidebar -> editor - | { type: 'applyCode', code: string } // user clicks "apply" in the sidebar + | { type: 'applyChanges', code: string } // user clicks "apply" in the sidebar // sidebar -> editor | { type: 'requestFiles', filepaths: vscode.Uri[] } diff --git a/extensions/void/src/sidebar/MarkdownRender.tsx b/extensions/void/src/sidebar/MarkdownRender.tsx index e9cc2b96..ebc9a2a7 100644 --- a/extensions/void/src/sidebar/MarkdownRender.tsx +++ b/extensions/void/src/sidebar/MarkdownRender.tsx @@ -8,7 +8,7 @@ export const BlockCode = ({ text, disableApplyButton = false }: { text: string, return
{disableApplyButton ? null :
+ onClick={async () => { getVSCodeAPI().postMessage({ type: 'applyChanges', code: text }) }}>Apply
}
diff --git a/extensions/void/src/sidebar/Sidebar.tsx b/extensions/void/src/sidebar/Sidebar.tsx
index 4cba1ccb..5cb9f9c8 100644
--- a/extensions/void/src/sidebar/Sidebar.tsx
+++ b/extensions/void/src/sidebar/Sidebar.tsx
@@ -186,10 +186,13 @@ const Sidebar = () => {
 
 
 	const formRef = useRef(null)
+
 	const onSubmit = async (e: FormEvent) => {
 
+		console.log(`11111`)
 		e.preventDefault()
 		if (isLoading) return
+		console.log(`2222222`)
 
 		setIsLoading(true)
 		setInstructions('');
@@ -197,9 +200,14 @@ const Sidebar = () => {
 		setSelection(null)
 		setFiles([])
 
+
+		console.log(`AAAAAA`)
+
 		// request file content from vscode and await response
 		getVSCodeAPI().postMessage({ type: 'requestFiles', filepaths: files })
+		console.log(`BBBBB`)
 		const relevantFiles = await awaitVSCodeResponse('files')
+		console.log(`CCCCCC`)
 
 		// add message to chat history
 		const content = userInstructionsStr(instructions, relevantFiles.files, selection)
@@ -207,7 +215,8 @@ const Sidebar = () => {
 		const newHistoryElt: ChatMessage = { role: 'user', content, displayContent: instructions, selection, files }
 		setChatHistory(chatMessageHistory => [...chatMessageHistory, newHistoryElt])
 
-		// send message to claude
+		// send message to LLM
+		console.log(`DDDDD`)
 		let { abort } = sendLLMMessage({
 			messages: [...chatMessageHistory.map(m => ({ role: m.role, content: m.content })), { role: 'user', content }],
 			onText: (newText, fullText) => setMessageStream(fullText),
@@ -223,7 +232,9 @@ const Sidebar = () => {
 			},
 			apiConfig: apiConfig
 		})
+		console.log(`EEEEE`)
 		abortFnRef.current = abort
+		console.log(`FFFF`)
 
 	}
 
@@ -266,7 +277,7 @@ const Sidebar = () => {
 					{!selection?.selectionStr ? null
 						: (
 							
-
- )} + )}
res[] const awaiting: { [c in Command]: ((res: any) => void)[] } = { "ctrl+l": [], - "applyCode": [], + "applyChanges": [], "requestFiles": [], "files": [], "apiConfig": [], From 19b2b8bd77c3c8b6d7b7744a9b74bc68e40c6209 Mon Sep 17 00:00:00 2001 From: Mathew P Date: Thu, 3 Oct 2024 03:00:55 -0700 Subject: [PATCH 02/10] create diffArea; remove _computedLensesOfDocument --- extensions/void/src/DisplayChangesProvider.ts | 21 +++++---------- extensions/void/src/extension.ts | 26 +++++++++++++++---- extensions/void/src/shared_types.ts | 22 +++++++++++++--- extensions/void/src/sidebar/Sidebar.tsx | 8 +++--- 4 files changed, 51 insertions(+), 26 deletions(-) diff --git a/extensions/void/src/DisplayChangesProvider.ts b/extensions/void/src/DisplayChangesProvider.ts index 31c2a28a..2b6bed37 100644 --- a/extensions/void/src/DisplayChangesProvider.ts +++ b/extensions/void/src/DisplayChangesProvider.ts @@ -1,13 +1,8 @@ import * as vscode from 'vscode'; import { SuggestedEdit } from './getDiffedLines'; +import { Diff, DiffArea } from './shared_types'; + -// each diff on the user's screen right now -type DiffType = { - diffid: number, - lenses: vscode.CodeLens[], - greenRange: vscode.Range, - originalCode: string, // If a revert happens, we replace the greenRange with this content. -} // TODO in theory this should be disposed const greenDecoration = vscode.window.createTextEditorDecorationType({ @@ -19,7 +14,9 @@ const greenDecoration = vscode.window.createTextEditorDecorationType({ // responsible for displaying diffs and showing accept/reject buttons export class ApplyChangesProvider implements vscode.CodeLensProvider { - private _diffsOfDocument: { [docUriStr: string]: DiffType[] } = {}; + private _diffAreasOfDocument: { [docUriStr: string]: DiffArea[] } = {} + private _diffsOfDocument: { [docUriStr: string]: Diff[] } = {} + private _computedLensesOfDocument: { [docUriStr: string]: vscode.CodeLens[] } = {} // computed from diffsOfDocument[docUriStr].lenses private _diffidPool = 0 private _weAreEditing: boolean = false @@ -32,7 +29,7 @@ export class ApplyChangesProvider implements vscode.CodeLensProvider { // used internally by vscode public provideCodeLenses(document: vscode.TextDocument, token: vscode.CancellationToken): vscode.ProviderResult { const docUriStr = document.uri.toString() - return this._computedLensesOfDocument[docUriStr] + return this._diffsOfDocument[docUriStr].flatMap(diff => diff.lenses) } // declared by us, registered with vscode.languages.registerCodeLensProvider() @@ -51,7 +48,6 @@ export class ApplyChangesProvider implements vscode.CodeLensProvider { this._diffsOfDocument[docUriStr].splice(0) // clear diffs editor.setDecorations(greenDecoration, []) // clear decorations - this._computedLensesOfDocument[docUriStr] = this._diffsOfDocument[docUriStr].flatMap(diff => diff.lenses) // recompute codelenses this._onDidChangeCodeLenses.fire() // rerender codelenses }) } @@ -59,7 +55,7 @@ export class ApplyChangesProvider implements vscode.CodeLensProvider { // used by us only private refreshLenses = (editor: vscode.TextEditor, docUriStr: string) => { editor.setDecorations(greenDecoration, this._diffsOfDocument[docUriStr].map(diff => diff.greenRange)) // refresh highlighting - this._computedLensesOfDocument[docUriStr] = this._diffsOfDocument[docUriStr].flatMap(diff => diff.lenses) // recompute _computedLensesOfDocument (can optimize this later) + this._onDidChangeCodeLenses.fire() // fire event for vscode to refresh lenses } @@ -72,9 +68,6 @@ export class ApplyChangesProvider implements vscode.CodeLensProvider { // if no diffs, set diffs to [] if (!this._diffsOfDocument[docUriStr]) this._diffsOfDocument[docUriStr] = [] - // if no codelenses, set codelenses to [] - if (!this._computedLensesOfDocument[docUriStr]) - this._computedLensesOfDocument[docUriStr] = [] // 1. convert suggested edits (which are described using line numbers) into actual edits (described using vscode.Range, vscode.Uri) diff --git a/extensions/void/src/extension.ts b/extensions/void/src/extension.ts index ab12b252..b85a9b7d 100644 --- a/extensions/void/src/extension.ts +++ b/extensions/void/src/extension.ts @@ -1,5 +1,5 @@ import * as vscode from 'vscode'; -import { WebviewMessage } from './shared_types'; +import { DiffArea, WebviewMessage } from './shared_types'; import { CtrlKCodeLensProvider } from './CtrlKCodeLensProvider'; import { getDiffedLines } from './getDiffedLines'; import { ApplyChangesProvider as DisplayChangesProvider } from './DisplayChangesProvider'; @@ -121,12 +121,28 @@ export function activate(context: vscode.ExtensionContext) { return } - const beforeCode = await readFileContentOfUri(editor.document.uri) + const diffArea: DiffArea = { + startLine: 0, // in ctrl+L the start and end lines are the full document + endLine: editor.document.lineCount, + originalCode: undefined, + } - // TODO change this to be animated - const suggestedEdits = getDiffedLines(beforeCode, m.code) + // save the original code + diffArea.originalCode = await readFileContentOfUri(editor.document.uri) - // when changes have been created + // write the new code `m.code` to the document + // TODO make this animated + editor.edit(editBuilder => { + editBuilder.replace(new vscode.Range(diffArea.startLine, 0, diffArea.endLine, 0), m.code); + }); + + + // rediff the changes based on the diffArea (start, end, original code, current code) + + + + // TODO!!! put this logic in `displayChangesProvider.displayChanges(diffArea)` function + const suggestedEdits = getDiffedLines(diffArea.originalCode, m.code) await displayChangesProvider.addNewChanges(editor, suggestedEdits) } else if (m.type === 'getApiConfig') { diff --git a/extensions/void/src/shared_types.ts b/extensions/void/src/shared_types.ts index 81efbe71..da4381ea 100644 --- a/extensions/void/src/shared_types.ts +++ b/extensions/void/src/shared_types.ts @@ -3,14 +3,29 @@ import * as vscode from 'vscode'; import { ApiConfig } from './common/sendLLMMessage'; // a selection is a frozen snapshot -type Selection = { selectionStr: string, selectionRange: vscode.Range, filePath: vscode.Uri } +type CodeSelection = { selectionStr: string, selectionRange: vscode.Range, filePath: vscode.Uri } type File = { filepath: vscode.Uri, content: string } +// an area that is currently being diffed +type DiffArea = { + startLine: number, + endLine: number, + originalCode: string | undefined +} + +// each diff on the user's screen right now +type Diff = { + diffid: number, + lenses: vscode.CodeLens[], + greenRange: vscode.Range, + originalCode: string, // If a revert happens, we replace the greenRange with this content. +} + type WebviewMessage = ( // editor -> sidebar - | { type: 'ctrl+l', selection: Selection } // user presses ctrl+l in the editor + | { type: 'ctrl+l', selection: CodeSelection } // user presses ctrl+l in the editor // sidebar -> editor | { type: 'applyChanges', code: string } // user clicks "apply" in the sidebar @@ -32,8 +47,9 @@ type WebviewMessage = ( type Command = WebviewMessage['type'] export { - Selection, + CodeSelection, File, WebviewMessage, Command, + Diff, DiffArea, } diff --git a/extensions/void/src/sidebar/Sidebar.tsx b/extensions/void/src/sidebar/Sidebar.tsx index 5cb9f9c8..cfa8d3a7 100644 --- a/extensions/void/src/sidebar/Sidebar.tsx +++ b/extensions/void/src/sidebar/Sidebar.tsx @@ -1,6 +1,6 @@ import React, { useState, ChangeEvent, useEffect, useRef, useCallback, FormEvent } from "react" import { ApiConfig, LLMMessage, sendLLMMessage } from "../common/sendLLMMessage" -import { Command, File, Selection, WebviewMessage } from "../shared_types" +import { Command, File, CodeSelection, WebviewMessage } from "../shared_types" import { awaitVSCodeResponse, getVSCodeAPI, resolveAwaitingVSCodeResponse } from "./getVscodeApi" import { marked } from 'marked'; @@ -18,7 +18,7 @@ ${content} \`\`\``).join('\n') } -const userInstructionsStr = (instructions: string, files: File[], selection: Selection | null) => { +const userInstructionsStr = (instructions: string, files: File[], selection: CodeSelection | null) => { return ` ${filesStr(files)} @@ -110,7 +110,7 @@ type ChatMessage = { role: 'user' content: string, // content sent to the llm displayContent: string, // content displayed to user - selection: Selection | null, // the user's selection + selection: CodeSelection | null, // the user's selection files: vscode.Uri[], // the files sent in the message } | { role: 'assistant', @@ -136,7 +136,7 @@ const useInstantState = (initVal: T) => { const Sidebar = () => { // state of current message - const [selection, setSelection] = useState(null) // the code the user is selecting + const [selection, setSelection] = useState(null) // the code the user is selecting const [files, setFiles] = useState([]) // the names of the files in the chat const [instructions, setInstructions] = useState('') // the user's instructions From b07842ec3fe18b86a40c1ad7d4747e2d7652ee64 Mon Sep 17 00:00:00 2001 From: Mathew P Date: Thu, 3 Oct 2024 03:01:27 -0700 Subject: [PATCH 03/10] Remove _computedLensesOfDocument --- extensions/void/src/DisplayChangesProvider.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/extensions/void/src/DisplayChangesProvider.ts b/extensions/void/src/DisplayChangesProvider.ts index 2b6bed37..36012174 100644 --- a/extensions/void/src/DisplayChangesProvider.ts +++ b/extensions/void/src/DisplayChangesProvider.ts @@ -17,7 +17,6 @@ export class ApplyChangesProvider implements vscode.CodeLensProvider { private _diffAreasOfDocument: { [docUriStr: string]: DiffArea[] } = {} private _diffsOfDocument: { [docUriStr: string]: Diff[] } = {} - private _computedLensesOfDocument: { [docUriStr: string]: vscode.CodeLens[] } = {} // computed from diffsOfDocument[docUriStr].lenses private _diffidPool = 0 private _weAreEditing: boolean = false From 11baebdf5468f996898822c1a3ad933b70ff7421 Mon Sep 17 00:00:00 2001 From: Mathew P Date: Mon, 7 Oct 2024 01:56:29 -0700 Subject: [PATCH 04/10] diffArea works. Need to fix highlighting --- extensions/void/src/DisplayChangesProvider.ts | 127 ++++++++++++------ extensions/void/src/extension.ts | 25 ++-- extensions/void/src/getDiffedLines.ts | 11 +- extensions/void/src/shared_types.ts | 2 +- 4 files changed, 112 insertions(+), 53 deletions(-) diff --git a/extensions/void/src/DisplayChangesProvider.ts b/extensions/void/src/DisplayChangesProvider.ts index 36012174..306b262d 100644 --- a/extensions/void/src/DisplayChangesProvider.ts +++ b/extensions/void/src/DisplayChangesProvider.ts @@ -1,5 +1,5 @@ import * as vscode from 'vscode'; -import { SuggestedEdit } from './getDiffedLines'; +import { getDiffedLines, SuggestedDiff } from './getDiffedLines'; import { Diff, DiffArea } from './shared_types'; @@ -24,7 +24,6 @@ export class ApplyChangesProvider implements vscode.CodeLensProvider { private _onDidChangeCodeLenses: vscode.EventEmitter = new vscode.EventEmitter(); // signals a UI refresh on .fire() events public readonly onDidChangeCodeLenses: vscode.Event = this._onDidChangeCodeLenses.event; - // used internally by vscode public provideCodeLenses(document: vscode.TextDocument, token: vscode.CancellationToken): vscode.ProviderResult { const docUriStr = document.uri.toString() @@ -33,6 +32,9 @@ export class ApplyChangesProvider implements vscode.CodeLensProvider { // declared by us, registered with vscode.languages.registerCodeLensProvider() constructor() { + + console.log('Creating DisplayChangesProvider') + // this acts as a useEffect. Every time text changes, clear the diffs in this editor vscode.workspace.onDidChangeTextDocument((e) => { const editor = vscode.window.activeTextEditor @@ -43,25 +45,82 @@ export class ApplyChangesProvider implements vscode.CodeLensProvider { return const docUri = editor.document.uri - const docUriStr = docUri.toString() - this._diffsOfDocument[docUriStr].splice(0) // clear diffs - editor.setDecorations(greenDecoration, []) // clear decorations + this.refreshDiffAreas(docUri) + + // const docUriStr = docUri.toString() + // this._diffAreasOfDocument[docUriStr].splice(0) // clear diff areas + // this._diffsOfDocument[docUriStr].splice(0) // clear diffs + // editor.setDecorations(greenDecoration, []) // clear decorations + // this._onDidChangeCodeLenses.fire() // rerender codelenses + - this._onDidChangeCodeLenses.fire() // rerender codelenses }) } - // used by us only - private refreshLenses = (editor: vscode.TextEditor, docUriStr: string) => { - editor.setDecorations(greenDecoration, this._diffsOfDocument[docUriStr].map(diff => diff.greenRange)) // refresh highlighting - this._onDidChangeCodeLenses.fire() // fire event for vscode to refresh lenses + // used by us only + public addDiffArea(uri: vscode.Uri, diffArea: DiffArea) { + + const uriStr = uri.toString() + + // make sure array is defined + if (!this._diffAreasOfDocument[uriStr]) + this._diffAreasOfDocument[uriStr] = [] + + // TODO!!! replace all areas that it is overlapping with + + + + // add diffArea to storage + this._diffAreasOfDocument[uriStr].push(diffArea) + + } + + + // used by us only + public refreshDiffAreas(docUri: vscode.Uri) { + + 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 docUriStr = docUri.toString() + const diffAreas = this._diffAreasOfDocument[docUriStr] || [] + + // reset all diffs (we update them below) + this._diffsOfDocument[docUriStr] = [] + + // for each diffArea + console.log('diffAreas.length:', diffAreas.length) + for (const diffArea of diffAreas) { + + // get code inside of diffArea + const currentCode = editor.document.getText(new vscode.Range(diffArea.startLine, 0, diffArea.endLine, Number.MAX_SAFE_INTEGER)) + + // compute the diffs + const diffs = getDiffedLines(diffArea.originalCode, currentCode) + + console.log('originalCode:', diffArea.originalCode) + console.log('currentCode:', currentCode) + + // add the diffs to `this._diffsOfDocument[docUriStr]` + this.addDiffs(editor.document.uri, diffs) + + } + + // update highlighting + editor.setDecorations(greenDecoration, this._diffsOfDocument[docUriStr].map(diff => diff.greenRange)) + + // update code lenses + this._onDidChangeCodeLenses.fire() + } // used by us only - public async addNewChanges(editor: vscode.TextEditor, suggestedEdits: SuggestedEdit[]) { + public addDiffs(docUri: vscode.Uri, diffs: SuggestedDiff[]) { - const docUri = editor.document.uri const docUriStr = docUri.toString() // if no diffs, set diffs to [] @@ -69,39 +128,39 @@ export class ApplyChangesProvider implements vscode.CodeLensProvider { this._diffsOfDocument[docUriStr] = [] - // 1. convert suggested edits (which are described using line numbers) into actual edits (described using vscode.Range, vscode.Uri) + // 1. convert suggested diffs (which are described using line numbers) into actual diffs (described using vscode.Range, vscode.Uri) // must do this before adding codelenses or highlighting so that codelens and highlights will apply to the fresh code and not the old code // apply changes in reverse order so additions don't push down the line numbers of the next edit let workspaceEdit = new vscode.WorkspaceEdit(); - for (let i = suggestedEdits.length - 1; i > -1; i -= 1) { - let suggestedEdit = suggestedEdits[i] + for (let i = diffs.length - 1; i > -1; i -= 1) { + let suggestedDiff = diffs[i] let greenRange: vscode.Range // INSERTIONS (e.g. {originalStartLine: 0, originalEndLine: -1}) - if (suggestedEdit.originalStartLine > suggestedEdit.originalEndLine) { - const originalPosition = new vscode.Position(suggestedEdit.originalStartLine, 0) - workspaceEdit.insert(docUri, originalPosition, suggestedEdit.afterCode + '\n') // add back in the line we deleted when we made the startline->endline range go negative - greenRange = new vscode.Range(suggestedEdit.startLine, 0, suggestedEdit.endLine + 1, 0) + if (suggestedDiff.originalStartLine > suggestedDiff.originalEndLine) { + const originalPosition = new vscode.Position(suggestedDiff.originalStartLine, 0) + workspaceEdit.insert(docUri, originalPosition, suggestedDiff.afterCode + '\n') // add back in the line we deleted when we made the startline->endline range go negative + greenRange = new vscode.Range(suggestedDiff.startLine, 0, suggestedDiff.endLine + 1, 0) } // DELETIONS - else if (suggestedEdit.startLine > suggestedEdit.endLine) { - const deleteRange = new vscode.Range(suggestedEdit.originalStartLine, 0, suggestedEdit.originalEndLine + 1, 0) + else if (suggestedDiff.startLine > suggestedDiff.endLine) { + const deleteRange = new vscode.Range(suggestedDiff.originalStartLine, 0, suggestedDiff.originalEndLine + 1, 0) workspaceEdit.delete(docUri, deleteRange) - greenRange = new vscode.Range(suggestedEdit.startLine, 0, suggestedEdit.startLine, 0) - suggestedEdit.beforeCode += '\n' // add back in the line we deleted when we made the startline->endline range go negative + greenRange = new vscode.Range(suggestedDiff.startLine, 0, suggestedDiff.startLine, 0) + suggestedDiff.beforeCode += '\n' // add back in the line we deleted when we made the startline->endline range go negative } // REPLACEMENTS else { - const originalRange = new vscode.Range(suggestedEdit.originalStartLine, 0, suggestedEdit.originalEndLine, Number.MAX_SAFE_INTEGER) - workspaceEdit.replace(docUri, originalRange, suggestedEdit.afterCode) - greenRange = new vscode.Range(suggestedEdit.startLine, 0, suggestedEdit.endLine, Number.MAX_SAFE_INTEGER) + const originalRange = new vscode.Range(suggestedDiff.originalStartLine, 0, suggestedDiff.originalEndLine, Number.MAX_SAFE_INTEGER) + workspaceEdit.replace(docUri, originalRange, suggestedDiff.afterCode) + greenRange = new vscode.Range(suggestedDiff.startLine, 0, suggestedDiff.endLine, Number.MAX_SAFE_INTEGER) } this._diffsOfDocument[docUriStr].push({ diffid: this._diffidPool, greenRange: greenRange, - originalCode: suggestedEdit.beforeCode, + originalCode: suggestedDiff.beforeCode, lenses: [ new vscode.CodeLens(greenRange, { title: 'Accept', command: 'void.acceptDiff', arguments: [{ diffid: this._diffidPool }] }), new vscode.CodeLens(greenRange, { title: 'Reject', command: 'void.rejectDiff', arguments: [{ diffid: this._diffidPool }] }) @@ -110,15 +169,7 @@ export class ApplyChangesProvider implements vscode.CodeLensProvider { this._diffidPool += 1 } - this._weAreEditing = true - await vscode.workspace.applyEdit(workspaceEdit) - await vscode.workspace.save(docUri) - this._weAreEditing = false - - // refresh - this.refreshLenses(editor, docUriStr) - - console.log('diffs after added:', this._diffsOfDocument[docUriStr]) + console.log('diffs:', this._diffsOfDocument[docUriStr]) } // called on void.acceptDiff @@ -142,7 +193,7 @@ export class ApplyChangesProvider implements vscode.CodeLensProvider { this._diffsOfDocument[docUriStr].splice(index, 1) // refresh - this.refreshLenses(editor, docUriStr) + this.refreshDiffAreas(docUri) } @@ -179,6 +230,6 @@ export class ApplyChangesProvider implements vscode.CodeLensProvider { this._weAreEditing = false // refresh - this.refreshLenses(editor, docUriStr) + this.refreshDiffAreas(docUri) } } diff --git a/extensions/void/src/extension.ts b/extensions/void/src/extension.ts index b85a9b7d..2c45415d 100644 --- a/extensions/void/src/extension.ts +++ b/extensions/void/src/extension.ts @@ -115,35 +115,38 @@ export function activate(context: vscode.ExtensionContext) { } else if (m.type === 'applyChanges') { + console.log('Applying changes') + const editor = vscode.window.activeTextEditor if (!editor) { vscode.window.showInformationMessage('No active editor!') return } + // create an area to show diffs const diffArea: DiffArea = { startLine: 0, // in ctrl+L the start and end lines are the full document endLine: editor.document.lineCount, - originalCode: undefined, + originalCode: await readFileContentOfUri(editor.document.uri), } + displayChangesProvider.addDiffArea(editor.document.uri, diffArea) - // save the original code - diffArea.originalCode = await readFileContentOfUri(editor.document.uri) - // write the new code `m.code` to the document - // TODO make this animated - editor.edit(editBuilder => { + // 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 + await editor.edit(editBuilder => { editBuilder.replace(new vscode.Range(diffArea.startLine, 0, diffArea.endLine, 0), m.code); }); - // rediff the changes based on the diffArea (start, end, original code, current code) + // rediff the changes based on the diffArea (start, end, original code, [current code]) + displayChangesProvider.refreshDiffAreas(editor.document.uri) - - // TODO!!! put this logic in `displayChangesProvider.displayChanges(diffArea)` function - const suggestedEdits = getDiffedLines(diffArea.originalCode, m.code) - await displayChangesProvider.addNewChanges(editor, suggestedEdits) } else if (m.type === 'getApiConfig') { diff --git a/extensions/void/src/getDiffedLines.ts b/extensions/void/src/getDiffedLines.ts index 7533b20e..04d5bb7b 100644 --- a/extensions/void/src/getDiffedLines.ts +++ b/extensions/void/src/getDiffedLines.ts @@ -1,6 +1,6 @@ import { diffLines, Change } from 'diff'; -export type SuggestedEdit = { +export type SuggestedDiff = { // start/end of current file startLine: number, endLine: number, @@ -16,6 +16,11 @@ export type SuggestedEdit = { export function getDiffedLines(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) + + // replace \r\n with \n + oldStr = oldStr.replace(/\r\n/g, '\n') + newStr = newStr.replace(/\r\n/g, '\n') + const lineByLineChanges: Change[] = diffLines(oldStr, newStr); console.debug('Line by line changes', lineByLineChanges) @@ -30,7 +35,7 @@ export function getDiffedLines(oldStr: string, newStr: string) { let oldStrLines = oldStr.split('\n') let newStrLines = newStr.split('\n') - const replacements: SuggestedEdit[] = [] + const replacements: SuggestedDiff[] = [] for (let line of lineByLineChanges) { // no change on this line @@ -46,7 +51,7 @@ export function getDiffedLines(oldStr: string, newStr: string) { const originalEndLine = oldFileLineNum - 1 // don't include current line, the edit was up to this line but not including it const originalContent = oldStrLines.slice(originalStartLine, originalEndLine + 1).join('\n') - const replacement: SuggestedEdit = { beforeCode: originalContent, afterCode: newContent, startLine, endLine, originalStartLine, originalEndLine, } + const replacement: SuggestedDiff = { beforeCode: originalContent, afterCode: newContent, startLine, endLine, originalStartLine, originalEndLine, } replacements.push(replacement) streakStartInNewFile = undefined diff --git a/extensions/void/src/shared_types.ts b/extensions/void/src/shared_types.ts index da4381ea..f16fa3f5 100644 --- a/extensions/void/src/shared_types.ts +++ b/extensions/void/src/shared_types.ts @@ -11,7 +11,7 @@ type File = { filepath: vscode.Uri, content: string } type DiffArea = { startLine: number, endLine: number, - originalCode: string | undefined + originalCode: string } // each diff on the user's screen right now From 38d31ae737416203488697ff22edc2937586be9c Mon Sep 17 00:00:00 2001 From: Mathew P Date: Fri, 11 Oct 2024 01:06:17 -0700 Subject: [PATCH 05/10] Make diffs dynamic on changes --- extensions/void/src/DisplayChangesProvider.ts | 74 +++++++++++++++---- extensions/void/src/extension.ts | 2 +- 2 files changed, 59 insertions(+), 17 deletions(-) diff --git a/extensions/void/src/DisplayChangesProvider.ts b/extensions/void/src/DisplayChangesProvider.ts index 306b262d..5c74cf34 100644 --- a/extensions/void/src/DisplayChangesProvider.ts +++ b/extensions/void/src/DisplayChangesProvider.ts @@ -12,7 +12,7 @@ const greenDecoration = vscode.window.createTextEditorDecorationType({ // responsible for displaying diffs and showing accept/reject buttons -export class ApplyChangesProvider implements vscode.CodeLensProvider { +export class DisplayChangesProvider implements vscode.CodeLensProvider { private _diffAreasOfDocument: { [docUriStr: string]: DiffArea[] } = {} private _diffsOfDocument: { [docUriStr: string]: Diff[] } = {} @@ -44,16 +44,57 @@ export class ApplyChangesProvider implements vscode.CodeLensProvider { if (this._weAreEditing) return + // console.log('e.contentChanges', e.contentChanges) + // console.log('e.contentChanges[0].text:', e.contentChanges?.[0]) + const docUri = editor.document.uri + const docUriStr = docUri.toString() + const diffAreas = this._diffAreasOfDocument[docUriStr] || [] + + // loop through each change + for (const change of e.contentChanges) { + + // 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.range.end.line - change.range.start.line + 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.range.start.line >= diffArea.startLine && change.range.end.line <= 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.range.end.line) { + diffArea.startLine += deltaNewlines + diffArea.endLine += deltaNewlines + } + } + + // TODO merge any diffAreas if they overlap with each other as a result from the shift + + } + + // refresh the diffAreas this.refreshDiffAreas(docUri) - // const docUriStr = docUri.toString() - // this._diffAreasOfDocument[docUriStr].splice(0) // clear diff areas - // this._diffsOfDocument[docUriStr].splice(0) // clear diffs - // editor.setDecorations(greenDecoration, []) // clear decorations - // this._onDidChangeCodeLenses.fire() // rerender codelenses - - }) } @@ -67,11 +108,16 @@ export class ApplyChangesProvider implements vscode.CodeLensProvider { if (!this._diffAreasOfDocument[uriStr]) this._diffAreasOfDocument[uriStr] = [] - // TODO!!! replace all areas that it is overlapping with + // remove all diffAreas that the new `diffArea` is overlapping with + this._diffAreasOfDocument[uriStr] = this._diffAreasOfDocument[uriStr].filter(da => { + // condition for no overlap + const noOverlap = da.startLine > diffArea.endLine || da.endLine < diffArea.startLine + // if there is overlap (ie there is `not noOverlap`), remove `da` + if (!noOverlap) return false + return true + }) - - - // add diffArea to storage + // add `diffArea` to storage this._diffAreasOfDocument[uriStr].push(diffArea) } @@ -93,7 +139,6 @@ export class ApplyChangesProvider implements vscode.CodeLensProvider { this._diffsOfDocument[docUriStr] = [] // for each diffArea - console.log('diffAreas.length:', diffAreas.length) for (const diffArea of diffAreas) { // get code inside of diffArea @@ -102,9 +147,6 @@ export class ApplyChangesProvider implements vscode.CodeLensProvider { // compute the diffs const diffs = getDiffedLines(diffArea.originalCode, currentCode) - console.log('originalCode:', diffArea.originalCode) - console.log('currentCode:', currentCode) - // add the diffs to `this._diffsOfDocument[docUriStr]` this.addDiffs(editor.document.uri, diffs) diff --git a/extensions/void/src/extension.ts b/extensions/void/src/extension.ts index 2c45415d..1735b8d5 100644 --- a/extensions/void/src/extension.ts +++ b/extensions/void/src/extension.ts @@ -2,7 +2,7 @@ import * as vscode from 'vscode'; import { DiffArea, WebviewMessage } from './shared_types'; import { CtrlKCodeLensProvider } from './CtrlKCodeLensProvider'; import { getDiffedLines } from './getDiffedLines'; -import { ApplyChangesProvider as DisplayChangesProvider } from './DisplayChangesProvider'; +import { DisplayChangesProvider } from './DisplayChangesProvider'; import { SidebarWebviewProvider } from './SidebarWebviewProvider'; import { ApiConfig } from './common/sendLLMMessage'; From 496c0178de384e7a6b09231bfbf3e09fa5c0c5fa Mon Sep 17 00:00:00 2001 From: Mathew P Date: Fri, 11 Oct 2024 14:03:11 -0700 Subject: [PATCH 06/10] Prepare to fix diff highlighting --- extensions/void/src/DisplayChangesProvider.ts | 16 +++++++++++----- extensions/void/src/extension.ts | 4 ++-- extensions/void/src/getDiffedLines.ts | 3 --- 3 files changed, 13 insertions(+), 10 deletions(-) diff --git a/extensions/void/src/DisplayChangesProvider.ts b/extensions/void/src/DisplayChangesProvider.ts index 5c74cf34..6099326e 100644 --- a/extensions/void/src/DisplayChangesProvider.ts +++ b/extensions/void/src/DisplayChangesProvider.ts @@ -44,9 +44,6 @@ export class DisplayChangesProvider implements vscode.CodeLensProvider { if (this._weAreEditing) return - // console.log('e.contentChanges', e.contentChanges) - // console.log('e.contentChanges[0].text:', e.contentChanges?.[0]) - const docUri = editor.document.uri const docUriStr = docUri.toString() const diffAreas = this._diffAreasOfDocument[docUriStr] || [] @@ -142,14 +139,24 @@ export class DisplayChangesProvider implements vscode.CodeLensProvider { for (const diffArea of diffAreas) { // get code inside of diffArea - const currentCode = editor.document.getText(new vscode.Range(diffArea.startLine, 0, diffArea.endLine, Number.MAX_SAFE_INTEGER)) + 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 = getDiffedLines(diffArea.originalCode, currentCode) + // print diffs + console.log('!CODEBefore:', JSON.stringify(diffArea.originalCode)) + console.log('!CODEAfter:', JSON.stringify(currentCode)) + // add the diffs to `this._diffsOfDocument[docUriStr]` this.addDiffs(editor.document.uri, diffs) + for (const diff of this._diffsOfDocument[docUriStr]) { + console.log('originalCodeDiff:', JSON.stringify(diff.originalCode)) + console.log('greenCodeDiff:', JSON.stringify(editor.document.getText(diff.greenRange).replace(/\r\n/g, '\n'))) + } + + } // update highlighting @@ -211,7 +218,6 @@ export class DisplayChangesProvider implements vscode.CodeLensProvider { this._diffidPool += 1 } - console.log('diffs:', this._diffsOfDocument[docUriStr]) } // called on void.acceptDiff diff --git a/extensions/void/src/extension.ts b/extensions/void/src/extension.ts index 1735b8d5..d2da0238 100644 --- a/extensions/void/src/extension.ts +++ b/extensions/void/src/extension.ts @@ -1,13 +1,13 @@ import * as vscode from 'vscode'; import { DiffArea, WebviewMessage } from './shared_types'; import { CtrlKCodeLensProvider } from './CtrlKCodeLensProvider'; -import { getDiffedLines } from './getDiffedLines'; import { DisplayChangesProvider } from './DisplayChangesProvider'; import { SidebarWebviewProvider } from './SidebarWebviewProvider'; import { ApiConfig } from './common/sendLLMMessage'; const readFileContentOfUri = async (uri: vscode.Uri) => { - return Buffer.from(await vscode.workspace.fs.readFile(uri)).toString('utf8').replace(/\r\n/g, '\n'); // must remove windows \r or every line will appear different because of it + return Buffer.from(await vscode.workspace.fs.readFile(uri)).toString('utf8') + .replace(/\r\n/g, '\n') // must remove windows \r or every line will appear different because of it } diff --git a/extensions/void/src/getDiffedLines.ts b/extensions/void/src/getDiffedLines.ts index 04d5bb7b..e16f56c9 100644 --- a/extensions/void/src/getDiffedLines.ts +++ b/extensions/void/src/getDiffedLines.ts @@ -22,7 +22,6 @@ export function getDiffedLines(oldStr: string, newStr: string) { newStr = newStr.replace(/\r\n/g, '\n') const lineByLineChanges: Change[] = diffLines(oldStr, newStr); - console.debug('Line by line changes', lineByLineChanges) lineByLineChanges.push({ value: '' }) // add a dummy so we flush any streaks we haven't yet at the very end (!line.added && !line.removed) @@ -89,8 +88,6 @@ export function getDiffedLines(oldStr: string, newStr: string) { } // end for - console.debug('Replacements', replacements) - return replacements } From 1557a44934d99484854aa0774e1f7f40172cf0ab Mon Sep 17 00:00:00 2001 From: Mathew P Date: Sun, 13 Oct 2024 00:15:41 -0700 Subject: [PATCH 07/10] Better diff algorithm --- extensions/void/package-lock.json | 13 ++ extensions/void/package.json | 2 + extensions/void/src/DisplayChangesProvider.ts | 70 +++---- extensions/void/src/extension.ts | 2 +- extensions/void/src/findDiffs.ts | 176 ++++++++++++++++++ extensions/void/src/getDiffedLines.ts | 93 --------- extensions/void/src/shared_types.ts | 19 +- 7 files changed, 235 insertions(+), 140 deletions(-) create mode 100644 extensions/void/src/findDiffs.ts delete mode 100644 extensions/void/src/getDiffedLines.ts diff --git a/extensions/void/package-lock.json b/extensions/void/package-lock.json index 2f929277..0bc6d47d 100644 --- a/extensions/void/package-lock.json +++ b/extensions/void/package-lock.json @@ -9,11 +9,13 @@ "version": "0.0.1", "dependencies": { "@anthropic-ai/sdk": "^0.27.1", + "diff-match-patch": "^1.0.5", "openai": "^4.57.0" }, "devDependencies": { "@eslint/js": "^9.9.1", "@types/diff": "^5.2.2", + "@types/diff-match-patch": "^1.0.36", "@types/jest": "^29.5.12", "@types/mocha": "^10.0.8", "@types/node": "^22.5.1", @@ -1001,6 +1003,12 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/diff-match-patch": { + "version": "1.0.36", + "resolved": "https://registry.npmjs.org/@types/diff-match-patch/-/diff-match-patch-1.0.36.tgz", + "integrity": "sha512-xFdR6tkm0MWvBfO8xXCSsinYxHcqkQUlcHeSpMC2ukzOb6lwQAfDmW+Qt0AvlGd8HpsS28qKsB+oPeJn9I39jg==", + "dev": true + }, "node_modules/@types/estree": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", @@ -2546,6 +2554,11 @@ "node": ">=0.3.1" } }, + "node_modules/diff-match-patch": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/diff-match-patch/-/diff-match-patch-1.0.5.tgz", + "integrity": "sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw==" + }, "node_modules/diff-sequences": { "version": "29.6.3", "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", diff --git a/extensions/void/package.json b/extensions/void/package.json index 809329dd..ee04b8fe 100644 --- a/extensions/void/package.json +++ b/extensions/void/package.json @@ -135,6 +135,7 @@ "devDependencies": { "@eslint/js": "^9.9.1", "@types/diff": "^5.2.2", + "@types/diff-match-patch": "^1.0.36", "@types/jest": "^29.5.12", "@types/mocha": "^10.0.8", "@types/node": "^22.5.1", @@ -165,6 +166,7 @@ }, "dependencies": { "@anthropic-ai/sdk": "^0.27.1", + "diff-match-patch": "^1.0.5", "openai": "^4.57.0" } } diff --git a/extensions/void/src/DisplayChangesProvider.ts b/extensions/void/src/DisplayChangesProvider.ts index 6099326e..7b83a359 100644 --- a/extensions/void/src/DisplayChangesProvider.ts +++ b/extensions/void/src/DisplayChangesProvider.ts @@ -1,6 +1,6 @@ import * as vscode from 'vscode'; -import { getDiffedLines, SuggestedDiff } from './getDiffedLines'; -import { Diff, DiffArea } from './shared_types'; +import { findDiffs } from './findDiffs'; +import { Diff, DiffArea, DiffBlock } from './shared_types'; @@ -142,7 +142,7 @@ export class DisplayChangesProvider implements vscode.CodeLensProvider { 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 = getDiffedLines(diffArea.originalCode, currentCode) + const diffs = findDiffs(diffArea.originalCode, currentCode) // print diffs console.log('!CODEBefore:', JSON.stringify(diffArea.originalCode)) @@ -152,15 +152,27 @@ export class DisplayChangesProvider implements vscode.CodeLensProvider { this.addDiffs(editor.document.uri, diffs) for (const diff of this._diffsOfDocument[docUriStr]) { - console.log('originalCodeDiff:', JSON.stringify(diff.originalCode)) - console.log('greenCodeDiff:', JSON.stringify(editor.document.getText(diff.greenRange).replace(/\r\n/g, '\n'))) + console.log('------------') + console.log('deletedCode:', JSON.stringify(diff.deletedCode)) + console.log('insertedCode:', JSON.stringify(diff.insertedCode)) + console.log('deletedRange:', diff.deletedRange.start.line, diff.deletedRange.end.line,) + console.log('insertedRange:', diff.insertedRange.start.line, diff.insertedRange.end.line,) } } - // update highlighting - editor.setDecorations(greenDecoration, this._diffsOfDocument[docUriStr].map(diff => diff.greenRange)) + // update green highlighting + editor.setDecorations( + greenDecoration, + (this._diffsOfDocument[docUriStr] + .filter(diff => diff.insertedRange !== undefined) + .map(diff => diff.insertedRange) + ) + ); + + // TODO update red highlighting + // this._diffsOfDocument[docUriStr].map(diff => diff.deletedCode) // update code lenses this._onDidChangeCodeLenses.fire() @@ -168,7 +180,7 @@ export class DisplayChangesProvider implements vscode.CodeLensProvider { } // used by us only - public addDiffs(docUri: vscode.Uri, diffs: SuggestedDiff[]) { + public addDiffs(docUri: vscode.Uri, diffs: DiffBlock[]) { const docUriStr = docUri.toString() @@ -176,43 +188,17 @@ export class DisplayChangesProvider implements vscode.CodeLensProvider { if (!this._diffsOfDocument[docUriStr]) this._diffsOfDocument[docUriStr] = [] - - // 1. convert suggested diffs (which are described using line numbers) into actual diffs (described using vscode.Range, vscode.Uri) - // must do this before adding codelenses or highlighting so that codelens and highlights will apply to the fresh code and not the old code - // apply changes in reverse order so additions don't push down the line numbers of the next edit - let workspaceEdit = new vscode.WorkspaceEdit(); + // add each diff and its codelens to the document for (let i = diffs.length - 1; i > -1; i -= 1) { let suggestedDiff = diffs[i] - let greenRange: vscode.Range - - // INSERTIONS (e.g. {originalStartLine: 0, originalEndLine: -1}) - if (suggestedDiff.originalStartLine > suggestedDiff.originalEndLine) { - const originalPosition = new vscode.Position(suggestedDiff.originalStartLine, 0) - workspaceEdit.insert(docUri, originalPosition, suggestedDiff.afterCode + '\n') // add back in the line we deleted when we made the startline->endline range go negative - greenRange = new vscode.Range(suggestedDiff.startLine, 0, suggestedDiff.endLine + 1, 0) - } - // DELETIONS - else if (suggestedDiff.startLine > suggestedDiff.endLine) { - const deleteRange = new vscode.Range(suggestedDiff.originalStartLine, 0, suggestedDiff.originalEndLine + 1, 0) - workspaceEdit.delete(docUri, deleteRange) - greenRange = new vscode.Range(suggestedDiff.startLine, 0, suggestedDiff.startLine, 0) - suggestedDiff.beforeCode += '\n' // add back in the line we deleted when we made the startline->endline range go negative - } - // REPLACEMENTS - else { - const originalRange = new vscode.Range(suggestedDiff.originalStartLine, 0, suggestedDiff.originalEndLine, Number.MAX_SAFE_INTEGER) - workspaceEdit.replace(docUri, originalRange, suggestedDiff.afterCode) - greenRange = new vscode.Range(suggestedDiff.startLine, 0, suggestedDiff.endLine, Number.MAX_SAFE_INTEGER) - } - this._diffsOfDocument[docUriStr].push({ + ...suggestedDiff, diffid: this._diffidPool, - greenRange: greenRange, - originalCode: suggestedDiff.beforeCode, + // originalCode: suggestedDiff.deletedText, lenses: [ - new vscode.CodeLens(greenRange, { title: 'Accept', command: 'void.acceptDiff', arguments: [{ diffid: this._diffidPool }] }), - new vscode.CodeLens(greenRange, { title: 'Reject', command: 'void.rejectDiff', arguments: [{ diffid: this._diffidPool }] }) + new vscode.CodeLens(suggestedDiff.insertedRange, { title: 'Accept', command: 'void.acceptDiff', arguments: [{ diffid: this._diffidPool }] }), + new vscode.CodeLens(suggestedDiff.insertedRange, { title: 'Reject', command: 'void.rejectDiff', arguments: [{ diffid: this._diffidPool }] }) ] }); this._diffidPool += 1 @@ -261,17 +247,17 @@ export class DisplayChangesProvider implements vscode.CodeLensProvider { return } - const { greenRange: range, lenses, originalCode } = this._diffsOfDocument[docUriStr][index] // do this before we splice and mess up index + const { insertedRange: range, lenses, deletedCode } = this._diffsOfDocument[docUriStr][index] // do this before we splice and mess up index // remove this diff from the diffsOfDocument[docStr] (can change this behavior in future if add something like history) this._diffsOfDocument[docUriStr].splice(index, 1) // clear the decoration in this diffs range - editor.setDecorations(greenDecoration, this._diffsOfDocument[docUriStr].map(diff => diff.greenRange)) + // editor.setDecorations(greenDecoration, this._diffsOfDocument[docUriStr].map(diff => diff.insertionRange)) // REVERT THE CHANGE (this is the only part that's different from acceptDiff) let workspaceEdit = new vscode.WorkspaceEdit(); - workspaceEdit.replace(docUri, range, originalCode); + // workspaceEdit.replace(docUri, range, deletedCode); this._weAreEditing = true await vscode.workspace.applyEdit(workspaceEdit) await vscode.workspace.save(docUri) diff --git a/extensions/void/src/extension.ts b/extensions/void/src/extension.ts index d2da0238..21cc3dfb 100644 --- a/extensions/void/src/extension.ts +++ b/extensions/void/src/extension.ts @@ -57,7 +57,7 @@ export function activate(context: vscode.ExtensionContext) { // vscode.commands.executeCommand('vscode.moveViewToPanel', CustomViewProvider.viewId); // move to aux bar // get the text the user is selecting - const selectionStr = editor.document.getText(editor.selection); + const selectionStr = editor.document.getText(editor.selection);5 // get the range of the selection const selectionRange = editor.selection; diff --git a/extensions/void/src/findDiffs.ts b/extensions/void/src/findDiffs.ts new file mode 100644 index 00000000..5e456f85 --- /dev/null +++ b/extensions/void/src/findDiffs.ts @@ -0,0 +1,176 @@ + +import * as vscode from 'vscode'; +// import { diffLines, Change } from 'diff'; +import { DiffBlock } from './shared_types'; + +import { diff_match_patch } from 'diff-match-patch'; + + +const diffLines = (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 findDiffs = (oldText: string, newText: string): DiffBlock[] => { + + const diffs = diffLines(oldText, newText); + + const blocks: DiffBlock[] = []; + 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('\n'), + deletedCode: deletedBlock.join('\n'), + insertedCode: insertedBlock.join('\n'), + deletedRange: new vscode.Range(deletedStart, 0, deletedLine, Number.MAX_SAFE_INTEGER), + insertedRange: new vscode.Range(insertedStart, 0, insertedLine, Number.MAX_SAFE_INTEGER), + }); + } + + return blocks; +}; + + + +// export const findDiffs = (oldText: string, newText: string): DiffBlock[] => { + +// const diffs = diffLines(oldText, newText); + +// const blocks: DiffBlock[] = []; + +// let reprBlock: string[] = []; +// let deletedBlock: string[] = []; +// let insertedBlock: string[] = []; + +// let insertedEnd = 0; +// let deletedEnd = 0; +// let insertedStart = 0; +// let deletedStart = 0; + +// diffs.forEach(part => { + +// part.count = part.count ?? 0 + +// // if the part is an addition or deletion, add it to the current block +// if (part.added || part.removed) { +// if (reprBlock.length === 0) { reprBlock.push('@@@@'); } +// if (part.added) { +// if (insertedBlock.length === 0) insertedStart = insertedEnd; +// insertedEnd += part.count +// insertedBlock.push(part.value); +// reprBlock.push(part.value.split('\n').map(line => `+ ${line}`).join('\n')); +// } +// if (part.removed) { +// if (deletedBlock.length === 0) deletedStart = deletedEnd; +// deletedEnd += part.count +// deletedBlock.push(part.value); +// reprBlock.push(part.value.split('\n').map(line => `- ${line}`).join('\n')); +// } +// } + +// // if the part is unchanged, finalize the block and add it to the array +// else { +// // if the block is not null, add it to the array +// if (insertedBlock.length > 0 || deletedBlock.length > 0) { +// blocks.push({ +// code: reprBlock.join('\n'), +// deletedCode: deletedBlock.join(''), +// insertedCode: insertedBlock.join(''), +// deletedRange: new vscode.Range(deletedStart, 0, deletedEnd, Number.MAX_SAFE_INTEGER), +// insertedRange: new vscode.Range(insertedStart, 0, insertedEnd, Number.MAX_SAFE_INTEGER), +// }); +// } + +// // update block variables +// reprBlock = []; +// deletedBlock = []; +// insertedBlock = []; +// insertedEnd += part.count; +// deletedEnd += part.count; + +// } + +// }) + +// // finally, add the last block to the array +// if (insertedBlock.length > 0 || deletedBlock.length > 0) { +// blocks.push({ +// code: reprBlock.join('\n'), +// deletedCode: deletedBlock.join(''), +// insertedCode: insertedBlock.join(''), +// deletedRange: new vscode.Range(deletedStart, 0, deletedEnd, Number.MAX_SAFE_INTEGER), +// insertedRange: new vscode.Range(insertedStart, 0, insertedEnd, Number.MAX_SAFE_INTEGER), +// }); +// } + +// return blocks; + +// } + diff --git a/extensions/void/src/getDiffedLines.ts b/extensions/void/src/getDiffedLines.ts deleted file mode 100644 index e16f56c9..00000000 --- a/extensions/void/src/getDiffedLines.ts +++ /dev/null @@ -1,93 +0,0 @@ -import { diffLines, Change } from 'diff'; - -export type SuggestedDiff = { - // start/end of current file - startLine: number, - endLine: number, - - // start/end of original file - originalStartLine: number, - originalEndLine: number, - - // original content (originalfile[originalStart...originalEnd]) - beforeCode: string; - afterCode: string; -} - -export function getDiffedLines(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) - - // replace \r\n with \n - oldStr = oldStr.replace(/\r\n/g, '\n') - newStr = newStr.replace(/\r\n/g, '\n') - - 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) - - 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: SuggestedDiff[] = [] - - for (let line of lineByLineChanges) { - // no change on this line - if (!line.added && !line.removed) { - // if we were on a streak, add it - if (streakStartInNewFile !== undefined) { - - const startLine = streakStartInNewFile - const endLine = newFileLineNum - 1 // don't include current line, the edit was up to this line but not including it - const newContent = newStrLines.slice(startLine, endLine + 1).join('\n') - - const originalStartLine = streakStartInOldFile! - const originalEndLine = oldFileLineNum - 1 // don't include current line, the edit was up to this line but not including it - const originalContent = oldStrLines.slice(originalStartLine, originalEndLine + 1).join('\n') - - const replacement: SuggestedDiff = { beforeCode: originalContent, afterCode: newContent, startLine, endLine, originalStartLine, originalEndLine, } - - 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 - - return replacements - -} diff --git a/extensions/void/src/shared_types.ts b/extensions/void/src/shared_types.ts index f16fa3f5..0c8dc933 100644 --- a/extensions/void/src/shared_types.ts +++ b/extensions/void/src/shared_types.ts @@ -2,6 +2,8 @@ import * as vscode from 'vscode'; import { ApiConfig } from './common/sendLLMMessage'; + + // a selection is a frozen snapshot type CodeSelection = { selectionStr: string, selectionRange: vscode.Range, filePath: vscode.Uri } @@ -14,13 +16,20 @@ type DiffArea = { originalCode: string } -// each diff on the user's screen right now +// the return type of diff creator +type DiffBlock = { + code: string; + deletedRange: vscode.Range; + deletedCode: string; + insertedRange: vscode.Range; + insertedCode: string; +} + +// each diff on the user's screen type Diff = { diffid: number, lenses: vscode.CodeLens[], - greenRange: vscode.Range, - originalCode: string, // If a revert happens, we replace the greenRange with this content. -} +} & DiffBlock type WebviewMessage = ( @@ -44,9 +53,11 @@ type WebviewMessage = ( ) + type Command = WebviewMessage['type'] export { + DiffBlock, CodeSelection, File, WebviewMessage, From 20056f209edc22e9b3bfdecf5c8a6157f24f12d2 Mon Sep 17 00:00:00 2001 From: Mathew P Date: Sun, 13 Oct 2024 17:47:13 -0700 Subject: [PATCH 08/10] Fix accept/reject diff --- extensions/void/src/DisplayChangesProvider.ts | 118 ++++++++++++------ extensions/void/src/extension.ts | 14 +-- extensions/void/src/findDiffs.ts | 6 +- extensions/void/src/shared_types.ts | 27 ++-- 4 files changed, 109 insertions(+), 56 deletions(-) diff --git a/extensions/void/src/DisplayChangesProvider.ts b/extensions/void/src/DisplayChangesProvider.ts index 7b83a359..4166d5b4 100644 --- a/extensions/void/src/DisplayChangesProvider.ts +++ b/extensions/void/src/DisplayChangesProvider.ts @@ -1,6 +1,6 @@ import * as vscode from 'vscode'; import { findDiffs } from './findDiffs'; -import { Diff, DiffArea, DiffBlock } from './shared_types'; +import { Diff, BaseDiffArea, BaseDiff, DiffArea } from './shared_types'; @@ -17,6 +17,7 @@ export class DisplayChangesProvider implements vscode.CodeLensProvider { private _diffAreasOfDocument: { [docUriStr: string]: DiffArea[] } = {} private _diffsOfDocument: { [docUriStr: string]: Diff[] } = {} + private _diffareaidPool = 0 private _diffidPool = 0 private _weAreEditing: boolean = false @@ -37,6 +38,7 @@ export class DisplayChangesProvider implements vscode.CodeLensProvider { // this acts as a useEffect. Every time text changes, clear the diffs in this editor vscode.workspace.onDidChangeTextDocument((e) => { + const editor = vscode.window.activeTextEditor if (!editor) @@ -97,7 +99,7 @@ export class DisplayChangesProvider implements vscode.CodeLensProvider { // used by us only - public addDiffArea(uri: vscode.Uri, diffArea: DiffArea) { + public addDiffArea(uri: vscode.Uri, diffArea: BaseDiffArea) { const uriStr = uri.toString() @@ -107,16 +109,20 @@ export class DisplayChangesProvider implements vscode.CodeLensProvider { // remove all diffAreas that the new `diffArea` is overlapping with this._diffAreasOfDocument[uriStr] = this._diffAreasOfDocument[uriStr].filter(da => { - // condition for no overlap + const noOverlap = da.startLine > diffArea.endLine || da.endLine < diffArea.startLine - // if there is overlap (ie there is `not noOverlap`), remove `da` + if (!noOverlap) return false + return true }) // add `diffArea` to storage - this._diffAreasOfDocument[uriStr].push(diffArea) - + this._diffAreasOfDocument[uriStr].push({ + ...diffArea, + diffareaid: this._diffareaidPool + }) + this._diffareaidPool += 1 } @@ -149,7 +155,7 @@ export class DisplayChangesProvider implements vscode.CodeLensProvider { console.log('!CODEAfter:', JSON.stringify(currentCode)) // add the diffs to `this._diffsOfDocument[docUriStr]` - this.addDiffs(editor.document.uri, diffs) + this.addDiffs(editor.document.uri, diffs, diffArea) for (const diff of this._diffsOfDocument[docUriStr]) { console.log('------------') @@ -180,7 +186,7 @@ export class DisplayChangesProvider implements vscode.CodeLensProvider { } // used by us only - public addDiffs(docUri: vscode.Uri, diffs: DiffBlock[]) { + public addDiffs(docUri: vscode.Uri, diffs: BaseDiff[], diffArea: DiffArea) { const docUriStr = docUri.toString() @@ -197,8 +203,8 @@ export class DisplayChangesProvider implements vscode.CodeLensProvider { diffid: this._diffidPool, // originalCode: suggestedDiff.deletedText, lenses: [ - new vscode.CodeLens(suggestedDiff.insertedRange, { title: 'Accept', command: 'void.acceptDiff', arguments: [{ diffid: this._diffidPool }] }), - new vscode.CodeLens(suggestedDiff.insertedRange, { title: 'Reject', command: 'void.rejectDiff', arguments: [{ diffid: this._diffidPool }] }) + new vscode.CodeLens(suggestedDiff.insertedRange, { title: 'Accept', command: 'void.acceptDiff', arguments: [{ diffid: this._diffidPool, diffareaid: diffArea.diffareaid }] }), + new vscode.CodeLens(suggestedDiff.insertedRange, { title: 'Reject', command: 'void.rejectDiff', arguments: [{ diffid: this._diffidPool, diffareaid: diffArea.diffareaid }] }) ] }); this._diffidPool += 1 @@ -207,7 +213,7 @@ export class DisplayChangesProvider implements vscode.CodeLensProvider { } // called on void.acceptDiff - public async acceptDiff({ diffid }: { diffid: number }) { + public async acceptDiff({ diffid, diffareaid }: { diffid: number, diffareaid: number }) { const editor = vscode.window.activeTextEditor if (!editor) return @@ -216,54 +222,94 @@ export class DisplayChangesProvider implements vscode.CodeLensProvider { const docUri = editor.document.uri const docUriStr = docUri.toString() - // get index of this diff in diffsOfDocument - const index = this._diffsOfDocument[docUriStr].findIndex(diff => diff.diffid === diffid); - if (index === -1) { - console.error('Error: DiffID could not be found: ', diffid, this._diffsOfDocument[docUriStr]) - return + // get relevant diff + // TODO speed up with hashmap + 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; } - // remove this diff from the diffsOfDocument[docStr] (can change this behavior in future if add something like history) - this._diffsOfDocument[docUriStr].splice(index, 1) + // get relevant diffArea + 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; + } - // refresh + const diff = this._diffsOfDocument[docUriStr][diffIdx] + const diffArea = this._diffAreasOfDocument[docUriStr][diffareaIdx] + + // replace `originalCode[diff.deletedRange]` with diff.insertedCode + // TODO add a history event to undo this change + const originalLines = diffArea.originalCode.split('\n'); + const relativeStart = diff.deletedRange.start.line - diffArea.originalStartLine + const relativeEnd = diff.deletedRange.end.line - diffArea.originalStartLine + diffArea.originalCode = [ + ...originalLines.slice(0, relativeStart), // lines before the deleted range + ...diff.insertedCode.split('\n'), // inserted lines + ...originalLines.slice(relativeEnd + 1) // lines after the deleted range + ].join('\n') + + // if the diffArea has no changes, remove it + const currentDiffAreaCode = editor.document.getText() + .replace(/\r\n/g, '\n') + .split('\n') + .slice(diffArea.startLine, diffArea.endLine + 1) + .join('\n') + if (diffArea.originalCode === currentDiffAreaCode) { // if the currentDiffAreaCode === diffArea.originalCode, remove the diffArea + const index = this._diffAreasOfDocument[docUriStr].findIndex(da => da.diffareaid === diffArea.diffareaid) + this._diffAreasOfDocument[docUriStr].splice(index, 1) + } + + // refresh the diff area this.refreshDiffAreas(docUri) } // called on void.rejectDiff - public async rejectDiff({ diffid }: { diffid: number }) { + public async rejectDiff({ diffid, diffareaid }: { diffid: number, diffareaid: number }) { const editor = vscode.window.activeTextEditor if (!editor) return + // get document uri const docUri = editor.document.uri const docUriStr = docUri.toString() - // get index of this diff in diffsOfDocument - const index = this._diffsOfDocument[docUriStr].findIndex(diff => diff.diffid === diffid); - if (index === -1) { - console.error('Void error: DiffID could not be found: ', diffid, this._diffsOfDocument[docUriStr]) - return + // get relevant diff + // TODO speed up with hashmap + 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 { insertedRange: range, lenses, deletedCode } = this._diffsOfDocument[docUriStr][index] // do this before we splice and mess up index + // get relevant diffArea + 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; + } - // remove this diff from the diffsOfDocument[docStr] (can change this behavior in future if add something like history) - this._diffsOfDocument[docUriStr].splice(index, 1) + const diff = this._diffsOfDocument[docUriStr][diffIdx] + const diffArea = this._diffAreasOfDocument[docUriStr][diffareaIdx] - // clear the decoration in this diffs range - // editor.setDecorations(greenDecoration, this._diffsOfDocument[docUriStr].map(diff => diff.insertionRange)) - - // REVERT THE CHANGE (this is the only part that's different from acceptDiff) - let workspaceEdit = new vscode.WorkspaceEdit(); - // workspaceEdit.replace(docUri, range, deletedCode); + // replace `editorCode[diff.insertedRange]` with diff.deletedCode + const workspaceEdit = new vscode.WorkspaceEdit(); + workspaceEdit.replace(docUri, diff.insertedRange, diff.deletedCode) this._weAreEditing = true await vscode.workspace.applyEdit(workspaceEdit) - await vscode.workspace.save(docUri) this._weAreEditing = false - // refresh + // if the diffArea has no changes, remove it + const currentDiffAreaCode = editor.document.getText() + .replace(/\r\n/g, '\n') + .split('\n') + .slice(diffArea.startLine, diffArea.endLine + 1) + .join('\n') + if (diffArea.originalCode === currentDiffAreaCode) { // if the currentDiffAreaCode === diffArea.originalCode, remove the diffArea + const index = this._diffAreasOfDocument[docUriStr].findIndex(da => da.diffareaid === diffArea.diffareaid) + this._diffAreasOfDocument[docUriStr].splice(index, 1) + } + + // refresh the diff area this.refreshDiffAreas(docUri) } } diff --git a/extensions/void/src/extension.ts b/extensions/void/src/extension.ts index 21cc3dfb..aa5a69d1 100644 --- a/extensions/void/src/extension.ts +++ b/extensions/void/src/extension.ts @@ -1,5 +1,5 @@ import * as vscode from 'vscode'; -import { DiffArea, WebviewMessage } from './shared_types'; +import { BaseDiffArea, WebviewMessage } from './shared_types'; import { CtrlKCodeLensProvider } from './CtrlKCodeLensProvider'; import { DisplayChangesProvider } from './DisplayChangesProvider'; import { SidebarWebviewProvider } from './SidebarWebviewProvider'; @@ -57,7 +57,7 @@ export function activate(context: vscode.ExtensionContext) { // vscode.commands.executeCommand('vscode.moveViewToPanel', CustomViewProvider.viewId); // move to aux bar // get the text the user is selecting - const selectionStr = editor.document.getText(editor.selection);5 + const selectionStr = editor.document.getText(editor.selection); 5 // get the range of the selection const selectionRange = editor.selection; @@ -124,9 +124,11 @@ export function activate(context: vscode.ExtensionContext) { } // create an area to show diffs - const diffArea: DiffArea = { + const diffArea: BaseDiffArea = { startLine: 0, // in ctrl+L the start and end lines are the full document endLine: editor.document.lineCount, + originalStartLine: 0, + originalEndLine: editor.document.lineCount, originalCode: await readFileContentOfUri(editor.document.uri), } displayChangesProvider.addDiffArea(editor.document.uri, diffArea) @@ -139,12 +141,10 @@ export function activate(context: vscode.ExtensionContext) { // await vscode.workspace.save(docUri) // this._weAreEditing = false await editor.edit(editBuilder => { - editBuilder.replace(new vscode.Range(diffArea.startLine, 0, diffArea.endLine, 0), m.code); + editBuilder.replace(new vscode.Range(diffArea.startLine, 0, diffArea.endLine, Number.MAX_SAFE_INTEGER), m.code); }); - - - // rediff the changes based on the diffArea (start, end, original code, [current code]) + // rediff the changes based on the diffAreas displayChangesProvider.refreshDiffAreas(editor.document.uri) } diff --git a/extensions/void/src/findDiffs.ts b/extensions/void/src/findDiffs.ts index 5e456f85..847e16f8 100644 --- a/extensions/void/src/findDiffs.ts +++ b/extensions/void/src/findDiffs.ts @@ -1,7 +1,7 @@ import * as vscode from 'vscode'; // import { diffLines, Change } from 'diff'; -import { DiffBlock } from './shared_types'; +import { BaseDiff } from './shared_types'; import { diff_match_patch } from 'diff-match-patch'; @@ -20,11 +20,11 @@ const diffLines = (text1: string, text2: string) => { // TODO use a better diff algorithm -export const findDiffs = (oldText: string, newText: string): DiffBlock[] => { +export const findDiffs = (oldText: string, newText: string): BaseDiff[] => { const diffs = diffLines(oldText, newText); - const blocks: DiffBlock[] = []; + const blocks: BaseDiff[] = []; let reprBlock: string[] = []; let deletedBlock: string[] = []; let insertedBlock: string[] = []; diff --git a/extensions/void/src/shared_types.ts b/extensions/void/src/shared_types.ts index 0c8dc933..25843aaa 100644 --- a/extensions/void/src/shared_types.ts +++ b/extensions/void/src/shared_types.ts @@ -10,18 +10,25 @@ type CodeSelection = { selectionStr: string, selectionRange: vscode.Range, fileP type File = { filepath: vscode.Uri, content: string } // an area that is currently being diffed -type DiffArea = { - startLine: number, - endLine: number, - originalCode: string +type BaseDiffArea = { + // use `startLine` and `endLine` instead of `range` for mutibility + // bounds are relative to the file, inclusive + startLine: number; + endLine: number; + originalStartLine: number, + originalEndLine: number, + originalCode: string, // the original chunk of code (not necessarily the whole file) + // `newCode: string,` is not included because it is the code in the actual file, `document.text()[startline: endLine + 1]` } +type DiffArea = BaseDiffArea & { diffareaid: number } + // the return type of diff creator -type DiffBlock = { - code: string; - deletedRange: vscode.Range; - deletedCode: string; +type BaseDiff = { + code: string; // representation of the diff in text + deletedRange: vscode.Range; // relative to the file, inclusive insertedRange: vscode.Range; + deletedCode: string; insertedCode: string; } @@ -29,7 +36,7 @@ type DiffBlock = { type Diff = { diffid: number, lenses: vscode.CodeLens[], -} & DiffBlock +} & BaseDiff type WebviewMessage = ( @@ -57,7 +64,7 @@ type WebviewMessage = ( type Command = WebviewMessage['type'] export { - DiffBlock, + BaseDiff, BaseDiffArea, CodeSelection, File, WebviewMessage, From 33c3ef675171457f79a23be96d3c5bf5bcc2b410 Mon Sep 17 00:00:00 2001 From: Mathew P Date: Sun, 13 Oct 2024 22:05:10 -0700 Subject: [PATCH 09/10] Fix newline bug --- extensions/void/src/findDiffs.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/extensions/void/src/findDiffs.ts b/extensions/void/src/findDiffs.ts index 847e16f8..eafa5143 100644 --- a/extensions/void/src/findDiffs.ts +++ b/extensions/void/src/findDiffs.ts @@ -86,9 +86,9 @@ export const findDiffs = (oldText: string, newText: string): BaseDiff[] => { // Add any remaining blocks after the loop ends if (insertedBlock.length > 0 || deletedBlock.length > 0) { blocks.push({ - code: reprBlock.join('\n'), - deletedCode: deletedBlock.join('\n'), - insertedCode: insertedBlock.join('\n'), + 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), }); From 2309fc53fb9f10da9309e841d59cbd9d6db14ab9 Mon Sep 17 00:00:00 2001 From: mp Date: Mon, 14 Oct 2024 20:37:45 -0700 Subject: [PATCH 10/10] Merge dynamic diff features --- extensions/void/src/common/sendLLMMessage.ts | 9 +- extensions/void/src/extension.ts | 5 +- extensions/void/src/findDiffs.ts | 101 +++++++++++++++++++ extensions/void/src/sidebar/Sidebar.tsx | 11 -- 4 files changed, 104 insertions(+), 22 deletions(-) diff --git a/extensions/void/src/common/sendLLMMessage.ts b/extensions/void/src/common/sendLLMMessage.ts index c0e1a8b4..9e47a80a 100644 --- a/extensions/void/src/common/sendLLMMessage.ts +++ b/extensions/void/src/common/sendLLMMessage.ts @@ -64,14 +64,9 @@ const sendClaudeMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinal const anthropic = new Anthropic({ apiKey: apiConfig.anthropic.apikey, dangerouslyAllowBrowser: true }); // defaults to process.env["ANTHROPIC_API_KEY"] - console.log('max_tokens:' + apiConfig.anthropic.maxTokens) - - let max_tokens = parseInt(apiConfig.anthropic.maxTokens) - if (isNaN(max_tokens)) { max_tokens = 4000 } // TODO make a default max_tokens - const stream = anthropic.messages.stream({ model: apiConfig.anthropic.model, - max_tokens: max_tokens, + max_tokens: parseInt(apiConfig.anthropic.maxTokens), messages: messages, }); @@ -226,8 +221,6 @@ const sendGreptileMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFin export const sendLLMMessage: SendLLMMessageFnTypeExternal = ({ messages, onText, onFinalMessage, apiConfig }) => { if (!apiConfig) return { abort: () => { } } - console.log(`void: sending LLMMessage to ${apiConfig.whichApi}`) - const whichApi = apiConfig.whichApi if (whichApi === 'anthropic') { diff --git a/extensions/void/src/extension.ts b/extensions/void/src/extension.ts index aa5a69d1..2a89a9e9 100644 --- a/extensions/void/src/extension.ts +++ b/extensions/void/src/extension.ts @@ -7,7 +7,7 @@ import { ApiConfig } from './common/sendLLMMessage'; const readFileContentOfUri = async (uri: vscode.Uri) => { return Buffer.from(await vscode.workspace.fs.readFile(uri)).toString('utf8') - .replace(/\r\n/g, '\n') // must remove windows \r or every line will appear different because of it + .replace(/\r\n/g, '\n') // replace windows \r\n with \n } @@ -57,7 +57,7 @@ export function activate(context: vscode.ExtensionContext) { // vscode.commands.executeCommand('vscode.moveViewToPanel', CustomViewProvider.viewId); // move to aux bar // get the text the user is selecting - const selectionStr = editor.document.getText(editor.selection); 5 + const selectionStr = editor.document.getText(editor.selection); // get the range of the selection const selectionRange = editor.selection; @@ -72,7 +72,6 @@ export function activate(context: vscode.ExtensionContext) { // 3. Show an approve/reject codelens above each change const displayChangesProvider = new DisplayChangesProvider(); - console.log(`void: Creating DisplayChangesProvider`) context.subscriptions.push(vscode.languages.registerCodeLensProvider('*', displayChangesProvider)); // 4. Add approve/reject commands diff --git a/extensions/void/src/findDiffs.ts b/extensions/void/src/findDiffs.ts index eafa5143..ee432200 100644 --- a/extensions/void/src/findDiffs.ts +++ b/extensions/void/src/findDiffs.ts @@ -174,3 +174,104 @@ export const findDiffs = (oldText: string, newText: string): BaseDiff[] => { // } + + + + + + + + + + +// import { diffLines, Change } from 'diff'; + +// export type SuggestedEdit = { +// // start/end of current file +// startLine: number; +// endLine: number; + +// // start/end of original file +// originalStartLine: number, +// originalEndLine: number, + +// // original content (originalfile[originalStart...originalEnd]) +// originalContent: string; +// newContent: string; +// } + +// export function getDiffedLines(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); +// console.debug('Line by line changes', lineByLineChanges) + +// lineByLineChanges.push({ value: '' }) // 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: SuggestedEdit[] = [] + +// for (let line of lineByLineChanges) { +// // no change on this line +// if (!line.added && !line.removed) { +// // if we were on a streak, add it +// if (streakStartInNewFile !== undefined) { + +// const startLine = streakStartInNewFile +// const endLine = newFileLineNum - 1 // don't include current line, the edit was up to this line but not including it +// const newContent = newStrLines.slice(startLine, endLine + 1).join('\n') + +// const originalStartLine = streakStartInOldFile! +// const originalEndLine = oldFileLineNum - 1 // don't include current line, the edit was up to this line but not including it +// const originalContent = oldStrLines.slice(originalStartLine, originalEndLine + 1).join('\n') + +// const replacement: SuggestedEdit = { startLine, endLine, newContent, originalStartLine, originalEndLine, 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 + +// } \ No newline at end of file diff --git a/extensions/void/src/sidebar/Sidebar.tsx b/extensions/void/src/sidebar/Sidebar.tsx index cfa8d3a7..82e633c1 100644 --- a/extensions/void/src/sidebar/Sidebar.tsx +++ b/extensions/void/src/sidebar/Sidebar.tsx @@ -186,13 +186,10 @@ const Sidebar = () => { const formRef = useRef(null) - const onSubmit = async (e: FormEvent) => { - console.log(`11111`) e.preventDefault() if (isLoading) return - console.log(`2222222`) setIsLoading(true) setInstructions(''); @@ -200,14 +197,9 @@ const Sidebar = () => { setSelection(null) setFiles([]) - - console.log(`AAAAAA`) - // request file content from vscode and await response getVSCodeAPI().postMessage({ type: 'requestFiles', filepaths: files }) - console.log(`BBBBB`) const relevantFiles = await awaitVSCodeResponse('files') - console.log(`CCCCCC`) // add message to chat history const content = userInstructionsStr(instructions, relevantFiles.files, selection) @@ -216,7 +208,6 @@ const Sidebar = () => { setChatHistory(chatMessageHistory => [...chatMessageHistory, newHistoryElt]) // send message to LLM - console.log(`DDDDD`) let { abort } = sendLLMMessage({ messages: [...chatMessageHistory.map(m => ({ role: m.role, content: m.content })), { role: 'user', content }], onText: (newText, fullText) => setMessageStream(fullText), @@ -232,9 +223,7 @@ const Sidebar = () => { }, apiConfig: apiConfig }) - console.log(`EEEEE`) abortFnRef.current = abort - console.log(`FFFF`) }