From d698e6f3c12117b286fdf5a9e85e1e7a71ddc566 Mon Sep 17 00:00:00 2001 From: Mathew Pareles Date: Tue, 11 Feb 2025 18:42:40 -0800 Subject: [PATCH] prepare fast apply --- .../void/browser/inlineDiffsService.ts | 217 +++++++++++++++++- .../contrib/void/browser/prompt/prompts.ts | 40 +++- .../react/src/markdown/ChatMarkdownRender.tsx | 43 ++-- .../react/src/sidebar-tsx/SidebarChat.tsx | 12 +- .../void/browser/searchAndReplaceService.ts | 76 ++++++ 5 files changed, 365 insertions(+), 23 deletions(-) create mode 100644 src/vs/workbench/contrib/void/browser/searchAndReplaceService.ts diff --git a/src/vs/workbench/contrib/void/browser/inlineDiffsService.ts b/src/vs/workbench/contrib/void/browser/inlineDiffsService.ts index d4d7d065..546065e8 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_userMessage, fastApply_systemMessage, defaultFimTags } from './prompt/prompts.js'; +import { voidPrefixAndSuffix, ctrlKStream_userMessage, ctrlKStream_systemMessage, fastApply_rewritewholething_userMessage, fastApply_rewritewholething_systemMessage, defaultFimTags, fastApply_searchreplace_systemMessage, fastApply_searchreplace_userMessage } from './prompt/prompts.js'; import { ILLMMessageService } from '../../../../platform/void/common/llmMessageService.js'; import { mountCtrlK } from '../browser/react/out/quick-edit-tsx/index.js' @@ -107,6 +107,7 @@ export type StartApplyingOpts = { } | { from: 'Chat'; applyStr: string; + applyBoxId: string; } | { from: 'Autocomplete'; range: IRange; @@ -1206,6 +1207,216 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService { return null } + private _generateSearchAndReplaceBlocks({ filename, applyStr }: { filename: URI, applyStr: string }): DiffZone | undefined { + + // call LLM to generate search and replace blocks (outputs something like [{search: 'this is my code', replace: 'this is m'}, ... ]) + + // 1a output search block + + let uri: URI + + const uri_ = this._getActiveEditorURI() + if (!uri_) return + uri = uri_ + + // reject all diffZones on this URI, adding to history (there can't possibly be overlap after this) + this.removeDiffAreas({ uri, behavior: 'reject', removeCtrlKs: true }) + + // in ctrl+L the start and end lines are the full document + + const numLines = this._getNumLines(uri) + if (numLines === null) return + + 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) + + + + + + + + let streamRequestIdRef: { current: string | null } = { current: null } + + + // add to history + 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 adding: Omit = { + type: 'DiffZone', + originalCode, + 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 }) + + if (from === 'QuickEdit') { + const { diffareaid } = opts + const ctrlKZone = this.diffAreaOfId[diffareaid] + if (ctrlKZone.type !== 'CtrlKZone') return + + ctrlKZone._linkedStreamingDiffZone = diffZone.diffareaid + } + + // now handle messages + let messages: LLMChatMessage[] + + if (from === 'Chat') { + const userContent = fastApply_searchreplace_userMessage({ originalCode, applyStr: opts.applyStr, uri }) + messages = [ + { role: 'system', content: fastApply_rewritewholething_systemMessage, }, + { role: 'user', content: userContent, } + ] + } + else if (from === 'QuickEdit') { + const { diffareaid } = opts + const ctrlKZone = this.diffAreaOfId[diffareaid] + if (ctrlKZone.type !== 'CtrlKZone') return + 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 }) + // type: 'messages', + messages = [ + { role: 'system', content: ctrlKStream_systemMessage({ fimTags: modelFimTags }), }, + { role: 'user', content: userContent, } + ] + // } + } + else { throw new Error(`featureName ${from} is invalid`) } + + + const onDone = (hadError: boolean) => { + diffZone._streamState = { isStreaming: false, } + this._onDidChangeStreaming.fire({ uri, diffareaid: diffZone.diffareaid }) + + if (from === 'QuickEdit') { + const ctrlKZone = this.diffAreaOfId[opts.diffareaid] as CtrlKZone + + ctrlKZone._linkedStreamingDiffZone = null + this._deleteCtrlKZone(ctrlKZone) + } + 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 + this._refreshStylesAndDiffsInURI(uri) + + + const extractText = (fullText: string, recentlyAddedTextLen: number) => { + if (from === 'QuickEdit') { + if (isOllamaFIM) return fullText + return extractCodeFromFIM({ text: fullText, recentlyAddedTextLen, midTag: modelFimTags.midTag }) + } + else if (from === 'Chat') { + return extractCodeFromRegular({ text: fullText, recentlyAddedTextLen }) + } + throw 1 + } + + const latestStreamInfo = { line: diffZone.startLine, addedSplitYet: false, col: 1, originalCodeStartLine: 1 } + + + // state used in onText: + let fullText = '' + let prevIgnoredSuffix = '' + + streamRequestIdRef.current = this._llmMessageService.sendLLMMessage({ + messagesType: 'chatMessages', + useProviderFor: opts.from === 'Chat' ? 'FastApply' : 'Ctrl+K', + logging: { loggingName: `startApplying - ${from}` }, + messages, + onText: ({ newText: newText_ }) => { + + const newText = prevIgnoredSuffix + newText_ // add the previously ignored suffix because it's no longer the suffix! + fullText += prevIgnoredSuffix + newText + + const [text, deltaText, ignoredSuffix] = extractText(fullText, newText.length) + this._writeStreamedDiffZoneLLMText(diffZone, text, deltaText, latestStreamInfo) + this._refreshStylesAndDiffsInURI(uri) + + prevIgnoredSuffix = ignoredSuffix + }, + onFinalMessage: ({ fullText }) => { + // console.log('DONE! FULL TEXT\n', extractText(fullText), diffZone.startLine, diffZone.endLine) + // at the end, re-write whole thing to make sure no sync errors + const [text, _] = extractText(fullText, 0) + this._writeText(uri, text, + { startLineNumber: diffZone.startLine, startColumn: 1, endLineNumber: diffZone.endLine, endColumn: Number.MAX_SAFE_INTEGER }, // 1-indexed + { shouldRealignDiffAreas: true } + ) + onDone(false) + }, + 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 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) + }, + + }) + + return diffZone + + } + + private _initializeStartApplying(opts: StartApplyingOpts): DiffZone | undefined { @@ -1290,9 +1501,9 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService { let messages: LLMChatMessage[] if (from === 'Chat') { - const userContent = fastApply_userMessage({ originalCode, applyStr: opts.applyStr, uri }) + const userContent = fastApply_rewritewholething_userMessage({ originalCode, applyStr: opts.applyStr, uri }) messages = [ - { role: 'system', content: fastApply_systemMessage, }, + { role: 'system', content: fastApply_rewritewholething_systemMessage, }, { role: 'user', content: userContent, } ] } diff --git a/src/vs/workbench/contrib/void/browser/prompt/prompts.ts b/src/vs/workbench/contrib/void/browser/prompt/prompts.ts index 7dc23d53..44e70e44 100644 --- a/src/vs/workbench/contrib/void/browser/prompt/prompts.ts +++ b/src/vs/workbench/contrib/void/browser/prompt/prompts.ts @@ -183,7 +183,7 @@ export const chat_userMessage = async (instructions: string, selections: Staging -export const fastApply_systemMessage = `\ +export const fastApply_rewritewholething_systemMessage = `\ You are a coding assistant that re-writes an entire file to make a change. You are given the original file \`ORIGINAL_FILE\` and a change \`CHANGE\`. Directions: @@ -195,7 +195,7 @@ Directions: -export const fastApply_userMessage = ({ originalCode, applyStr, uri }: { originalCode: string, applyStr: string, uri: URI }) => { +export const fastApply_rewritewholething_userMessage = ({ originalCode, applyStr, uri }: { originalCode: string, applyStr: string, uri: URI }) => { const language = filenameToVscodeLanguage(uri.fsPath) ?? '' @@ -218,6 +218,42 @@ Please finish writing the new file by applying the change to the original file. +export const fastApply_searchreplace_systemMessage = `\ +You are a coding assistant that re-writes an entire file to make a change. You are given the original file \`ORIGINAL_FILE\` and a change \`CHANGE\`. + +Directions: +1. Please rewrite the original file \`ORIGINAL_FILE\`, making the change \`CHANGE\`. You must completely re-write the whole file. +2. Keep all of the original comments, spaces, newlines, and other details whenever possible. +3. ONLY output the full new file. Do not add any other explanations or text. +` + + +export const fastApply_searchreplace_userMessage = ({ originalCode, applyStr, uri }: { originalCode: string, applyStr: string, uri: URI }) => { + + const language = filenameToVscodeLanguage(uri.fsPath) ?? '' + + return `\ +ORIGINAL_FILE +\`\`\`${language} +${originalCode} +\`\`\` + +CHANGE +\`\`\` +${applyStr} +\`\`\` + +INSTRUCTIONS +Please finish writing the new file by applying the change to the original file. Return ONLY the completion of the file, without any explanation. +` +} + + + + + + + export const voidPrefixAndSuffix = ({ fullFileStr, startLine, endLine }: { fullFileStr: string, startLine: number, endLine: number }) => { diff --git a/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx b/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx index 15f67d9a..86afcc33 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx @@ -6,7 +6,8 @@ import React, { JSX, useCallback, useEffect, useState } from 'react' import { marked, MarkedToken, Token } from 'marked' import { BlockCode } from './BlockCode.js' -import { useAccessor } from '../util/services.js' +import { useAccessor, useChatThreadsState, useChatThreadsStreamState } from '../util/services.js' +import { ChatLocation, getApplyBoxId, } from '../../../searchAndReplaceService.js' import { nameToVscodeLanguage } from '../../../helpers/detectLanguage.js' @@ -18,7 +19,7 @@ enum CopyButtonState { const COPY_FEEDBACK_TIMEOUT = 1000 // amount of time to say 'Copied!' -const CodeButtonsOnHover = ({ text }: { text: string }) => { +const ApplyButtonsOnHover = ({ applyStr, applyBoxId }: { applyStr: string, applyBoxId: string }) => { const accessor = useAccessor() const [copyButtonState, setCopyButtonState] = useState(CopyButtonState.Copy) @@ -36,22 +37,24 @@ const CodeButtonsOnHover = ({ text }: { text: string }) => { }, [copyButtonState]) const onCopy = useCallback(() => { - clipboardService.writeText(text) + clipboardService.writeText(applyStr) .then(() => { setCopyButtonState(CopyButtonState.Copied) }) .catch(() => { setCopyButtonState(CopyButtonState.Error) }) - metricsService.capture('Copy Code', { length: text.length }) // capture the length only + metricsService.capture('Copy Code', { length: applyStr.length }) // capture the length only - }, [metricsService, clipboardService, text]) + }, [metricsService, clipboardService, applyStr]) const onApply = useCallback(() => { + inlineDiffService.startApplying({ from: 'Chat', - applyStr: text, + applyStr, + applyBoxId, }) - metricsService.capture('Apply Code', { length: text.length }) // capture the length only - }, [metricsService, inlineDiffService, text]) + metricsService.capture('Apply Code', { length: applyStr.length }) // capture the length only + }, [metricsService, inlineDiffService, applyStr]) - const isSingleLine = !text.includes('\n') + const isSingleLine = !applyStr.includes('\n') return <>