searchRepalce

This commit is contained in:
Andrew Pareles 2025-02-12 00:34:24 -08:00
parent fc97949887
commit d504daffa5
2 changed files with 147 additions and 90 deletions

View file

@ -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<ExtractedCodeBlock[] | undefined> {
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<DiffZone, 'diffareaid'> = {
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<DiffZone, 'diffareaid'> = {
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

View file

@ -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