registerInlineDiffs progress

This commit is contained in:
Andrew Pareles 2024-11-13 03:25:18 -08:00
parent a5912ba538
commit ee72a2c3cd
11 changed files with 622 additions and 677 deletions

View file

@ -1,15 +1,8 @@
import * as vscode from 'vscode'; import * as vscode from 'vscode';
import { findDiffs } from './src/extension/findDiffs'; import { findDiffs } from './findDiffs';
import { throttle } from 'lodash'; import { DiffArea, Diff } from '../common/shared_types';
import { DiffArea, BaseDiff, Diff } from '../common/shared_types';
import { readFileContentOfUri } from './src/extension/extensionLib/readFileContentOfUri';
import { AbortRef, sendLLMMessage } from '../common/sendLLMMessage';
import { writeFileWithDiffInstructions } from '../common/systemPrompts';
import { VoidConfig } from './src/webviews/common/contextForConfig';
const THROTTLE_TIME = 100
// TODO in theory this should be disposed // TODO in theory this should be disposed
const lightGrayDecoration = vscode.window.createTextEditorDecorationType({ const lightGrayDecoration = vscode.window.createTextEditorDecorationType({
backgroundColor: 'rgba(218 218 218 / .2)', backgroundColor: 'rgba(218 218 218 / .2)',
@ -48,52 +41,52 @@ export class DiffProvider implements vscode.CodeLensProvider {
console.log('Creating DisplayChangesProvider') console.log('Creating DisplayChangesProvider')
// this acts as a useEffect every time text changes // // this acts as a useEffect every time text changes
vscode.workspace.onDidChangeTextDocument((e) => { // vscode.workspace.onDidChangeTextDocument((e) => {
const editor = vscode.window.activeTextEditor // const editor = vscode.window.activeTextEditor
if (!editor) return // if (!editor) return
const docUriStr = editor.document.uri.toString() // const docUriStr = editor.document.uri.toString()
const changes = e.contentChanges.map(c => ({ startLine: c.range.start.line, endLine: c.range.end.line, text: c.text, })) // const changes = e.contentChanges.map(c => ({ startLine: c.range.start.line, endLine: c.range.end.line, text: c.text, }))
// on user change, grow/shrink/merge/delete diff areas // // on user change, grow/shrink/merge/delete diff areas
this.resizeDiffAreas(docUriStr, changes, 'currentFile') // this.resizeDiffAreas(docUriStr, changes, 'currentFile')
// refresh the diffAreas // // refresh the diffAreas
this.refreshStylesAndDiffs(docUriStr) // this.refreshStylesAndDiffs(docUriStr)
}) // })
} }
// used by us only // used by us only
public createDiffArea(uri: vscode.Uri, partialDiffArea: Omit<DiffArea, 'diffareaid'>, originalFile: string) { // public createDiffArea(uri: vscode.Uri, partialDiffArea: Omit<DiffArea, 'diffareaid'>, originalFile: string) {
const uriStr = uri.toString() // const uriStr = uri.toString()
this._originalFileOfDocument[uriStr] = originalFile // this._originalFileOfDocument[uriStr] = originalFile
// make sure array is defined // // make sure array is defined
if (!this._diffAreasOfDocument[uriStr]) this._diffAreasOfDocument[uriStr] = [] // if (!this._diffAreasOfDocument[uriStr]) this._diffAreasOfDocument[uriStr] = []
// remove all diffAreas that the new `diffArea` is overlapping with // // remove all diffAreas that the new `diffArea` is overlapping with
this._diffAreasOfDocument[uriStr] = this._diffAreasOfDocument[uriStr].filter(da => { // this._diffAreasOfDocument[uriStr] = this._diffAreasOfDocument[uriStr].filter(da => {
const noOverlap = da.startLine > partialDiffArea.endLine || da.endLine < partialDiffArea.startLine // const noOverlap = da.startLine > partialDiffArea.endLine || da.endLine < partialDiffArea.startLine
if (!noOverlap) return false // if (!noOverlap) return false
return true // return true
}) // })
// add `diffArea` to storage // // add `diffArea` to storage
const diffArea = { // const diffArea = {
...partialDiffArea, // ...partialDiffArea,
diffareaid: this._diffareaidPool // diffareaid: this._diffareaidPool
} // }
this._diffAreasOfDocument[uriStr].push(diffArea) // this._diffAreasOfDocument[uriStr].push(diffArea)
this._diffareaidPool += 1 // this._diffareaidPool += 1
return diffArea // return diffArea
} // }
// used by us only // used by us only
// changes the start/line locations based on the changes that were recently made. does not change any of the diffs in the diff areas // changes the start/line locations based on the changes that were recently made. does not change any of the diffs in the diff areas
@ -244,205 +237,205 @@ export class DiffProvider implements vscode.CodeLensProvider {
} }
// called on void.acceptDiff // // called on void.acceptDiff
public async acceptDiff({ diffid, diffareaid }: { diffid: number, diffareaid: number }) { // public async acceptDiff({ diffid, diffareaid }: { diffid: number, diffareaid: number }) {
const editor = vscode.window.activeTextEditor // const editor = vscode.window.activeTextEditor
if (!editor) // if (!editor)
return // return
const docUriStr = editor.document.uri.toString() // const docUriStr = editor.document.uri.toString()
const diffIdx = this._diffsOfDocument[docUriStr].findIndex(diff => diff.diffid === diffid); // 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; } // if (diffIdx === -1) { console.error('Error: DiffID could not be found: ', diffid, diffareaid, this._diffsOfDocument[docUriStr], this._diffAreasOfDocument[docUriStr]); return; }
const diffareaIdx = this._diffAreasOfDocument[docUriStr].findIndex(diff => diff.diffareaid === diffareaid); // 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; } // if (diffareaIdx === -1) { console.error('Error: DiffAreaID could not be found: ', diffid, diffareaid, this._diffsOfDocument[docUriStr], this._diffAreasOfDocument[docUriStr]); return; }
const diff = this._diffsOfDocument[docUriStr][diffIdx] // const diff = this._diffsOfDocument[docUriStr][diffIdx]
const originalFile = this._originalFileOfDocument[docUriStr] // const originalFile = this._originalFileOfDocument[docUriStr]
const currentFile = await readFileContentOfUri(editor.document.uri) // const currentFile = await readFileContentOfUri(editor.document.uri)
// Fixed: Handle newlines properly by splitting into lines and joining with proper newlines // // Fixed: Handle newlines properly by splitting into lines and joining with proper newlines
const originalLines = originalFile.split('\n'); // const originalLines = originalFile.split('\n');
const currentLines = currentFile.split('\n'); // const currentLines = currentFile.split('\n');
// Get the changed lines from current file // // Get the changed lines from current file
const changedLines = currentLines.slice(diff.range.start.line, diff.range.end.line + 1); // const changedLines = currentLines.slice(diff.range.start.line, diff.range.end.line + 1);
// Create new original file content by replacing the affected lines // // Create new original file content by replacing the affected lines
const newOriginalLines = [ // const newOriginalLines = [
...originalLines.slice(0, diff.originalRange.start.line), // ...originalLines.slice(0, diff.originalRange.start.line),
...changedLines, // ...changedLines,
...originalLines.slice(diff.originalRange.end.line + 1) // ...originalLines.slice(diff.originalRange.end.line + 1)
]; // ];
this._originalFileOfDocument[docUriStr] = newOriginalLines.join('\n'); // this._originalFileOfDocument[docUriStr] = newOriginalLines.join('\n');
// Update diff areas based on the change // // Update diff areas based on the change
this.resizeDiffAreas(docUriStr, [{ // this.resizeDiffAreas(docUriStr, [{
text: changedLines.join('\n'), // text: changedLines.join('\n'),
startLine: diff.originalRange.start.line, // startLine: diff.originalRange.start.line,
endLine: diff.originalRange.end.line // endLine: diff.originalRange.end.line
}], 'originalFile') // }], 'originalFile')
// Check if diffArea should be removed // // Check if diffArea should be removed
const diffArea = this._diffAreasOfDocument[docUriStr][diffareaIdx] // const diffArea = this._diffAreasOfDocument[docUriStr][diffareaIdx]
const currentArea = currentLines.slice(diffArea.startLine, diffArea.endLine + 1).join('\n') // const currentArea = currentLines.slice(diffArea.startLine, diffArea.endLine + 1).join('\n')
const originalArea = newOriginalLines.slice(diffArea.originalStartLine, diffArea.originalEndLine + 1).join('\n') // const originalArea = newOriginalLines.slice(diffArea.originalStartLine, diffArea.originalEndLine + 1).join('\n')
if (originalArea === currentArea) { // if (originalArea === currentArea) {
const index = this._diffAreasOfDocument[docUriStr].findIndex(da => da.diffareaid === diffArea.diffareaid) // const index = this._diffAreasOfDocument[docUriStr].findIndex(da => da.diffareaid === diffArea.diffareaid)
this._diffAreasOfDocument[docUriStr].splice(index, 1) // this._diffAreasOfDocument[docUriStr].splice(index, 1)
} // }
this.refreshStylesAndDiffs(docUriStr) // this.refreshStylesAndDiffs(docUriStr)
} // }
// called on void.rejectDiff // // called on void.rejectDiff
public async rejectDiff({ diffid, diffareaid }: { diffid: number, diffareaid: number }) { // public async rejectDiff({ diffid, diffareaid }: { diffid: number, diffareaid: number }) {
const editor = vscode.window.activeTextEditor // const editor = vscode.window.activeTextEditor
if (!editor) // if (!editor)
return // return
const docUriStr = editor.document.uri.toString() // const docUriStr = editor.document.uri.toString()
const diffIdx = this._diffsOfDocument[docUriStr].findIndex(diff => diff.diffid === diffid); // 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; } // if (diffIdx === -1) { console.error('Error: DiffID could not be found: ', diffid, diffareaid, this._diffsOfDocument[docUriStr], this._diffAreasOfDocument[docUriStr]); return; }
const diffareaIdx = this._diffAreasOfDocument[docUriStr].findIndex(diff => diff.diffareaid === diffareaid); // 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; } // if (diffareaIdx === -1) { console.error('Error: DiffAreaID could not be found: ', diffid, diffareaid, this._diffsOfDocument[docUriStr], this._diffAreasOfDocument[docUriStr]); return; }
const diff = this._diffsOfDocument[docUriStr][diffIdx] // const diff = this._diffsOfDocument[docUriStr][diffIdx]
// Apply the rejection by replacing with original code // // Apply the rejection by replacing with original code
// we don't have to edit the original or final file; just do a workspace edit so the code equals the original code // // we don't have to edit the original or final file; just do a workspace edit so the code equals the original code
const workspaceEdit = new vscode.WorkspaceEdit(); // const workspaceEdit = new vscode.WorkspaceEdit();
workspaceEdit.replace(editor.document.uri, diff.range, diff.originalCode) // workspaceEdit.replace(editor.document.uri, diff.range, diff.originalCode)
await vscode.workspace.applyEdit(workspaceEdit) // await vscode.workspace.applyEdit(workspaceEdit)
// Check if diffArea should be removed // // Check if diffArea should be removed
const originalFile = this._originalFileOfDocument[docUriStr] // const originalFile = this._originalFileOfDocument[docUriStr]
const currentFile = await readFileContentOfUri(editor.document.uri) // const currentFile = await readFileContentOfUri(editor.document.uri)
const diffArea = this._diffAreasOfDocument[docUriStr][diffareaIdx] // const diffArea = this._diffAreasOfDocument[docUriStr][diffareaIdx]
const currentLines = currentFile.split('\n'); // const currentLines = currentFile.split('\n');
const originalLines = originalFile.split('\n'); // const originalLines = originalFile.split('\n');
const currentArea = currentLines.slice(diffArea.startLine, diffArea.endLine + 1).join('\n') // const currentArea = currentLines.slice(diffArea.startLine, diffArea.endLine + 1).join('\n')
const originalArea = originalLines.slice(diffArea.originalStartLine, diffArea.originalEndLine + 1).join('\n') // const originalArea = originalLines.slice(diffArea.originalStartLine, diffArea.originalEndLine + 1).join('\n')
if (originalArea === currentArea) { // if (originalArea === currentArea) {
const index = this._diffAreasOfDocument[docUriStr].findIndex(da => da.diffareaid === diffArea.diffareaid) // const index = this._diffAreasOfDocument[docUriStr].findIndex(da => da.diffareaid === diffArea.diffareaid)
this._diffAreasOfDocument[docUriStr].splice(index, 1) // this._diffAreasOfDocument[docUriStr].splice(index, 1)
} // }
this.refreshStylesAndDiffs(docUriStr) // this.refreshStylesAndDiffs(docUriStr)
} // }
async startStreamingInDiffArea({ docUri, oldFileStr, diffRepr, diffArea, voidConfig, abortRef }: { docUri: vscode.Uri, oldFileStr: string, diffRepr: string, voidConfig: VoidConfig, diffArea: DiffArea, abortRef: AbortRef }) { // async startStreamingInDiffArea({ docUri, oldFileStr, diffRepr, diffArea, voidConfig, abortRef }: { docUri: vscode.Uri, oldFileStr: string, diffRepr: string, voidConfig: VoidConfig, diffArea: DiffArea, abortRef: AbortRef }) {
const promptContent = `\ // const promptContent = `\
ORIGINAL_FILE // ORIGINAL_FILE
\`\`\` // \`\`\`
${oldFileStr} // ${oldFileStr}
\`\`\` // \`\`\`
DIFF // DIFF
\`\`\` // \`\`\`
${diffRepr} // ${diffRepr}
\`\`\` // \`\`\`
INSTRUCTIONS // INSTRUCTIONS
Please finish writing the new file by applying the diff to the original file. Return ONLY the completion of the file, without any explanation. // Please finish writing the new file by applying the diff to the original file. Return ONLY the completion of the file, without any explanation.
` // `
// make LLM complete the file to include the diff // // make LLM complete the file to include the diff
await new Promise<void>((resolve, reject) => { // await new Promise<void>((resolve, reject) => {
sendLLMMessage({ // sendLLMMessage({
logging: { loggingName: 'streamChunk' }, // logging: { loggingName: 'streamChunk' },
messages: [ // messages: [
{ role: 'system', content: writeFileWithDiffInstructions, }, // { role: 'system', content: writeFileWithDiffInstructions, },
// TODO include more context too // // TODO include more context too
{ role: 'user', content: promptContent, } // { role: 'user', content: promptContent, }
], // ],
onText: (newText, fullText) => { // onText: (newText, fullText) => {
this._updateStream(docUri.toString(), diffArea, fullText) // this._updateStream(docUri.toString(), diffArea, fullText)
}, // },
onFinalMessage: (fullText) => { // onFinalMessage: (fullText) => {
this._updateStream(docUri.toString(), diffArea, fullText) // this._updateStream(docUri.toString(), diffArea, fullText)
resolve(); // resolve();
}, // },
onError: (e) => { // onError: (e) => {
console.error('Error rewriting file with diff', e); // console.error('Error rewriting file with diff', e);
resolve(); // resolve();
}, // },
voidConfig, // voidConfig,
abortRef, // abortRef,
}) // })
}) // })
} // }
// used by us only // // used by us only
private _updateStream = throttle(async (docUriStr: string, diffArea: DiffArea, newDiffAreaCode: string) => { // private _updateStream = throttle(async (docUriStr: string, diffArea: DiffArea, newDiffAreaCode: string) => {
const editor = vscode.window.activeTextEditor // TODO the editor should be that of `docUri` and not necessarily the current editor // const editor = vscode.window.activeTextEditor // TODO the editor should be that of `docUri` and not necessarily the current editor
if (!editor) { // if (!editor) {
console.log('Error: No active editor!') // console.log('Error: No active editor!')
return; // return;
} // }
// original code all diffs are based on in the code // // original code all diffs are based on in the code
const originalDiffAreaCode = (this._originalFileOfDocument[docUriStr] || '').split('\n').slice(diffArea.originalStartLine, diffArea.originalEndLine + 1).join('\n') // const originalDiffAreaCode = (this._originalFileOfDocument[docUriStr] || '').split('\n').slice(diffArea.originalStartLine, diffArea.originalEndLine + 1).join('\n')
// figure out where to highlight based on where the AI is in the stream right now, use the last diff in findDiffs to figure that out // // figure out where to highlight based on where the AI is in the stream right now, use the last diff in findDiffs to figure that out
const diffs = findDiffs(originalDiffAreaCode, newDiffAreaCode) // const diffs = findDiffs(originalDiffAreaCode, newDiffAreaCode)
const lastDiff = diffs?.[diffs.length - 1] ?? null // const lastDiff = diffs?.[diffs.length - 1] ?? null
// these are two different coordinate systems - new and old line number // // these are two different coordinate systems - new and old line number
let newFileEndLine: number // get new[0...newStoppingPoint] with line=newStoppingPoint highlighted // let newFileEndLine: number // get new[0...newStoppingPoint] with line=newStoppingPoint highlighted
let oldFileStartLine: number // get original[oldStartingPoint...] // let oldFileStartLine: number // get original[oldStartingPoint...]
if (!lastDiff) { // if (!lastDiff) {
// if the writing is identical so far, display no changes // // if the writing is identical so far, display no changes
newFileEndLine = 0 // newFileEndLine = 0
oldFileStartLine = 0 // oldFileStartLine = 0
} // }
else { // else {
if (lastDiff.type === 'insertion') { // if (lastDiff.type === 'insertion') {
newFileEndLine = lastDiff.range.end.line // newFileEndLine = lastDiff.range.end.line
oldFileStartLine = lastDiff.originalRange.start.line // oldFileStartLine = lastDiff.originalRange.start.line
} // }
else if (lastDiff.type === 'deletion') { // else if (lastDiff.type === 'deletion') {
newFileEndLine = lastDiff.range.start.line // newFileEndLine = lastDiff.range.start.line
oldFileStartLine = lastDiff.originalRange.start.line // oldFileStartLine = lastDiff.originalRange.start.line
} // }
else if (lastDiff.type === 'edit') { // else if (lastDiff.type === 'edit') {
newFileEndLine = lastDiff.range.end.line // newFileEndLine = lastDiff.range.end.line
oldFileStartLine = lastDiff.originalRange.start.line // oldFileStartLine = lastDiff.originalRange.start.line
} // }
else { // else {
throw new Error(`updateStream: diff.type not recognized: ${lastDiff.type}`) // throw new Error(`updateStream: diff.type not recognized: ${lastDiff.type}`)
} // }
} // }
// display // // display
const newFileTop = newDiffAreaCode.split('\n').slice(0, newFileEndLine + 1).join('\n') // const newFileTop = newDiffAreaCode.split('\n').slice(0, newFileEndLine + 1).join('\n')
const oldFileBottom = originalDiffAreaCode.split('\n').slice(oldFileStartLine + 1, Infinity).join('\n') // const oldFileBottom = originalDiffAreaCode.split('\n').slice(oldFileStartLine + 1, Infinity).join('\n')
let newCode = `${newFileTop}\n${oldFileBottom}` // let newCode = `${newFileTop}\n${oldFileBottom}`
diffArea.sweepIndex = newFileEndLine // diffArea.sweepIndex = newFileEndLine
// replace oldDACode with newDACode with a vscode edit // // replace oldDACode with newDACode with a vscode edit
const workspaceEdit = new vscode.WorkspaceEdit(); // const workspaceEdit = new vscode.WorkspaceEdit();
const diffareaRange = new vscode.Range(diffArea.startLine, 0, diffArea.endLine, Number.MAX_SAFE_INTEGER) // const diffareaRange = new vscode.Range(diffArea.startLine, 0, diffArea.endLine, Number.MAX_SAFE_INTEGER)
workspaceEdit.replace(editor.document.uri, diffareaRange, newCode) // workspaceEdit.replace(editor.document.uri, diffareaRange, newCode)
await vscode.workspace.applyEdit(workspaceEdit) // await vscode.workspace.applyEdit(workspaceEdit)
}, THROTTLE_TIME) // }, THROTTLE_TIME)
} }

