multiple find/replace blocks almost work. only 1 diffarea per find/replace application

This commit is contained in:
Andrew Pareles 2025-02-20 01:21:28 -08:00
parent 62c2622ced
commit e14aab632d
3 changed files with 188 additions and 158 deletions

View file

@ -270,7 +270,7 @@ export class EditorGroupWatermark extends Disposable {
const keys3 = this.keybindingService.lookupKeybinding('workbench.action.openGlobalKeybindings');
const button3 = append(recentsBox, $('button'));
button3.textContent = 'Void Settings'
button3.textContent = `Void's Settings`
button3.style.display = 'block'
button3.style.marginLeft = 'auto'
button3.style.marginRight = 'auto'

View file

@ -72,8 +72,7 @@ export type ChatMessage =
stagingSelections: StagingSelectionItem[];
isBeingEdited: boolean;
}
}
| {
} | {
role: 'assistant';
content: string | null; // content received from LLM - allowed to be '', will be replaced with (empty)
displayContent: string | null; // content displayed to user (this is the same as content for now) - allowed to be '', will be ignored
@ -85,7 +84,7 @@ type UserMessageState = UserMessageType['state']
export const defaultMessageState: UserMessageState = {
stagingSelections: [],
isBeingEdited: false
isBeingEdited: false,
}
// a 'thread' means a chat message history

View file

@ -39,7 +39,7 @@ import { Emitter } from '../../../../base/common/event.js';
import { VOID_OPEN_SETTINGS_ACTION_ID } from './voidSettingsPane.js';
import { ICommandService } from '../../../../platform/commands/common/commands.js';
import { ILLMMessageService } from '../common/llmMessageService.js';
import { LLMChatMessage, errorDetails } from '../common/llmMessageTypes.js';
import { LLMChatMessage, OnError, errorDetails } from '../common/llmMessageTypes.js';
import { IMetricsService } from '../common/metricsService.js';
import { IVoidFileService } from '../common/voidFileService.js';
@ -65,6 +65,8 @@ registerColor('void.sweepIdxBG', configOfBG(sweepIdxBG), '', true);
const getLeadingWhitespacePx = (editor: ICodeEditor, startLine: number): number => {
const model = editor.getModel();
@ -102,6 +104,25 @@ const getLeadingWhitespacePx = (editor: ICodeEditor, startLine: number): number
// finds block.orig in fileContents and return its range in file
const findTextInCode = (text: string, fileContents: string) => {
console.log('TEXTTTT', JSON.stringify(text))
const idx = fileContents.indexOf(text)
if (idx === -1) return 'Not found' as const
const lastIdx = fileContents.lastIndexOf(text)
if (lastIdx !== idx) return 'Not unique' as const
console.log('TEXTTTT22222', JSON.stringify(fileContents.substring(0, idx)))
const startLine = fileContents.substring(0, idx).split('\n').length
const numLines = text.split('\n').length
const endLine = startLine + numLines - 1
console.log('startline', startLine)
console.log('endline', endLine)
return [startLine, endLine]
}
export type StartApplyingOpts = {
from: 'QuickEdit';
type: 'rewrite';
@ -328,6 +349,29 @@ class EditCodeService extends Disposable implements IEditCodeService {
}
private _notifyError = (e: Parameters<OnError>[0]) => {
const details = errorDetails(e.fullError)
this._notificationService.notify({
severity: Severity.Warning,
message: `Void Error: ${e.message}`,
actions: {
secondary: [{
id: 'void.onerror.opensettings',
enabled: true,
label: `Open Void's settings`,
tooltip: '',
class: undefined,
run: () => { this._commandService.executeCommand(VOID_OPEN_SETTINGS_ACTION_ID) }
}]
},
source: details ? `(Hold ${isMacintosh ? 'Option' : 'Alt'} to hover) - ${details}\n\nIf this persists, feel free to [report](https://github.com/voideditor/void/issues/new) it.` : undefined
})
}
// highlight the region
private _addLineDecoration = (model: ITextModel | null, startLine: number, endLine: number, className: string, options?: Partial<IModelDecorationOptions>) => {
if (model === null) return
@ -1000,18 +1044,11 @@ class EditCodeService extends Disposable implements IEditCodeService {
// @throttle(100)
private _writeStreamedDiffZoneLLMText(diffZone: DiffZone, llmText: string, deltaText: string, latestMutable: StreamLocationMutable) {
private _writeStreamedDiffZoneLLMText(uri: URI, originalCode: string, llmTextSoFar: string, deltaText: string, latestMutable: StreamLocationMutable) {
// ----------- 1. Write the new code to the document -----------
// figure out where to highlight based on where the AI is in the stream right now, use the last diff to figure that out
const uri = diffZone._URI
const computedDiffs = findDiffs(diffZone.originalCode, llmText)
// should always be in streaming state here
if (!diffZone._streamState.isStreaming) {
console.error('DiffZone was not in streaming state on _writeDiffZoneLLMText')
return
}
const computedDiffs = findDiffs(originalCode, llmTextSoFar)
// if streaming, use diffs to figure out where to write new code
// these are two different coordinate systems - new and old line number
@ -1036,8 +1073,6 @@ class EditCodeService extends Disposable implements IEditCodeService {
throw new Error(`Void: diff.type not recognized on: ${lastDiff}`)
}
// at the start, add a newline between the stream and originalCode to make reasoning easier
if (!latestMutable.addedSplitYet) {
this._writeText(uri, '\n',
@ -1066,18 +1101,14 @@ class EditCodeService extends Disposable implements IEditCodeService {
)
}
else if (latestMutable.originalCodeStartLine > originalCodeStartLine) {
this._writeText(uri, '\n' + diffZone.originalCode.split('\n').slice((originalCodeStartLine - 1), (latestMutable.originalCodeStartLine - 1) - 1 + 1).join('\n'),
this._writeText(uri, '\n' + originalCode.split('\n').slice((originalCodeStartLine - 1), (latestMutable.originalCodeStartLine - 1) - 1 + 1).join('\n'),
{ startLineNumber: latestMutable.line, startColumn: latestMutable.col, endLineNumber: latestMutable.line, endColumn: latestMutable.col },
{ shouldRealignDiffAreas: true }
)
}
latestMutable.originalCodeStartLine = originalCodeStartLine
// add diffZone.startLine to convert to right coordinate system (line in file, not in diffarea)
diffZone._streamState.line = (diffZone.startLine - 1) + newCodeEndLine
return computedDiffs
return newCodeEndLine
}
@ -1145,7 +1176,7 @@ class EditCodeService extends Disposable implements IEditCodeService {
public startApplying(opts: StartApplyingOpts) {
if (opts.type === 'rewrite') {
const addedDiffZone = this._initializeRewriteStream(opts)
const addedDiffZone = this._initializeWriteoverStream(opts)
return addedDiffZone?.diffareaid
}
@ -1183,41 +1214,55 @@ class EditCodeService extends Disposable implements IEditCodeService {
const uri = uri_
// generate search/replace block text
const origFileContents = await this._voidFileService.readFile(uri)
if (origFileContents === null) return
const originalFileCode = await this._voidFileService.readFile(uri)
if (originalFileCode === null) return
const numLines = this._getNumLines(uri)
if (numLines === null) return
// // reject all diffZones on this URI, adding to history (there can't possibly be overlap after this)
// this.removeDiffAreas({ uri, behavior: 'reject', removeCtrlKs: true })
// reject all diffZones on this URI, adding to history (there can't possibly be overlap after this)
this.removeDiffAreas({ uri, behavior: 'reject', removeCtrlKs: true })
const userMessageContent = searchReplace_userMessage({ originalCode: origFileContents, applyStr: applyStr })
const startLine = 1
const endLine = numLines
const userMessageContent = searchReplace_userMessage({ originalCode: originalFileCode, applyStr: applyStr })
const messages: LLMChatMessage[] = [
{ role: 'system', content: searchReplace_systemMessage },
{ role: 'user', content: userMessageContent },
]
let streamRequestIdRef: { current: string | null } = { current: null }
const diffareaidOfBlockNum: number[] = []
const diffAreaOriginalLines: [number, number][] = []
let { onFinishEdit } = this._addToHistory(uri)
// TODO replace these with whatever block we're on initially if already started
const infoOfBlockNum: {
originalLines: [number, number], // 1-indexed
finalStartLine: number, // 1-indexed
originalCode: string,
}[] = []
// TODO replace all these with whatever block we're on initially if already started
let latestStreamLocationMutable: StreamLocationMutable | null = null
let currStreamingBlockNum = 0
let oldBlocks: ExtractedSearchReplaceBlock[] = []
// find block.orig in fileContents and return its range in file
const findTextInCode = (text: string, fileContents: string) => {
const idx = fileContents.indexOf(text)
if (idx === -1) return 'Not found' as const
const lastIdx = fileContents.lastIndexOf(text)
if (lastIdx !== idx) return 'Not unique' as const
const startLine = fileContents.substring(0, idx).split('\n').length
const numLines = text.split('\n').length
const endLine = startLine + numLines - 1
return [startLine, endLine]
const adding: Omit<DiffZone, 'diffareaid'> = {
type: 'DiffZone',
originalCode: originalFileCode,
startLine,
endLine,
_URI: uri,
_streamState: {
isStreaming: true,
streamRequestIdRef,
line: startLine,
},
_diffOfId: {}, // added later
_removeStylesFns: new Set(),
}
const diffZone = this._addDiffArea(adding)
this._onDidChangeStreaming.fire({ uri, diffareaid: diffZone.diffareaid })
this._onDidAddOrDeleteDiffZones.fire({ uri })
let { onFinishEdit } = this._addToHistory(uri)
const revertAndContinueHistory = () => {
this._undoHistory(uri)
@ -1225,90 +1270,55 @@ class EditCodeService extends Disposable implements IEditCodeService {
onFinishEdit = onFinishEdit_
}
const onDone = (errorMessage: false | string) => {
for (const blockNum in diffareaidOfBlockNum) {
const diffareaid = diffareaidOfBlockNum[blockNum]
const diffZone = this.diffAreaOfId[diffareaid]
if (diffZone?.type !== 'DiffZone') continue
diffZone._streamState = { isStreaming: false, }
this._onDidChangeStreaming.fire({ uri, diffareaid: diffZone.diffareaid })
}
this._refreshStylesAndDiffsInURI(uri)
if (errorMessage) {
this._notificationService.info(`Void had an error when running Apply: ${errorMessage}.\nIf this persists, feel free to [report](https://github.com/voideditor/void/issues/new) this error.`)
this._metricsService.capture('Error - Apply', { errorMessage })
this._undoHistory(uri)
}
onFinishEdit()
}
const onNewBlockStart = (blockNum: number, block: ExtractedSearchReplaceBlock): { errorStartingBlock?: undefined } | { errorStartingBlock: string } => {
console.log('STARTING BLOCK', JSON.stringify(block, null, 2))
const foundInCode = findTextInCode(block.orig, origFileContents)
const findTextInCodeWithAdjustedOffset = (blockNum: number, block: ExtractedSearchReplaceBlock): string | { originalLines: [number, number], currentLines: [number, number] } => {
const foundInCode = findTextInCode(block.orig, originalFileCode)
if (typeof foundInCode === 'string') {
console.log('Apply error:', foundInCode, '; trying again.')
return { errorStartingBlock: foundInCode }
return foundInCode
}
const [originalStart, originalEnd] = foundInCode
// compute line offset if there were changes in the past
let lineOffset = 0
// compute line offset given multiple changes
for (let i = 0; i < blockNum; i += 1) {
const [diffAreaOriginalStart, diffAreaOriginalEnd] = diffAreaOriginalLines[i]
console.log('ROIGGINAL!!!', diffAreaOriginalStart, diffAreaOriginalEnd)
if (diffAreaOriginalStart > originalEnd) continue
const { originalLines: [otherBlockOriginalStart, otherBlockOriginalEnd], } = infoOfBlockNum[i]
const finalCode = block.final
const diffareaid = diffareaidOfBlockNum[i]
const diffArea = this.diffAreaOfId[diffareaid]
if (otherBlockOriginalStart > originalEnd) continue
if (finalCode === null) continue
const numNewLines = diffArea.endLine - diffArea.startLine
const numOldLines = diffAreaOriginalEnd - diffAreaOriginalStart
console.log('NUM NEW', numNewLines, numOldLines)
const numNewLines = finalCode.split('\n').length
const numOldLines = otherBlockOriginalEnd - otherBlockOriginalStart + 1
lineOffset += numNewLines - numOldLines
}
const startLine = originalStart + lineOffset
const endLine = originalEnd + lineOffset
console.log('adding to', startLine, endLine)
const adding: Omit<DiffZone, 'diffareaid'> = {
type: 'DiffZone',
originalCode: block.orig,
startLine,
endLine,
_URI: uri,
_streamState: {
isStreaming: true,
streamRequestIdRef,
line: startLine,
},
_diffOfId: {}, // added later
_removeStylesFns: new Set(),
return {
originalLines: [originalStart, originalEnd],
currentLines: [originalStart + lineOffset, originalEnd + lineOffset],
}
const diffZone = this._addDiffArea(adding)
this._onDidChangeStreaming.fire({ uri, diffareaid: diffZone.diffareaid })
this._onDidAddOrDeleteDiffZones.fire({ uri })
diffareaidOfBlockNum.push(diffZone.diffareaid)
diffAreaOriginalLines.push([originalStart, originalEnd])
latestStreamLocationMutable = { line: diffZone.startLine, addedSplitYet: false, col: 1, originalCodeStartLine: 1 }
return { errorStartingBlock: undefined }
}
let latestStreamLocationMutable: StreamLocationMutable | null = null
const onDone = () => {
diffZone._streamState = { isStreaming: false, }
this._onDidChangeStreaming.fire({ uri, diffareaid: diffZone.diffareaid })
this._refreshStylesAndDiffsInURI(uri)
onFinishEdit()
}
// refresh now in case onText takes a while to get 1st message
this._refreshStylesAndDiffsInURI(uri)
let shouldSendAnotherMessage = true
let nMessagesSent = 0
// this generates >>>>>>> ORIGINAL <<<<<<< REPLACE blocks and and simultaneously applies it
let currStreamingBlockNum = 0
while (shouldSendAnotherMessage) {
shouldSendAnotherMessage = false
nMessagesSent += 1
@ -1324,49 +1334,73 @@ class EditCodeService extends Disposable implements IEditCodeService {
for (let blockNum = currStreamingBlockNum; blockNum < blocks.length; blockNum += 1) {
const block = blocks[blockNum]
if (block.state === 'done')
currStreamingBlockNum = blockNum
// if a block is done, finish it
if (block.state === 'done') {
console.log('FINISHING BLOCK')
if (block.state === 'writingOriginal') // must be done writing original
const { finalStartLine } = infoOfBlockNum[blockNum]
const numLines = block.final.split('\n').length
this._writeText(uri, block.final,
{ startLineNumber: finalStartLine, startColumn: 1, endLineNumber: finalStartLine + numLines, endColumn: Number.MAX_SAFE_INTEGER }, // 1-indexed
{ shouldRealignDiffAreas: true }
)
currStreamingBlockNum = blockNum + 1
}
// must be done writing original to stream code
if (block.state === 'writingOriginal')
continue
// if this is the first time we're seeing this block, add it as a diffarea
if (!(blockNum in diffareaidOfBlockNum)) {
console.log('FULLTEXT!!!!!\n', fullText)
const { errorStartingBlock } = onNewBlockStart(blockNum, block)
// if this is the first time we're seeing this block, add it as a blocknum
if (!(blockNum in infoOfBlockNum)) {
console.log('----FULLTEXT!!!!!----\n', blockNum, fullText)
if (errorStartingBlock) {
const pos = findTextInCodeWithAdjustedOffset(blockNum, block)
console.log('OFFSET', pos)
if (typeof pos === 'string') {
const errorStartingBlock = pos
console.log('ERROR STARTING BLOCK SPOT!!!!!', errorStartingBlock)
const errMsgForLLM = errorStartingBlock === 'Not found' ?
'I interrupted you because the latest ORIGINAL code could not be found in the file. Please output all SEARCH/REPLACE blocks again, making sure the code in ORIGINAL is identical to a code snippet in the file.'
: errorStartingBlock === 'Not unique' ?
'I interrupted you because the latest ORIGINAL code shows up multiple times in the file. Please output all SEARCH/REPLACE blocks again, making sure the code in each ORIGINAL section is unique in the file.'
: ''
messages.push(
{ role: 'assistant', content: fullText }, // latest output
{ role: 'user', content: errMsgForLLM } // user explanation of what's wrong
)
if (streamRequestIdRef.current) this._llmMessageService.abort(streamRequestIdRef.current)
shouldSendAnotherMessage = true
revertAndContinueHistory()
return
continue
}
infoOfBlockNum.push({
originalLines: pos.originalLines,
finalStartLine: pos.currentLines[0],
originalCode: block.orig,
})
latestStreamLocationMutable = { line: diffZone.startLine, addedSplitYet: false, col: 1, originalCodeStartLine: 1 }
}
const deltaFinalText = block.final.substring((oldBlocks[blockNum]?.final ?? '').length, Infinity)
oldBlocks = blocks
// write new text to diffarea
const diffareaid = diffareaidOfBlockNum[blockNum]
const diffZone = this.diffAreaOfId[diffareaid]
if (diffZone?.type !== 'DiffZone') continue
if (!latestStreamLocationMutable) continue
this._writeStreamedDiffZoneLLMText(diffZone, block.final, deltaFinalText, latestStreamLocationMutable)
// should always be in streaming state here
if (!diffZone._streamState.isStreaming) {
console.error('DiffZone was not in streaming state in _initializeSearchAndReplaceStream')
continue
}
const latestEndLine = this._writeStreamedDiffZoneLLMText(uri, diffZone.originalCode, block.final, deltaFinalText, latestStreamLocationMutable)
diffZone._streamState.line = (diffZone.startLine - 1) + latestEndLine // change coordinate systems from originalCode to full file
} // end for
this._refreshStylesAndDiffsInURI(uri)
@ -1379,25 +1413,39 @@ class EditCodeService extends Disposable implements IEditCodeService {
const blocks = extractSearchReplaceBlocks(fullText)
if (blocks.length === 0) {
this._notificationService.info(`Void: When running Apply, your model didn't output any changes we recognized. You might need to use a smarter model for Apply.`)
this._notificationService.info(`Void: When running Apply, your model didn't output any changes that Void recognized. You might need to use a smarter model for Apply.`)
}
for (let blockNum = 0; blockNum < blocks.length; blockNum += 1) {
const block = blocks[blockNum]
const diffareaid = diffareaidOfBlockNum[blockNum]
const diffZone = this.diffAreaOfId[diffareaid]
if (diffZone?.type !== 'DiffZone') continue
// writeover the whole file
let newCode = originalFileCode
for (let blockNum = infoOfBlockNum.length - 1; blockNum >= 0; blockNum -= 1) {
const { originalLines } = infoOfBlockNum[blockNum]
const finalCode = blocks[blockNum].final
this._writeText(uri, block.final,
{ startLineNumber: diffZone.startLine, startColumn: 1, endLineNumber: diffZone.endLine, endColumn: Number.MAX_SAFE_INTEGER }, // 1-indexed
if (finalCode === null) continue
const [originalStart, originalEnd] = originalLines
const lines = newCode.split('\n')
newCode = [
...lines.slice(0, (originalStart - 1)),
...finalCode.split('\n'),
...lines.slice((originalEnd - 1) + 1, Infinity)
].join('\n')
}
const numLines = this._getNumLines(uri)
if (numLines !== null) {
this._writeText(uri, newCode,
{ startLineNumber: 1, startColumn: 1, endLineNumber: numLines, endColumn: Number.MAX_SAFE_INTEGER },
{ shouldRealignDiffAreas: true }
)
}
onDone(false)
onDone()
},
onError: (e) => {
console.log('ERROR in SearchReplace:', e.message)
onDone(e.message)
this._notifyError(e)
onDone()
this._undoHistory(uri)
},
})
@ -1409,7 +1457,7 @@ class EditCodeService extends Disposable implements IEditCodeService {
private _initializeRewriteStream(opts: StartApplyingOpts): DiffZone | undefined {
private _initializeWriteoverStream(opts: StartApplyingOpts): DiffZone | undefined {
const { from } = opts
@ -1516,7 +1564,7 @@ class EditCodeService extends Disposable implements IEditCodeService {
else { throw new Error(`featureName ${from} is invalid`) }
const onDone = (hadError: boolean) => {
const onDone = () => {
diffZone._streamState = { isStreaming: false, }
this._onDidChangeStreaming.fire({ uri, diffareaid: diffZone.diffareaid })
@ -1528,11 +1576,6 @@ class EditCodeService extends Disposable implements IEditCodeService {
}
this._refreshStylesAndDiffsInURI(uri)
onFinishEdit()
// if had error, revert!
if (hadError) {
this._undoHistory(diffZone._URI)
}
}
// refresh now in case onText takes a while to get 1st message
@ -1566,7 +1609,9 @@ class EditCodeService extends Disposable implements IEditCodeService {
fullText += prevIgnoredSuffix + newText // full text, including ```, etc
const [croppedText, deltaCroppedText, croppedSuffix] = extractText(fullText, newText.length)
this._writeStreamedDiffZoneLLMText(diffZone, croppedText, deltaCroppedText, latestStreamInfoMutable)
const latestEndLine = this._writeStreamedDiffZoneLLMText(uri, originalCode, croppedText, deltaCroppedText, latestStreamInfoMutable)
diffZone._streamState.line = (diffZone.startLine - 1) + latestEndLine // change coordinate systems from originalCode to full file
this._refreshStylesAndDiffsInURI(uri)
prevIgnoredSuffix = croppedSuffix
@ -1579,26 +1624,12 @@ class EditCodeService extends Disposable implements IEditCodeService {
{ startLineNumber: diffZone.startLine, startColumn: 1, endLineNumber: diffZone.endLine, endColumn: Number.MAX_SAFE_INTEGER }, // 1-indexed
{ shouldRealignDiffAreas: true }
)
onDone(false)
onDone()
},
onError: (e) => {
const details = errorDetails(e.fullError)
this._notificationService.notify({
severity: Severity.Warning,
message: `Void Error: ${e.message}`,
actions: {
secondary: [{
id: 'void.onerror.opensettings',
enabled: true,
label: `Open Void's settings`,
tooltip: '',
class: undefined,
run: () => { this._commandService.executeCommand(VOID_OPEN_SETTINGS_ACTION_ID) }
}]
},
source: details ? `(Hold ${isMacintosh ? 'Option' : 'Alt'} to hover) - ${details}` : undefined
})
onDone(true)
this._notifyError(e)
onDone()
this._undoHistory(uri)
},
})