From d504daffa50668e8b088e127a6ceceb699151264 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Wed, 12 Feb 2025 00:34:24 -0800 Subject: [PATCH] searchRepalce --- .../void/browser/inlineDiffsService.ts | 229 +++++++++++------- .../contrib/void/browser/prompt/prompts.ts | 8 +- 2 files changed, 147 insertions(+), 90 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/inlineDiffsService.ts b/src/vs/workbench/contrib/void/browser/inlineDiffsService.ts index b90998b1..88dc49e6 100644 --- a/src/vs/workbench/contrib/void/browser/inlineDiffsService.ts +++ b/src/vs/workbench/contrib/void/browser/inlineDiffsService.ts @@ -25,7 +25,7 @@ import * as dom from '../../../../base/browser/dom.js'; import { Widget } from '../../../../base/browser/ui/widget.js'; import { URI } from '../../../../base/common/uri.js'; import { IConsistentEditorItemService, IConsistentItemService } from './helperServices/consistentItemService.js'; -import { voidPrefixAndSuffix, ctrlKStream_userMessage, ctrlKStream_systemMessage, fastApply_rewritewholething_userMessage, fastApply_rewritewholething_systemMessage, defaultFimTags, fastApply_searchreplace_systemMessage, fastApply_searchreplace_userMessage, tripleTick } from './prompt/prompts.js'; +import { voidPrefixAndSuffix, ctrlKStream_userMessage, ctrlKStream_systemMessage, fastApply_rewritewholething_userMessage, fastApply_rewritewholething_systemMessage, defaultQuickEditFimTags, tripleTick } from './prompt/prompts.js'; import { mountCtrlK } from '../browser/react/out/quick-edit-tsx/index.js' import { QuickEditPropsType } from './quickEditActions.js'; @@ -39,9 +39,9 @@ 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, _InternalLLMChatMessage, errorDetails } from '../common/llmMessageTypes.js'; import { IMetricsService } from '../common/metricsService.js'; -import { string } from 'zod'; +import { VSReadFile } from './helpers/readFile.js'; const configOfBG = (color: Color) => { return { dark: color, light: color, hcDark: color, hcLight: color, } @@ -101,20 +101,19 @@ const getLeadingWhitespacePx = (editor: ICodeEditor, startLine: number): number return paddingLeft; }; -// similar to ServiceLLM + + export type StartApplyingOpts = { from: 'QuickEdit'; + type: 'rewrite'; diffareaid: number; // id of the CtrlK area (contains text selection) } | { - from: 'Chat'; + from: 'ClickApply'; + type: 'searchReplace' | 'rewrite'; applyStr: string; - applyBoxId: string; -} | { - from: 'Autocomplete'; - range: IRange; - userMessage: string; } + export type AddCtrlKOpts = { startLine: number, endLine: number, @@ -139,6 +138,11 @@ export type Diff = { +type ExtractedCodeBlock = { + state: 'writingOriginal' | 'writingFinal' | 'done', + orig: string, + final: string, +} // _ means anything we don't include if we clone it // DiffArea.originalStartLine is the line in originalCode (not the file) @@ -998,7 +1002,7 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService { // @throttle(100) - private _writeStreamedDiffZoneLLMText(diffZone: DiffZone, llmText: string, deltaText: string, latest: { line: number, col: number, addedSplitYet: boolean, originalCodeStartLine: number }) { + private _writeStreamedDiffZoneLLMText(diffZone: DiffZone, llmText: string, deltaText: string, latestMutable: { line: number, col: number, addedSplitYet: boolean, originalCodeStartLine: number }) { // ----------- 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 @@ -1037,39 +1041,39 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService { // at the start, add a newline between the stream and originalCode to make reasoning easier - if (!latest.addedSplitYet) { + if (!latestMutable.addedSplitYet) { this._writeText(uri, '\n', - { startLineNumber: latest.line, startColumn: latest.col, endLineNumber: latest.line, endColumn: latest.col, }, + { startLineNumber: latestMutable.line, startColumn: latestMutable.col, endLineNumber: latestMutable.line, endColumn: latestMutable.col, }, { shouldRealignDiffAreas: true } ) - latest.addedSplitYet = true + latestMutable.addedSplitYet = true } // insert deltaText at latest line and col this._writeText(uri, deltaText, - { startLineNumber: latest.line, startColumn: latest.col, endLineNumber: latest.line, endColumn: latest.col }, + { startLineNumber: latestMutable.line, startColumn: latestMutable.col, endLineNumber: latestMutable.line, endColumn: latestMutable.col }, { shouldRealignDiffAreas: true } ) - latest.line += deltaText.split('\n').length - 1 + latestMutable.line += deltaText.split('\n').length - 1 const lastNewlineIdx = deltaText.lastIndexOf('\n') - latest.col = lastNewlineIdx === -1 ? latest.col + deltaText.length : deltaText.length - lastNewlineIdx + latestMutable.col = lastNewlineIdx === -1 ? latestMutable.col + deltaText.length : deltaText.length - lastNewlineIdx // delete or insert to get original up to speed - if (latest.originalCodeStartLine < originalCodeStartLine) { + if (latestMutable.originalCodeStartLine < originalCodeStartLine) { // moved up, delete - const numLinesDeleted = originalCodeStartLine - latest.originalCodeStartLine + const numLinesDeleted = originalCodeStartLine - latestMutable.originalCodeStartLine this._writeText(uri, '', - { startLineNumber: latest.line, startColumn: latest.col, endLineNumber: latest.line + numLinesDeleted, endColumn: Number.MAX_SAFE_INTEGER, }, + { startLineNumber: latestMutable.line, startColumn: latestMutable.col, endLineNumber: latestMutable.line + numLinesDeleted, endColumn: Number.MAX_SAFE_INTEGER, }, { shouldRealignDiffAreas: true } ) } - else if (latest.originalCodeStartLine > originalCodeStartLine) { - this._writeText(uri, '\n' + diffZone.originalCode.split('\n').slice((originalCodeStartLine - 1), (latest.originalCodeStartLine - 1) - 1 + 1).join('\n'), - { startLineNumber: latest.line, startColumn: latest.col, endLineNumber: latest.line, endColumn: latest.col }, + else if (latestMutable.originalCodeStartLine > originalCodeStartLine) { + this._writeText(uri, '\n' + diffZone.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 } ) } - latest.originalCodeStartLine = originalCodeStartLine + 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 @@ -1187,7 +1191,7 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService { public startApplying(opts: StartApplyingOpts) { - const addedDiffZone = this._initializeStartApplying(opts) + const addedDiffZone = this._initializeRewriteStream(opts) return addedDiffZone?.diffareaid } @@ -1209,12 +1213,12 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService { } - private _generateSearchAndReplaceBlocks({ filename, applyStr }: { filename: URI, applyStr: string }): DiffZone | undefined { + private async _generateSearchAndReplaceBlocks({ uri, applyStr }: { uri: URI, applyStr: string }): Promise { const ORIGINAL = `<<<<<<< ORIGINAL` const DIVIDER = `=======` const FINAL = `>>>>>>> UPDATED` - const searchReplaceGenSysMessage = `\ + const searchReplaceSysMessage = `\ You are a coding assistant that generates SEARCH/REPLACE code blocks that will be used to edit a file. A SEARCH/REPLACE block describes the code before and after a change. Here is the format: @@ -1228,11 +1232,12 @@ You will be given the original file \`ORIGINAL_FILE\` and a description of a cha Output SEARCH/REPLACE blocks to edit the file according to the desired change. You may output multiple SEARCH/REPLACE blocks. Directions: -1. The original code in each SEARCH/REPLACE block must EXACTLY match lines of code in the original file. -2. The original code in each SEARCH/REPLACE block should include enough text to uniquely identify the change in the file. -3. The original code cannot be empty. -4. The SEARCH/REPLACE blocks you generate will be applied immediately, and so they **MUST** produce a file that the user can run IMMEDIATELY, with NO ERRORS. +1. Your OUTPUT should consist ONLY of SEARCH/REPLACE blocks. Do NOT output any text or explanations before or after this. +2. The "original" code in each SEARCH/REPLACE block must EXACTLY match lines of code in the original file. +3. The "original" code in each SEARCH/REPLACE block should include enough text to uniquely identify the change in the file. +4. The SEARCH/REPLACE blocks you generate will be applied immediately, and so they **MUST** produce a file that the user can run IMMEDIATELY. - Make sure you add all necessary imports. + - Make sure the "final" code is complete and will not result in syntax/lint errors. 5. Follow coding convention (spaces, semilcolons, comments, etc). ## EXAMPLE 1 @@ -1263,6 +1268,18 @@ ${FINAL} ${tripleTick[1]} ` + + const searchReplaceUserMessage = ({ originalCode, applyStr }: { originalCode: string, applyStr: string }) => `\ +ORIGINAL_FILE +${originalCode} + +CHANGE +${applyStr} + +INSTRUCTIONS +Please output SEARCH/REPLACE blocks to make the change. Return ONLY your suggested SEARCH/REPLACE blocks, without any explanation. +` + function endsWithAnyPrefixOf(str: string, anyPrefix: string) { // for each prefix for (let i = anyPrefix.length; i >= 0; i--) { @@ -1279,11 +1296,7 @@ ${tripleTick[1]} const FINAL_ = '\n' + FINAL - const blocks: ({ - state: 'writingOriginal' | 'writingFinal' | 'done', - orig: string, - final: string, - })[] = [] + const blocks: ExtractedCodeBlock[] = [] let i = 0 // search i and beyond (this is done by plain index, not by line number. much simpler this way) while (true) { @@ -1329,58 +1342,115 @@ ${tripleTick[1]} state: 'done' }) } - } - - let uri: URI - - const uri_ = this._getActiveEditorURI() - if (!uri_) return - uri = uri_ + // generate search/replace block text + const fileContents = await VSReadFile(this._modelService, uri) + if (fileContents === 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 }) + const userMessageContent = searchReplaceUserMessage({ originalCode: fileContents, applyStr: applyStr }) + const messages: LLMChatMessage[] = [ + { role: 'system', content: searchReplaceSysMessage }, + { role: 'user', content: userMessageContent } + ] + let streamRequestIdRef: { current: string | null } = { current: null } + + const diffareaidOfBlockNum: number[] = [] + + const onText = ({ newText, fullText }: { newText: string, fullText: string }) => { + const blocks = extractBlocks(fullText) + + // find block.orig in fileContents and return its range in file + const findBlock = (block: { orig: string }, fileContents: string) => { + const origText = block.orig + const idx = fileContents.indexOf(origText) + if (idx === -1) return 'Not found' as const + const lastIdx = fileContents.lastIndexOf(origText) + if (lastIdx !== idx) return 'Not unique' as const + + const startLine = fileContents.substring(0, idx).split('\n').length + const numLines = origText.split('\n').length + const endLine = startLine + numLines - 1 + + return [startLine, endLine] + } + + let latestStreamInfoMutable: any = {} - // generate search/replace block text + for (let blockNum = 0; blockNum < blocks.length; blockNum += 1) { + const block = blocks[blockNum] + const foundInCode = findBlock(block, fileContents) + if (typeof foundInCode === 'string') { + console.log('ERROR!!!!', foundInCode) + continue + } + const [startLine, endLine] = foundInCode - // parse output, make sure: 1. not redundant search and 2. valid output (retry if not) + if (block.state === 'writingOriginal') continue - // apply change to string, check if it looks good (retry if not) + // if should add new diffarea + if (blockNum > diffareaidOfBlockNum.length) { + const adding: Omit = { + type: 'DiffZone', + originalCode: block.orig, + 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 }) - // TODO check dirty + diffareaidOfBlockNum.push(diffZone.diffareaid) - // + latestStreamInfoMutable = { line: diffZone.startLine, addedSplitYet: false, col: 1, originalCodeStartLine: 1 } + } - // in ctrl+L the start and end lines are the full document + const diffareaid = diffareaidOfBlockNum[blockNum] + const diffZone = this.diffAreaOfId[diffareaid] + if (diffZone.type !== 'DiffZone') continue - const numLines = this._getNumLines(uri) - if (numLines === null) return + this._writeStreamedDiffZoneLLMText(diffZone, fullText, newText, latestStreamInfoMutable) + this._refreshStylesAndDiffsInURI(uri) + } - let startLine: number - let endLine: number - - startLine = 1 - endLine = numLines - - const currentFileStr = this._readURI(uri) - if (currentFileStr === null) return - const originalCode = currentFileStr.split('\n').slice((startLine - 1), (endLine - 1) + 1).join('\n') + } - // 1b find the start and end line that the search block lives on (if can't find it, retry 1a) + + // TODO turn this into a service and provide it + streamRequestIdRef.current = this._llmMessageService.sendLLMMessage({ + messagesType: 'chatMessages', + useProviderFor: 'FastApply', + logging: { loggingName: `generateSearchAndReplace` }, + messages, + onText: ({ newText, fullText }) => { onText({ newText, fullText }) }, + onFinalMessage: ({ fullText }) => { }, + onError: (e) => { console.log('ERROR', e) }, + + }) + + } - - private _initializeStartApplying(opts: StartApplyingOpts): DiffZone | undefined { + private _initializeRewriteStream(opts: StartApplyingOpts): DiffZone | undefined { const { from } = opts @@ -1388,7 +1458,7 @@ ${tripleTick[1]} let endLine: number let uri: URI - if (from === 'Chat') { + if (from === 'ClickApply') { const uri_ = this._getActiveEditorURI() if (!uri_) return @@ -1430,8 +1500,7 @@ ${tripleTick[1]} const { onFinishEdit } = this._addToHistory(uri) // __TODO__ let users customize modelFimTags - const isOllamaFIM = false // this._voidSettingsService.state.modelSelectionOfFeature['Ctrl+K']?.providerName === 'ollama' - const modelFimTags = defaultFimTags + const quickEditFIMTags = defaultQuickEditFimTags const adding: Omit = { type: 'DiffZone', @@ -1462,7 +1531,7 @@ ${tripleTick[1]} // now handle messages let messages: LLMChatMessage[] - if (from === 'Chat') { + if (from === 'ClickApply') { const userContent = fastApply_rewritewholething_userMessage({ originalCode, applyStr: opts.applyStr, uri }) messages = [ { role: 'system', content: fastApply_rewritewholething_systemMessage, }, @@ -1476,25 +1545,14 @@ ${tripleTick[1]} const { _mountInfo } = ctrlKZone const instructions = _mountInfo?.textAreaRef.current?.value ?? '' - // __TODO__ use Ollama's FIM api, if (isOllamaFIM) {...} else: const { prefix, suffix } = voidPrefixAndSuffix({ fullFileStr: currentFileStr, startLine, endLine }) - // if (isOllamaFIM) { - // messages = { - // type: 'ollamaFIM', - // prefix, - // suffix, - // } - - // } - // else { const language = filenameToVscodeLanguage(uri.fsPath) ?? '' - const userContent = ctrlKStream_userMessage({ selection: originalCode, instructions: instructions, prefix, suffix, isOllamaFIM: false, fimTags: modelFimTags, language }) + const userContent = ctrlKStream_userMessage({ selection: originalCode, instructions: instructions, prefix, suffix, isOllamaFIM: false, fimTags: quickEditFIMTags, language }) // type: 'messages', messages = [ - { role: 'system', content: ctrlKStream_systemMessage({ fimTags: modelFimTags }), }, + { role: 'system', content: ctrlKStream_systemMessage({ quickEditFIMTags: quickEditFIMTags }), }, { role: 'user', content: userContent, } ] - // } } else { throw new Error(`featureName ${from} is invalid`) } @@ -1524,16 +1582,15 @@ ${tripleTick[1]} const extractText = (fullText: string, recentlyAddedTextLen: number) => { if (from === 'QuickEdit') { - if (isOllamaFIM) return fullText - return extractCodeFromFIM({ text: fullText, recentlyAddedTextLen, midTag: modelFimTags.midTag }) + return extractCodeFromFIM({ text: fullText, recentlyAddedTextLen, midTag: quickEditFIMTags.midTag }) } - else if (from === 'Chat') { + else if (from === 'ClickApply') { return extractCodeFromRegular({ text: fullText, recentlyAddedTextLen }) } throw 1 } - const latestStreamInfo = { line: diffZone.startLine, addedSplitYet: false, col: 1, originalCodeStartLine: 1 } + const latestStreamInfoMutable = { line: diffZone.startLine, addedSplitYet: false, col: 1, originalCodeStartLine: 1 } // state used in onText: let fullText = '' @@ -1541,7 +1598,7 @@ ${tripleTick[1]} streamRequestIdRef.current = this._llmMessageService.sendLLMMessage({ messagesType: 'chatMessages', - useProviderFor: opts.from === 'Chat' ? 'FastApply' : 'Ctrl+K', + useProviderFor: opts.from === 'ClickApply' ? 'FastApply' : 'Ctrl+K', logging: { loggingName: `startApplying - ${from}` }, messages, onText: ({ newText: newText_ }) => { @@ -1550,7 +1607,7 @@ ${tripleTick[1]} fullText += prevIgnoredSuffix + newText const [text, deltaText, ignoredSuffix] = extractText(fullText, newText.length) - this._writeStreamedDiffZoneLLMText(diffZone, text, deltaText, latestStreamInfo) + this._writeStreamedDiffZoneLLMText(diffZone, text, deltaText, latestStreamInfoMutable) this._refreshStylesAndDiffsInURI(uri) prevIgnoredSuffix = ignoredSuffix diff --git a/src/vs/workbench/contrib/void/browser/prompt/prompts.ts b/src/vs/workbench/contrib/void/browser/prompt/prompts.ts index 1925bccc..428625fd 100644 --- a/src/vs/workbench/contrib/void/browser/prompt/prompts.ts +++ b/src/vs/workbench/contrib/void/browser/prompt/prompts.ts @@ -279,19 +279,19 @@ export const voidPrefixAndSuffix = ({ fullFileStr, startLine, endLine }: { fullF } -export type FimTagsType = { +export type QuickEditFimTagsType = { preTag: string, sufTag: string, midTag: string } -export const defaultFimTags: FimTagsType = { +export const defaultQuickEditFimTags: QuickEditFimTagsType = { preTag: 'ABOVE', sufTag: 'BELOW', midTag: 'SELECTION', } // this should probably be longer -export const ctrlKStream_systemMessage = ({ fimTags: { preTag, midTag, sufTag } }: { fimTags: FimTagsType }) => { +export const ctrlKStream_systemMessage = ({ quickEditFIMTags: { preTag, midTag, sufTag } }: { quickEditFIMTags: QuickEditFimTagsType }) => { return `\ You are a FIM (fill-in-the-middle) coding assistant. Your task is to fill in the middle SELECTION marked by <${midTag}> tags. @@ -307,7 +307,7 @@ Instructions: } export const ctrlKStream_userMessage = ({ selection, prefix, suffix, instructions, fimTags, isOllamaFIM, language }: { - selection: string, prefix: string, suffix: string, instructions: string, fimTags: FimTagsType, language: string, + selection: string, prefix: string, suffix: string, instructions: string, fimTags: QuickEditFimTagsType, language: string, isOllamaFIM: false, // we require this be false for clarity }) => { const { preTag, sufTag, midTag } = fimTags