View file

@ -50,7 +50,7 @@ export function findDiffs(oldStr: string, newStr: string) {
let originalStartLine = streakStartInOldFile! let originalStartLine = streakStartInOldFile!
let originalEndLine = oldFileLineNum - 1 // don't include current line, the edit was up to this line but not including it let originalEndLine = oldFileLineNum - 1 // don't include current line, the edit was up to this line but not including it
let originalStartCol = 0 // let originalStartCol = 0
// let originalEndCol = Number.MAX_SAFE_INTEGER // let originalEndCol = Number.MAX_SAFE_INTEGER
let newContent = newStrLines.slice(startLine, endLine + 1).join('\n') let newContent = newStrLines.slice(startLine, endLine + 1).join('\n')
@ -70,13 +70,15 @@ export function findDiffs(oldStr: string, newStr: string) {
else if (originalEndLine === originalStartLine - 1) { else if (originalEndLine === originalStartLine - 1) {
type = 'insertion' type = 'insertion'
originalEndLine = originalStartLine originalEndLine = originalStartLine
originalStartCol = 0 // originalStartCol = 0
// originalEndCol = 0 // originalEndCol = 0
} }
const replacement: BaseDiff = { const replacement: BaseDiff = {
type, type,
startLine, startCol, endLine, endCol, startLine, endLine,
startCol, endCol,
originalStartLine, originalEndLine,
// code: newContent, // code: newContent,
// originalRange: new Range(originalStartLine, originalStartCol, originalEndLine, originalEndCol), // originalRange: new Range(originalStartLine, originalStartCol, originalEndLine, originalEndCol),
originalCode: originalContent, originalCode: originalContent,

View file

@ -1,7 +1,11 @@
import { ChatFile, ChatCodeSelection } from '../sidebar-tsx/SidebarChat.js'; import { URI } from '../../../../../base/common/uri.js';
export const filesStr = (fullFiles: ChatFile[]) => {
export type LLMCodeSelection = { selectionStr: string; filePath: URI }
export type LLMFile = { content: string, filepath: URI }
export const filesStr = (fullFiles: LLMFile[]) => {
return fullFiles.map(({ filepath, content }) => return fullFiles.map(({ filepath, content }) =>
` `
${filepath.fsPath} ${filepath.fsPath}
@ -11,7 +15,7 @@ ${content}
} }
export const userInstructionsStr = (instructions: string, files: ChatFile[], selection: ChatCodeSelection | null) => { export const userInstructionsStr = (instructions: string, files: LLMFile[], selection: LLMCodeSelection | null) => {
let str = ''; let str = '';
if (files.length > 0) { if (files.length > 0) {

View file

@ -1,4 +1,4 @@
import React, { ReactNode, useCallback, useEffect, useState } from "react" import React, { ReactNode } from "react"
import SyntaxHighlighter from "react-syntax-highlighter"; import SyntaxHighlighter from "react-syntax-highlighter";
import { atomOneDarkReasonable } from "react-syntax-highlighter/dist/esm/styles/hljs"; import { atomOneDarkReasonable } from "react-syntax-highlighter/dist/esm/styles/hljs";
@ -16,9 +16,9 @@ export const BlockCode = ({ text, buttonsOnHover, language }: { text: string, bu
return (<> return (<>
<div className={`relative group w-full bg-vscode-sidebar-bg overflow-hidden isolate`}> <div className={`relative group w-full bg-vscode-sidebar-bg overflow-hidden isolate`}>
{!toolbar ? null : ( {buttonsOnHover === null ? null : (
<div className="absolute top-0 right-0 opacity-0 group-hover:opacity-100 duration-200"> <div className="absolute top-0 right-0 opacity-0 group-hover:opacity-100 duration-200">
<div className="flex space-x-2 p-2">{buttonsOnHover === null ? null : buttonsOnHover}</div> <div className="flex space-x-2 p-2">{buttonsOnHover}</div>
</div> </div>
)} )}

View file

@ -1,5 +1,5 @@
import React, { JSX, useCallback, useEffect, useState } from "react" import React, { JSX, useCallback, useEffect, useState } from "react"
import { marked, MarkedToken, Token, TokensList } from "marked" import { marked, MarkedToken, Token } from "marked"
import { BlockCode } from "./BlockCode.js" import { BlockCode } from "./BlockCode.js"

View file

@ -1,39 +1,23 @@
import React, { FormEvent, useCallback, useRef, useState } from 'react'; import React, { FormEvent, useCallback, useRef, useState } from 'react';
import { generateDiffInstructions } from '../prompt/systemPrompts.js';
import { useConfigState, useService, useSidebarState, useThreadsState } from '../util/contextForServices.js'; import { useConfigState, useService, useSidebarState, useThreadsState } from '../util/contextForServices.js';
import { URI } from '../../../../../../../base/common/uri.js'; import { URI } from '../../../../../../../base/common/uri.js';
import { IFileService } from '../../../../../../../platform/files/common/files.js'; import { VSReadFile } from '../../../registerInlineDiffs.js';
import { userInstructionsStr } from '../prompt/stringifyFiles.js';
import { sendLLMMessage } from '../util/sendLLMMessage.js'; import { sendLLMMessage } from '../util/sendLLMMessage.js';
import { generateDiffInstructions } from '../../../prompt/systemPrompts.js';
import { LLMCodeSelection, userInstructionsStr } from '../../../prompt/stringifyFiles.js';
import { BlockCode } from '../markdown/BlockCode.js'; import { BlockCode } from '../markdown/BlockCode.js';
import { MarkdownRender } from '../markdown/MarkdownRender.js'; import { MarkdownRender } from '../markdown/MarkdownRender.js';
// read files from VSCode
let VSReadFile = async (fileService: IFileService, filepath: URI): Promise<ChatFile | null> => {
try {
const fileObj = await fileService.readFile(filepath)
const content = fileObj.value.toString()
return { filepath, content }
} catch (error) {
console.error(`Failed to read ${filepath}:`, error);
return null
}
}
export type ChatCodeSelection = { selectionStr: string; filePath: URI }
export type ChatFile = { filepath: URI; content: string }
export type ChatMessage = export type ChatMessage =
| { | {
role: 'user'; role: 'user';
content: string; // content sent to the llm content: string; // content sent to the llm
displayContent: string; // content displayed to user displayContent: string; // content displayed to user
selection: ChatCodeSelection | null; // the user's selection selection: LLMCodeSelection | null; // the user's selection
files: URI[]; // the files sent in the message files: URI[]; // the files sent in the message
} }
| { | {
@ -147,7 +131,7 @@ export const SidebarChat = ({ chatInputRef }: { chatInputRef: React.RefObject<HT
// ----- SIDEBAR CHAT state (local) ----- // ----- SIDEBAR CHAT state (local) -----
// state of current message // state of current message
const [selection, setSelection] = useState<ChatCodeSelection | null>(null) // the code the user is selecting const [selection, setSelection] = useState<LLMCodeSelection | null>(null) // the code the user is selecting
const [files, setFiles] = useState<URI[]>([]) // the names of the files in the chat const [files, setFiles] = useState<URI[]>([]) // the names of the files in the chat
const [instructions, setInstructions] = useState('') // the user's instructions const [instructions, setInstructions] = useState('') // the user's instructions
@ -181,9 +165,9 @@ export const SidebarChat = ({ chatInputRef }: { chatInputRef: React.RefObject<HT
const relevantFiles = await Promise.all( const relevantFiles = await Promise.all(
files.map((filepath) => VSReadFile(fileService, filepath)) files.map(async (filepath) => ({ content: await VSReadFile(fileService, filepath), filepath }))
).then( ).then(
(files) => files.filter(file => file !== null) (files) => files.filter(file => file.content !== null) as {content:string, filepath:URI}[]
) )
// add system message to chat history // add system message to chat history

View file

@ -2,12 +2,11 @@ import Anthropic from '@anthropic-ai/sdk';
import OpenAI from 'openai'; import OpenAI from 'openai';
import { Ollama } from 'ollama/browser' import { Ollama } from 'ollama/browser'
import { Content, GoogleGenerativeAI, GoogleGenerativeAIFetchError } from '@google/generative-ai'; import { Content, GoogleGenerativeAI, GoogleGenerativeAIFetchError } from '@google/generative-ai';
import { VoidConfig } from '../../../registerConfig.js';
// import { VoidConfig } from '../webviews/common/contextForConfig' // import { VoidConfig } from '../webviews/common/contextForConfig'
// import { captureEvent } from '../webviews/common/posthog'; // import { captureEvent } from '../webviews/common/posthog';
// import { ChatMessage } from './shared_types'; // import { ChatMessage } from './shared_types';
type VoidConfig = any
export type AbortRef = { current: (() => void) | null } export type AbortRef = { current: (() => void) | null }
export type OnText = (newText: string, fullText: string) => void export type OnText = (newText: string, fullText: string) => void

View file

@ -6,6 +6,6 @@
"esModuleInterop": true, "esModuleInterop": true,
}, },
"include": [ "include": [
"./src/**/*.ts", "./src/**/*.ts"
] ]
} }

View file

@ -1,335 +0,0 @@
import { Disposable } from '../../../../base/common/lifecycle.js';
import { registerSingleton, InstantiationType } from '../../../../platform/instantiation/common/extensions.js';
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
import { ICodeEditor, IViewZone } from '../../../../editor/browser/editorBrowser.js';
import { IUndoRedoElement, IUndoRedoService, UndoRedoElementType, UndoRedoGroup } from '../../../../platform/undoRedo/common/undoRedo.js';
import { ICodeEditorService } from '../../../../editor/browser/services/codeEditorService.js';
import { IBulkEditService } from '../../../../editor/browser/services/bulkEditService.js';
import { WorkspaceEdit } from 'vscode';
import { IModelDeltaDecoration } from '../../../../editor/common/model.js';
import { IRange } from '../../../../editor/common/core/range.js';
import { EditorOption } from '../../../../editor/common/config/editorOptions.js';
// if (m.type === 'applyChanges') {
// const editor = vscode.window.activeTextEditor
// if (!editor) {
// vscode.window.showInformationMessage('No active editor!')
// return
// }
// // create an area to show diffs
// const partialDiffArea: Omit<DiffArea, 'diffareaid'> = {
// startLine: 0, // in ctrl+L the start and end lines are the full document
// endLine: editor.document.lineCount,
// originalStartLine: 0,
// originalEndLine: editor.document.lineCount,
// sweepIndex: null,
// }
// const diffArea = diffProvider.createDiffArea(editor.document.uri, partialDiffArea, await readFileContentOfUri(editor.document.uri))
// const docUri = editor.document.uri
// const fileStr = await readFileContentOfUri(docUri)
// const voidConfig = getVoidConfigFromPartial(context.globalState.get('partialVoidConfig') ?? {})
// await diffProvider.startStreamingInDiffArea({ docUri, oldFileStr: fileStr, diffRepr: m.diffRepr, voidConfig, diffArea, abortRef: abortApplyRef })
// }
// // an area that is currently being diffed
type DiffArea = {
diffareaid: number,
startLine: number,
endLine: number,
originalStartLine: number,
originalEndLine: number,
sweepIndex: number | null // null iff not sweeping
}
// the return type of diff creator
type BaseDiff = {
type: 'edit' | 'insertion' | 'deletion';
// repr: string; // representation of the diff in text
originalRange: IRange;
originalCode: string;
range: IRange;
code: string;
}
// each diff on the user's screen
type Diff = BaseDiff & {
diffid: number,
lenses: CodeLens[],
}
export interface IInlineDiffService {
readonly _serviceBrand: undefined;
addDiff(editor: ICodeEditor, originalText: string, modifiedRange: IRange): void;
removeDiffs(editor: ICodeEditor): void;
}
export const IInlineDiffService = createDecorator<IInlineDiffService>('inlineDiffService');
class InlineDiffService extends Disposable implements IInlineDiffService {
private readonly _diffDecorations = new Map<ICodeEditor, string[]>();
private readonly _diffZones = new Map<ICodeEditor, string[]>();
_serviceBrand: undefined;
constructor() {
super();
}
initStream() {
}
public addDiff: IInlineDiffService['addDiff'] = (editor, originalText, modifiedRange) => {
// Clear existing diffs
this.removeDiffs(editor);
// green decoration and gutter decoration
const greenDecoration: IModelDeltaDecoration[] = [{
range: modifiedRange,
options: {
className: 'line-insert', // .monaco-editor .line-insert
description: 'line-insert',
isWholeLine: true,
minimap: {
color: { id: 'minimapGutter.addedBackground' },
position: 2
},
overviewRuler: {
color: { id: 'editorOverviewRuler.addedForeground' },
position: 7
}
}
}];
this._diffDecorations.set(editor, editor.deltaDecorations([], greenDecoration));
// red in a view zone
editor.changeViewZones(accessor => {
// Get the editor's font info
const fontInfo = editor.getOption(EditorOption.fontInfo);
const domNode = document.createElement('div');
domNode.className = 'monaco-editor view-zones line-delete monaco-mouse-cursor-text';
domNode.style.fontSize = `${fontInfo.fontSize}px`;
domNode.style.fontFamily = fontInfo.fontFamily;
domNode.style.lineHeight = `${fontInfo.lineHeight}px`;
// div
const lineContent = document.createElement('div');
lineContent.className = 'view-line'; // .monaco-editor .inline-deleted-text
// span
const contentSpan = document.createElement('span');
// span
const codeSpan = document.createElement('span');
codeSpan.className = 'mtk1'; // char-delete
codeSpan.textContent = originalText;
// Mount
contentSpan.appendChild(codeSpan);
lineContent.appendChild(contentSpan);
domNode.appendChild(lineContent);
// gutter element
const gutterDiv = document.createElement('div');
gutterDiv.className = 'inline-diff-gutter';
const minusDiv = document.createElement('div');
minusDiv.className = 'inline-diff-deleted-gutter';
// minusDiv.textContent = '-';
gutterDiv.appendChild(minusDiv);
const viewZone: IViewZone = {
afterLineNumber: modifiedRange.startLineNumber - 1,
heightInLines: originalText.split('\n').length + 1,
domNode: domNode,
suppressMouseDown: true,
marginDomNode: gutterDiv
};
const zoneId = accessor.addZone(viewZone);
// editor.layout();
this._diffZones.set(editor, [zoneId]);
});
}
public removeDiffs(editor: ICodeEditor): void {
const decorationIds = this._diffDecorations.get(editor) || [];
editor.deltaDecorations(decorationIds, []);
this._diffDecorations.delete(editor);
editor.changeViewZones(accessor => {
const zoneIds = this._diffZones.get(editor) || [];
zoneIds.forEach(id => accessor.removeZone(id));
});
this._diffZones.delete(editor);
}
override dispose(): void {
super.dispose();
this._diffDecorations.clear();
this._diffZones.clear();
}
}
registerSingleton(IInlineDiffService, InlineDiffService, InstantiationType.Eager);
class StreamManager extends Disposable {
// private readonly _disposables = new DisposableStore();
_streamingState: { type: 'streaming'; editGroup: UndoRedoGroup } | { type: 'idle' } = { type: 'idle' }
constructor(
@IInlineDiffService private readonly _inlineDiff: IInlineDiffService,
@ICodeEditorService private readonly _editorService: ICodeEditorService,
// @IHistoryService private readonly _historyService: IHistoryService, // history service is the history of pressing alt left/right
@IUndoRedoService private readonly _undoRedoService: IUndoRedoService, // undoRedo service is the history of pressing ctrl+z
@IBulkEditService private readonly _bulkEditService: IBulkEditService,
) {
super();
}
startStreaming(editorId: string) {
const editor = this._getEditor(editorId)
if (!editor) return
const model = editor.getModel()
if (!model) return
// all changes made when streaming should be a part of the group so we can undo them all together
this._streamingState = {
type: 'streaming',
editGroup: new UndoRedoGroup()
}
// TODO probably need to convert this to a stack
const diffsSnapshotBefore = { placeholder: '' }
const diffsSnapshotAfter = { placeholder: '' }
const elt: IUndoRedoElement = {
type: UndoRedoElementType.Resource,
resource: model.uri,
label: 'Add Diffs',
code: 'undoredo.inlineDiff',
undo: () => {
// reapply diffareas and diffs here
console.log('reverting diffareas...', diffsSnapshotBefore.placeholder)
},
redo: () => {
// reapply diffareas and diffs here
// when done, need to record diffSnapshotAfter
console.log('re-applying diffareas...', diffsSnapshotAfter.placeholder)
}
}
this._undoRedoService.pushElement(elt, this._streamingState.editGroup)
// ---------- START ----------
editor.updateOptions({ readOnly: true })
// ---------- WHEN DONE ----------
editor.updateOptions({ readOnly: false })
}
streamChange(editorId: string, edit: WorkspaceEdit) {
const editor = this._getEditor(editorId)
if (!editor) return
if (this._streamingState.type !== 'streaming') {
console.error('Expected streamChange to be in state \'streaming\'.')
return
}
// count all changes towards the group
this._bulkEditService.apply(edit, { undoRedoGroupId: this._streamingState.editGroup.id, })
}
_getEditor = (editorId: string): ICodeEditor | undefined => {
let editor: ICodeEditor | undefined;
editorId = editorId.substr(0, editorId.indexOf(',')); //todo@jrieken HACK
for (const candidate of this._editorService.listCodeEditors()) {
if (candidate.getId() === editorId
// && candidate.hasModel() && isEqual(candidate.getModel().uri, URI.revive(uri))
) {
editor = candidate;
break;
}
}
return editor
}
$addDiff(editorId: string, originalText: string, range: IRange): void {
const editor = this._getEditor(editorId);
if (!editor) return
this._inlineDiff.addDiff(editor, originalText, range)
}
}
// // Void created this file
// // it comes from mainThreadCodeInsets.ts
// import { Disposable } from '../../../base/common/lifecycle.js';
// import { ICodeEditorService } from '../../../editor/browser/services/codeEditorService.js';
// import { MainContext, MainThreadInlineDiffShape } from '../common/extHost.protocol.js';
// import { IInlineDiffService } from '../../../editor/browser/services/inlineDiffService/inlineDiffService.js';
// import { ICodeEditor } from '../../../editor/browser/editorBrowser.js';
// import { IRange } from '../../../editor/common/core/range.js';
// import { extHostNamedCustomer, IExtHostContext } from '../../services/extensions/common/extHostCustomers.js';
// import { IUndoRedoElement, IUndoRedoService, UndoRedoElementType, UndoRedoGroup } from '../../../platform/undoRedo/common/undoRedo.js';
// import { IBulkEditService } from '../../../editor/browser/services/bulkEditService.js';
// import { WorkspaceEdit } from '../../../editor/common/languages.js';
// // import { IHistoryService } from '../../services/history/common/history.js';

View file

@ -6,62 +6,78 @@ import { ICodeEditor, IViewZone } from '../../../../editor/browser/editorBrowser
import { IUndoRedoElement, IUndoRedoService, UndoRedoElementType, UndoRedoGroup } from '../../../../platform/undoRedo/common/undoRedo.js'; import { IUndoRedoElement, IUndoRedoService, UndoRedoElementType, UndoRedoGroup } from '../../../../platform/undoRedo/common/undoRedo.js';
import { ICodeEditorService } from '../../../../editor/browser/services/codeEditorService.js'; import { ICodeEditorService } from '../../../../editor/browser/services/codeEditorService.js';
import { IBulkEditService } from '../../../../editor/browser/services/bulkEditService.js'; import { IBulkEditService, ResourceTextEdit } from '../../../../editor/browser/services/bulkEditService.js';
import { WorkspaceEdit } from 'vscode';
import { EditorOption } from '../../../../editor/common/config/editorOptions.js';
import { Emitter } from '../../../../base/common/event.js'; import { Emitter } from '../../../../base/common/event.js';
import { sendLLMMessage } from './out/util/sendLLMMessage.js';
import { throttle } from '../../../../base/common/decorators.js';
import { IFileService } from '../../../../platform/files/common/files.js';
import { URI } from '../../../../base/common/uri.js';
import { IVoidConfigStateService } from './registerConfig.js';
import { writeFileWithDiffInstructions } from './prompt/systemPrompts.js';
import { findDiffs } from './findDiffs.js';
// read files from VSCode
export const VSReadFile = async (fileService: IFileService, uri: URI): Promise<string | null> => {
try {
const fileObj = await fileService.readFile(uri)
const content = fileObj.value.toString()
return content
} catch (error) {
console.error(`VSReadFile (Void) - Failed to read URI`, uri, error);
return null
}
}
// red in a view zone // // red in a view zone
editor.changeViewZones(accessor => { // editor.changeViewZones(accessor => {
// Get the editor's font info // // Get the editor's font info
const fontInfo = editor.getOption(EditorOption.fontInfo); // const fontInfo = editor.getOption(EditorOption.fontInfo);
const domNode = document.createElement('div'); // const domNode = document.createElement('div');
domNode.className = 'monaco-editor view-zones line-delete monaco-mouse-cursor-text'; // domNode.className = 'monaco-editor view-zones line-delete monaco-mouse-cursor-text';
domNode.style.fontSize = `${fontInfo.fontSize}px`; // domNode.style.fontSize = `${fontInfo.fontSize}px`;
domNode.style.fontFamily = fontInfo.fontFamily; // domNode.style.fontFamily = fontInfo.fontFamily;
domNode.style.lineHeight = `${fontInfo.lineHeight}px`; // domNode.style.lineHeight = `${fontInfo.lineHeight}px`;
// div // // div
const lineContent = document.createElement('div'); // const lineContent = document.createElement('div');
lineContent.className = 'view-line'; // .monaco-editor .inline-deleted-text // lineContent.className = 'view-line'; // .monaco-editor .inline-deleted-text
// span // // span
const contentSpan = document.createElement('span'); // const contentSpan = document.createElement('span');
// span // // span
const codeSpan = document.createElement('span'); // const codeSpan = document.createElement('span');
codeSpan.className = 'mtk1'; // char-delete // codeSpan.className = 'mtk1'; // char-delete
codeSpan.textContent = originalText; // codeSpan.textContent = originalText;
// Mount // // Mount
contentSpan.appendChild(codeSpan); // contentSpan.appendChild(codeSpan);
lineContent.appendChild(contentSpan); // lineContent.appendChild(contentSpan);
domNode.appendChild(lineContent); // domNode.appendChild(lineContent);
// gutter element // // gutter element
const gutterDiv = document.createElement('div'); // const gutterDiv = document.createElement('div');
gutterDiv.className = 'inline-diff-gutter'; // gutterDiv.className = 'inline-diff-gutter';
const minusDiv = document.createElement('div'); // const minusDiv = document.createElement('div');
minusDiv.className = 'inline-diff-deleted-gutter'; // minusDiv.className = 'inline-diff-deleted-gutter';
// minusDiv.textContent = '-'; // // minusDiv.textContent = '-';
gutterDiv.appendChild(minusDiv); // gutterDiv.appendChild(minusDiv);
const viewZone: IViewZone = { // const viewZone: IViewZone = {
afterLineNumber: modifiedRange.startLineNumber - 1, // afterLineNumber: modifiedRange.startLineNumber - 1,
heightInLines: originalText.split('\n').length + 1, // heightInLines: originalText.split('\n').length + 1,
domNode: domNode, // domNode: domNode,
suppressMouseDown: true, // suppressMouseDown: true,
marginDomNode: gutterDiv // marginDomNode: gutterDiv
}; // };
const zoneId = accessor.addZone(viewZone); // const zoneId = accessor.addZone(viewZone);
// editor.layout(); // // editor.layout();
this._diffZones.set(editor, [zoneId]); // this._diffZones.set(editor, [zoneId]);
}); // });
@ -94,10 +110,16 @@ editor.changeViewZones(accessor => {
// override dispose(): void { // public removeAllDiffs(editor: ICodeEditor): void {
// super.dispose(); // const decorationIds = this._diffDecorations.get(editor) || [];
// this._diffDecorations.clear(); // editor.deltaDecorations(decorationIds, []);
// this._diffZones.clear(); // this._diffDecorations.delete(editor);
// editor.changeViewZones(accessor => {
// const zoneIds = this._diffZones.get(editor) || [];
// zoneIds.forEach(id => accessor.removeZone(id));
// });
// this._diffZones.delete(editor);
// } // }
@ -105,41 +127,32 @@ editor.changeViewZones(accessor => {
public removeAllDiffs(editor: ICodeEditor): void { // _ means computed later, temporary, or part of current state
const decorationIds = this._diffDecorations.get(editor) || [];
editor.deltaDecorations(decorationIds, []);
this._diffDecorations.delete(editor);
editor.changeViewZones(accessor => {
const zoneIds = this._diffZones.get(editor) || [];
zoneIds.forEach(id => accessor.removeZone(id));
});
this._diffZones.delete(editor);
}
// _ means computed / temporary
type DiffArea = { type DiffArea = {
diffareaid: string, diffareaid: number,
originalStartLine: number,
originalEndLine: number,
startLine: number, startLine: number,
endLine: number, endLine: number,
_uri: URI, // document uri
_streamId: number,
_diffIds: string[], _diffIds: string[],
_sweepIdx: number | null, _sweepLine: number | null,
_sweepCol: number | null,
} }
export type Diff = { export type Diff = {
diffid: string, diffid: number,
diffareaid: string, // the diff area this diff belongs to, "computed" diffareaid: number, // the diff area this diff belongs to, "computed"
type: 'edit' | 'insertion' | 'deletion'; type: 'edit' | 'insertion' | 'deletion';
originalCode: string; originalCode: string;
startLine: number; startLine: number;
endLine: number; endLine: number;
originalStartLine: number;
originalEndLine: number;
startCol: number; startCol: number;
endCol: number; endCol: number;
@ -163,6 +176,12 @@ type HistorySnapshot = {
}) })
type StreamingState = {
type: 'streaming';
editGroup: UndoRedoGroup; // all changes made by us when streaming should be a part of the group so we can undo them all together
} | {
type: 'idle';
}
export interface IInlineDiffsService { export interface IInlineDiffsService {
@ -174,43 +193,49 @@ export const IInlineDiffsService = createDecorator<IInlineDiffsService>('inlineD
class InlineDiffsService extends Disposable implements IInlineDiffsService { class InlineDiffsService extends Disposable implements IInlineDiffsService {
_serviceBrand: undefined; _serviceBrand: undefined;
// state of each document (uri)
diffAreasOfURI: Record<string, string[]> = {} // uriStr -> diffAreaId[]
originalFileOfURI: Record<string, string> = {} // uriStr -> originalFile
streamingStateOfURI: Record<string, StreamingState> = {} // uriStr -> state
diffAreaOfId: Map<string, DiffArea> = new Map(); diffAreaOfId: Map<string, DiffArea> = new Map();
diffOfId: Map<string, Diff> = new Map(); diffOfId: Map<string, Diff> = new Map();
_streamIdPool = 0
streamingState: { _diffareaIdPool = 0
type: 'streaming';
editGroup: UndoRedoGroup;
} | { type: 'idle' }
= { type: 'idle' }
private readonly _onDidFinishStreaming = new Emitter<void>(); private readonly _onDidFinishStreaming = new Emitter<void>();
constructor( constructor(
// @IHistoryService private readonly _historyService: IHistoryService, // history service is the history of pressing alt left/right // @IHistoryService private readonly _historyService: IHistoryService, // history service is the history of pressing alt left/right
@IInlineDiffsService private readonly _inlineDiff: IInlineDiffsService,
@ICodeEditorService private readonly _editorService: ICodeEditorService, @ICodeEditorService private readonly _editorService: ICodeEditorService,
@IUndoRedoService private readonly _undoRedoService: IUndoRedoService, // undoRedo service is the history of pressing ctrl+z @IUndoRedoService private readonly _undoRedoService: IUndoRedoService, // undoRedo service is the history of pressing ctrl+z
@IBulkEditService private readonly _bulkEditService: IBulkEditService, @IBulkEditService private readonly _bulkEditService: IBulkEditService,
@IFileService private readonly _fileService: IFileService,
@IVoidConfigStateService private readonly _voidConfigStateService: IVoidConfigStateService,
) { ) {
super(); super();
// // this acts as a useEffect every time text changes
// vscode.workspace.onDidChangeTextDocument((e) => {
// const editor = vscode.window.activeTextEditor
// if (!editor) return
// const docUriStr = editor.document.uri.toString()
// const changes = e.contentChanges.map(c => ({ startLine: c.range.start.line, endLine: c.range.end.line, text: c.text, }))
// // on user change, grow/shrink/merge/delete diff areas
// this.resizeDiffAreas(docUriStr, changes, 'currentFile')
// // refresh the diffAreas
// this.refreshStylesAndDiffs(docUriStr)
// })
// listen for document changes, and re-add the diffAreas of this document
} }
startStreaming() { private _addToHistory(uri: URI, editGroup: UndoRedoGroup) {
const editor = this._editorService.getActiveCodeEditor()
if (!editor) return
const model = editor.getModel()
if (!model) return
// all changes made by us when streaming should be a part of the group so we can undo them all together
this.streamingState = {
type: 'streaming',
editGroup: new UndoRedoGroup(),
}
const beforeSnapshot: HistorySnapshot = { const beforeSnapshot: HistorySnapshot = {
diffAreaOfId: new Map(this.diffAreaOfId), diffAreaOfId: new Map(this.diffAreaOfId),
@ -232,7 +257,7 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService {
const elt: IUndoRedoElement = { const elt: IUndoRedoElement = {
type: UndoRedoElementType.Resource, type: UndoRedoElementType.Resource,
resource: model.uri, resource: uri,
label: 'Add Diffs', label: 'Add Diffs',
code: 'undoredo.inlineDiffs', code: 'undoredo.inlineDiffs',
// called when undoing this state // called when undoing this state
@ -249,41 +274,314 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService {
this.diffOfId = new Map(afterSnapshot.diffOfId) this.diffOfId = new Map(afterSnapshot.diffOfId)
} }
} }
this._undoRedoService.pushElement(elt, editGroup)
this._undoRedoService.pushElement(elt, this.streamingState.editGroup)
// ---------- START ----------
editor.updateOptions({ readOnly: true })
// ---------- WHEN DONE ----------
editor.updateOptions({ readOnly: false })
} }
private async _initializeStream(editor: ICodeEditor, diffRepr: string) {
private _streamChange(editor: ICodeEditor, edit: WorkspaceEdit) { const model = editor.getModel()
if (!model) return
// count all changes towards the group const uri = model.uri
this._bulkEditService.apply(edit, { undoRedoGroupId: this._streamingState.editGroup.id, }) const uriStr = uri.toString()
console.log('Model URI:', uriStr)
}
endStreaming() { // create a diffArea for the stream
const diffareaid = this._diffareaIdPool++
const streamId = this._streamIdPool++
// in ctrl+L the start and end lines are the full document
const lineCount = model.getLineCount()
const diffArea: DiffArea = {
diffareaid: diffareaid,
originalStartLine: 0,
originalEndLine: lineCount,
startLine: 0,
endLine: lineCount, // starts out the same as the current file
_uri: uri,
_sweepLine: null,
_sweepCol: null,
_streamId: streamId,
_diffIds: [], // added later
}
const originalFileStr = await VSReadFile(this._fileService, uri)
if (originalFileStr === null) return
this.originalFileOfURI[uriStr] = originalFileStr
// make sure array is defined
if (!(uriStr in this.diffAreasOfURI))
this.diffAreasOfURI[uriStr] = []
// remove all diffAreas that the new `diffArea` is overlapping with
this.diffAreasOfURI[uriStr] = this.diffAreasOfURI[uriStr].filter(diffareaid => {
const da2 = this.diffAreaOfId.get(diffareaid)
if (!da2) return false
const noOverlap = da2.startLine > diffArea.endLine || da2.endLine < diffArea.startLine
if (!noOverlap) return false
return true
})
// add `diffArea` to storage
this.diffAreasOfURI[uriStr].push(diffArea.diffareaid.toString())
// actually call the LLM
const voidConfig = this._voidConfigStateService.state
const promptContent = `\
ORIGINAL_FILE
\`\`\`
${originalFileStr}
\`\`\`
DIFF
\`\`\`
${diffRepr}
\`\`\`
INSTRUCTIONS
Please finish writing the new file by applying the diff to the original file. Return ONLY the completion of the file, without any explanation.
`
await new Promise<void>((resolve, reject) => {
sendLLMMessage({
logging: { loggingName: 'streamChunk' },
messages: [
{ role: 'system', content: writeFileWithDiffInstructions, },
// TODO include more context too
{ role: 'user', content: promptContent, }
],
onText: (newText, fullText) => {
this._onStreamChunk(uri, diffArea, fullText)
},
onFinalMessage: (fullText) => {
this._onStreamChunk(uri, diffArea, fullText)
resolve();
},
onError: (e) => {
console.error('Error rewriting file with diff', e);
resolve();
},
voidConfig,
abortRef,
})
})
this._onDidFinishStreaming.fire() this._onDidFinishStreaming.fire()
} }
// used by us only
@throttle(100)
private async _onStreamChunk(uri: URI, diffArea: DiffArea, newDiffAreaCode: string) {
const docUriStr = uri.toString()
if (this.streamingStateOfURI[docUriStr].type !== 'streaming')
return
// original code all diffs are based on in the code
const originalDiffAreaCode = (this.originalFileOfURI[docUriStr] || '').split('\n').slice(diffArea.originalStartLine, diffArea.originalEndLine + 1).join('\n')
// figure out where to highlight based on where the AI is in the stream right now, use the last diff in findDiffs to figure that out
const diffs = findDiffs(originalDiffAreaCode, newDiffAreaCode)
const lastDiff = diffs?.[diffs.length - 1] ?? null
// these are two different coordinate systems - new and old line number
let newFileEndLine: number // get new[0...newStoppingPoint] with line=newStoppingPoint highlighted
let oldFileStartLine: number // get original[oldStartingPoint...]
if (!lastDiff) {
// if the writing is identical so far, display no changes
newFileEndLine = 0
oldFileStartLine = 0
}
else {
if (lastDiff.type === 'insertion') {
newFileEndLine = lastDiff.endLine
oldFileStartLine = lastDiff.originalStartLine
}
else if (lastDiff.type === 'deletion') {
newFileEndLine = lastDiff.startLine
oldFileStartLine = lastDiff.originalStartLine
}
else if (lastDiff.type === 'edit') {
newFileEndLine = lastDiff.endLine
oldFileStartLine = lastDiff.originalStartLine
}
else {
throw new Error(`updateStream: diff.type not recognized: ${lastDiff.type}`)
}
}
// display
const newFileTop = newDiffAreaCode.split('\n').slice(0, newFileEndLine + 1).join('\n')
const oldFileBottom = originalDiffAreaCode.split('\n').slice(oldFileStartLine + 1, Infinity).join('\n')
let newCode = `${newFileTop}\n${oldFileBottom}`
diffArea._sweepLine = newFileEndLine
this._bulkEditService.apply(
[new ResourceTextEdit(uri, {
range: {
startLineNumber: diffArea.startLine,
startColumn: 0,
endLineNumber: diffArea.endLine,
endColumn: Number.MAX_SAFE_INTEGER,
},
text: newCode
})],
// count all changes towards the group
{ undoRedoGroupId: this.streamingStateOfURI[docUriStr].editGroup.id });
}
startStreaming(type: 'ctrl+k' | 'ctrl+l', userMessage: string) {
const editor = this._editorService.getActiveCodeEditor()
if (!editor) return
const model = editor.getModel()
if (!model) return
// update streaming state
const streamingState: StreamingState = { type: 'streaming', editGroup: new UndoRedoGroup(), }
this.streamingStateOfURI[model.uri.toString()] = streamingState
// add to history
this._addToHistory(model.uri, streamingState.editGroup)
// initialize stream
this._initializeStream(editor, userMessage)
}
// called on void.acceptDiff
public async acceptDiff({ diffid }: { diffid: number }) {
const diff = this.diffOfId.get(diffid + '')!
if (!diff) return
const { diffareaid } = diff
const diffArea = this.diffAreaOfId.get(diffareaid + '')
if (!diffArea) return
const uri = diffArea._uri
const uriStr = uri.toString()
const originalFile = this.originalFileOfURI[uriStr]
const currentFile = await VSReadFile(this._fileService, uri)
if (!currentFile) return
// Fixed: Handle newlines properly by splitting into lines and joining with proper newlines
const originalLines = originalFile.split('\n');
const currentLines = currentFile.split('\n');
// Get the changed lines from current file
const changedLines = currentLines.slice(diff.startLine, diff.endLine + 1);
// Create new original file content by replacing the affected lines
const newOriginalLines = [
...originalLines.slice(0, diff.originalStartLine),
...changedLines,
...originalLines.slice(diff.originalEndLine + 1)
];
this.originalFileOfURI[uriStr] = newOriginalLines.join('\n');
// // Update diff areas based on the change (this)
// this.resizeDiffAreas(uriStr, [{
// text: changedLines.join('\n'),
// startLine: diff.originalRange.start.line,
// endLine: diff.originalRange.end.line
// }], 'originalFile')
// // Check if diffArea should be removed
// const diffArea = this._diffAreasOfDocument[docUriStr][diffareaIdx]
// const currentArea = currentLines.slice(diffArea.startLine, diffArea.endLine + 1).join('\n')
// const originalArea = newOriginalLines.slice(diffArea.originalStartLine, diffArea.originalEndLine + 1).join('\n')
// if (originalArea === currentArea) {
// const index = this._diffAreasOfDocument[docUriStr].findIndex(da => da.diffareaid === diffArea.diffareaid)
// this._diffAreasOfDocument[docUriStr].splice(index, 1)
// }
this.refreshStylesAndDiffs(docUriStr)
}
// called on void.rejectDiff
public async rejectDiff({ diffid, diffareaid }: { diffid: number, diffareaid: number }) {
const editor = vscode.window.activeTextEditor
if (!editor)
return
const docUriStr = editor.document.uri.toString()
const diffIdx = this._diffsOfDocument[docUriStr].findIndex(diff => diff.diffid === diffid);
if (diffIdx === -1) { console.error('Error: DiffID could not be found: ', diffid, diffareaid, this._diffsOfDocument[docUriStr], this._diffAreasOfDocument[docUriStr]); return; }
const diffareaIdx = this._diffAreasOfDocument[docUriStr].findIndex(diff => diff.diffareaid === diffareaid);
if (diffareaIdx === -1) { console.error('Error: DiffAreaID could not be found: ', diffid, diffareaid, this._diffsOfDocument[docUriStr], this._diffAreasOfDocument[docUriStr]); return; }
const diff = this._diffsOfDocument[docUriStr][diffIdx]
// Apply the rejection by replacing with original code
// we don't have to edit the original or final file; just do a workspace edit so the code equals the original code
const workspaceEdit = new vscode.WorkspaceEdit();
workspaceEdit.replace(editor.document.uri, diff.range, diff.originalCode)
await vscode.workspace.applyEdit(workspaceEdit)
// Check if diffArea should be removed
const originalFile = this._originalFileOfDocument[docUriStr]
const currentFile = await readFileContentOfUri(editor.document.uri)
const diffArea = this._diffAreasOfDocument[docUriStr][diffareaIdx]
const currentLines = currentFile.split('\n');
const originalLines = originalFile.split('\n');
const currentArea = currentLines.slice(diffArea.startLine, diffArea.endLine + 1).join('\n')
const originalArea = originalLines.slice(diffArea.originalStartLine, diffArea.originalEndLine + 1).join('\n')
if (originalArea === currentArea) {
const index = this._diffAreasOfDocument[docUriStr].findIndex(da => da.diffareaid === diffArea.diffareaid)
this._diffAreasOfDocument[docUriStr].splice(index, 1)
}
this.refreshStylesAndDiffs(docUriStr)
}
} }
registerSingleton(IInlineDiffsService, InlineDiffsService, InstantiationType.Eager); registerSingleton(IInlineDiffsService, InlineDiffsService, InstantiationType.Eager);