Add working autocomplete with caching

This commit is contained in:
mp 2024-11-08 01:23:09 -08:00
parent fe413cb474
commit 268d03fad5
7 changed files with 389 additions and 55 deletions

View file

@ -6029,7 +6029,6 @@
"resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.52.0.tgz",
"integrity": "sha512-OeWhNpABLCeTqubfqLMXGsqf6OmPU6pHM85kF3dhy6kq5hnhuVS1p3VrEW/XhWHc71P2tHyS5JFySD8mgs1crw==",
"dev": true,
"license": "MIT",
"peer": true
},
"node_modules/ms": {

View file

@ -397,6 +397,13 @@ COMPLETION
export default Sidebar;\`\`\`
`
// used for ctrl+l
const partialGenerationInstructions = ``
// used for ctrl+k, autocomplete
const fimInstructions = ``
export {

View file

@ -0,0 +1,218 @@
import * as vscode from 'vscode';
import { AbortRef, LLMMessage, sendLLMMessage } from '../common/sendLLMMessage';
import { getVoidConfigFromPartial } from '../webviews/common/contextForConfig';
type AutocompletionStatus = 'pending' | 'finished' | 'error';
type Autocompletion = {
prefix: string,
suffix: string,
startTime: number,
endTime: number | undefined,
abortRef: AbortRef,
status: AutocompletionStatus,
promise: Promise<string> | undefined,
result: string,
}
const TIMEOUT_TIME = 10000
const toInlineCompletion = ({ prefix, suffix, autocompletion }: { prefix: string, suffix: string, autocompletion: Autocompletion }): vscode.InlineCompletionItem => {
const originalPrefix = autocompletion.prefix
const generatedMiddle = autocompletion.result
const fullPrefix = originalPrefix + generatedMiddle
// check if the currently generated text matches with the prefix
let remainingText = ''
if (fullPrefix.startsWith(prefix)) {
// example:
// originalPrefix = abcd
// generatedMiddle = efgh
// originalSuffix = ijkl
// the user has typed "ef" so prefix = abcdef
// we want to return the rest of the generatedMiddle, which is "gh"
const index = (prefix.length - originalPrefix.length) - 1
remainingText = generatedMiddle.substring(index + 1)
}
console.log('----')
console.log('fullPrefix', fullPrefix)
console.log('----')
console.log('----')
console.log('prefix', prefix)
console.log('----')
console.log('completion: ', remainingText)
return new vscode.InlineCompletionItem(remainingText)
}
export class AutocompleteProvider implements vscode.InlineCompletionItemProvider {
private _extensionContext: vscode.ExtensionContext;
private _autocompletionsOfDocument: { [docUriStr: string]: Autocompletion[] } = {}
// used internally by vscode
// fires after every keystroke
async provideInlineCompletionItems(
document: vscode.TextDocument,
position: vscode.Position,
context: vscode.InlineCompletionContext,
token: vscode.CancellationToken,
): Promise<vscode.InlineCompletionItem[]> {
const docUriStr = document.uri.toString()
const fullText = document.getText();
const cursorOffset = document.offsetAt(position);
const prefix = fullText.substring(0, cursorOffset);
const suffix = fullText.substring(cursorOffset);
if (!this._autocompletionsOfDocument[docUriStr]) {
this._autocompletionsOfDocument[docUriStr] = []
}
const voidConfig = getVoidConfigFromPartial(this._extensionContext.globalState.get('partialVoidConfig') ?? {})
// get autocompletion from cache
let cachedAutocompletion: Autocompletion | undefined = undefined
loop: for (const autocompletion of this._autocompletionsOfDocument[docUriStr]!) {
const originalPrefix = autocompletion.prefix
const generatedMiddle = autocompletion.result
// if the user's change matches up with the generated text
if ((originalPrefix + generatedMiddle).startsWith(prefix)) {
cachedAutocompletion = autocompletion
break loop;
}
}
// if there is an autocompletion for this line, return it
if (cachedAutocompletion) {
if (cachedAutocompletion.status === 'finished') {
const inlineCompletion = toInlineCompletion({ autocompletion: cachedAutocompletion, prefix, suffix, })
return [inlineCompletion]
} else if (cachedAutocompletion.status === 'pending') {
try {
// await the result; if it hasnt resolved in 10 seconds assume the request is dead
await Promise.race([
cachedAutocompletion.promise,
new Promise<string>((resolve, reject) => setTimeout(() => reject('Request timed out'), TIMEOUT_TIME)),
])
const inlineCompletion = toInlineCompletion({ autocompletion: cachedAutocompletion, prefix, suffix, })
return [inlineCompletion]
} catch (e) {
console.error('Error creating autocompletion (1): ' + e)
return []
}
} else {
return []
}
}
// if there is no autocomplete for this line, create it and add it to cache
let messages: LLMMessage[] = []
switch (voidConfig.default.whichApi) {
case 'ollama':
messages = [
{ role: 'user', content: `[SUFFIX]${suffix}[PREFIX]${prefix} ` }
]
break;
case 'anthropic':
case 'openAI':
messages = [
{ role: 'system', content: '' },
{ role: 'user', content: `[SUFFIX]${suffix}[PREFIX]${prefix}` },
]
break;
default:
throw new Error(`We do not recommend using autocomplete with your selected provider (${voidConfig.default.whichApi}).`);
}
const newAutocompletion: Autocompletion = {
prefix: prefix,
suffix: suffix,
startTime: Date.now(),
endTime: undefined,
abortRef: { current: () => { } },
status: 'pending',
promise: undefined,
result: '',
}
// set the parameters of `newAutocompletion` appropriately
newAutocompletion.promise = new Promise((resolve, reject) => {
sendLLMMessage({
messages: messages,
onText: async (tokenStr, completionStr) => {
// TODO filter out bad responses here
newAutocompletion.result = completionStr
},
onFinalMessage: (finalMessage) => {
console.log('finalMessage:', finalMessage);
// newAutocompletion.prefix = prefix
// newAutocompletion.suffix = suffix
// newAutocompletion.startTime = Date.now()
newAutocompletion.endTime = Date.now()
// newAutocompletion.abortRef = { current: () => { } }
newAutocompletion.status = 'finished'
// newAutocompletion.promise = undefined
newAutocompletion.result = finalMessage
resolve(finalMessage)
},
onError: (e) => {
newAutocompletion.endTime = Date.now()
newAutocompletion.status = 'error'
newAutocompletion.result = ''
reject(e)
},
voidConfig,
abortRef: newAutocompletion.abortRef,
})
})
this._autocompletionsOfDocument[docUriStr]!.push(newAutocompletion)
try {
const result = await Promise.race([
newAutocompletion.promise,
new Promise<string>((resolve, reject) => setTimeout(() => reject('Request timed out'), TIMEOUT_TIME)),
])
const inlineCompletion = toInlineCompletion({ autocompletion: newAutocompletion, prefix, suffix, })
return [inlineCompletion]
} catch (e) {
console.error('Error creating autocompletion (2): ' + e)
return []
}
}
constructor(context: vscode.ExtensionContext) {
this._extensionContext = context
}
}

View file

@ -48,8 +48,6 @@ export class DiffProvider implements vscode.CodeLensProvider {
constructor(context: vscode.ExtensionContext) {
this._extensionUri = context.extensionUri
console.log('Creating DisplayChangesProvider')
// this acts as a useEffect every time text changes
vscode.workspace.onDidChangeTextDocument((e) => {
@ -167,7 +165,9 @@ export class DiffProvider implements vscode.CodeLensProvider {
}
const originalFile = this._originalFileOfDocument[docUriStr]
if (!originalFile) {
console.log('Error: No original file!')
if (this._diffAreasOfDocument[docUriStr]?.length > 0) {
console.log('Error: More than one diff area exists, but no original file was set.')
}
return;
}
@ -189,7 +189,6 @@ export class DiffProvider implements vscode.CodeLensProvider {
// add the diffs to `this._diffsOfDocument[docUriStr]`
this.createDiffs(editor.document.uri, diffs, diffArea)
// // print diffs
// console.log('!ORIGINAL FILE:', JSON.stringify(originalFile))
// console.log('!NEW FILE :', JSON.stringify(editor.document.getText().replace(/\r\n/g, '\n')))
@ -451,6 +450,7 @@ export class DiffProvider implements vscode.CodeLensProvider {
const diffareaRange = new vscode.Range(diffArea.startLine, 0, diffArea.endLine, Number.MAX_SAFE_INTEGER)
workspaceEdit.replace(editor.document.uri, diffareaRange, newCode)
await vscode.workspace.applyEdit(workspaceEdit)
}, THROTTLE_TIME)
}

View file

@ -0,0 +1,130 @@
import * as vscode from 'vscode';
import { AbortRef, OnFinalMessage, OnText, sendLLMMessage } from "../common/sendLLMMessage"
import { VoidConfig } from '../webviews/common/contextForConfig';
type AutocompletionStatus = 'pending' | 'complete' | 'error';
type Autocompletion = {
prefix: string,
suffix: string,
startTime: number,
endTime: number,
abortRef: AbortRef,
status: AutocompletionStatus,
result: string,
}
const recentEdits = []
const autocompletionsOfDocument: { [docUriStr: string]: Autocompletion[] } = {}
const showRecentAutocompletion = () => {
console.log('showRecentAutocompletion')
const editor = vscode.window.activeTextEditor
if (!editor) return;
const docUriStr = editor.document.uri.toString();
const autocompletions = autocompletionsOfDocument[docUriStr]
if (!autocompletions || autocompletions.length === 0) return;
const completion = autocompletions[autocompletions.length - 1]
if (completion.status === 'pending') return;
if (completion.status === 'error') return;
const decorationType = vscode.window.createTextEditorDecorationType({
after: { contentText: completion.result, color: '#888', }
});
const position = editor.document.positionAt(completion.prefix.length);
const decorationOptions = [{ range: new vscode.Range(position, position) }];
editor.setDecorations(decorationType, decorationOptions);
}
export const setupAutocomplete = ({ voidConfig, abortRef }: { voidConfig: VoidConfig, abortRef: AbortRef }) => {
vscode.workspace.onDidChangeTextDocument(e => {
let shouldAutocomplete = true;
// 1. determine if we should do an autocomplete
// -check that we're not predicting too many changes at a time
// -look at cache and see if current location has already been predicted
// -check if the user's selection has overlap with the current prediction they are selecting
const editor = vscode.window.activeTextEditor
if (!editor) return;
if (e.document !== editor.document) return;
if (e.contentChanges.length === 0) return;
const docUriStr = editor.document.uri.toString();
// get the prefix + suffix
const change = e.contentChanges[e.contentChanges.length - 1];
const fullText = editor.document.getText();
const startOffset = editor.document.offsetAt(change.range.start);
const cursorOffset = startOffset + (change.text.length > 0 ? change.text.length : 0);
const prefix = fullText.substring(0, cursorOffset);
const suffix = fullText.substring(cursorOffset);
// TODO do checks as mentioned above
if (!shouldAutocomplete) return;
// 2. if we should do an autocomplete, get the relevant quantities
// -LSP types of variables around the cursor
// -LSP imports of variables around the cursor
// -code context of recent edits
// 3. create an autocompletion
if (!autocompletionsOfDocument[docUriStr]) {
autocompletionsOfDocument[docUriStr] = []
}
let promptContent = ``;
switch (voidConfig.default.whichApi) {
case 'ollama':
promptContent = `[SUFFIX]${suffix}[PREFIX]${prefix}`;
break;
case 'anthropic':
case 'openAI':
promptContent = `[SUFFIX]${suffix}[PREFIX]${prefix}`;
break;
default:
throw new Error(`We do not recommend using autocomplete with your selected provider (${voidConfig.default.whichApi}).`);
}
const startTime = Date.now();
sendLLMMessage({
messages: [{ role: 'user', content: promptContent, }],
onText: async (tokenStr, completionStr) => {
// TODO filter out bad responses here
},
onFinalMessage: (finalMessage) => {
console.log('finalMessage:', finalMessage);
const autocompletion: Autocompletion = {
prefix,
suffix,
abortRef,
startTime,
endTime: Date.now(),
status: 'complete',
result: finalMessage,
}
autocompletionsOfDocument[docUriStr].push(autocompletion)
showRecentAutocompletion()
},
onError: (e) => {
console.error('Error generating autocompletion:', e);
},
voidConfig,
abortRef,
})
})
}

View file

@ -5,31 +5,6 @@ import { searchDiffChunkInstructions, writeFileWithDiffInstructions } from '../c
import { throttle } from 'lodash';
import { readFileContentOfUri } from './extensionLib/readFileContentOfUri';
type Res<T> = ((value: T) => void)
const THRTOTLE_TIME = 100 // minimum time between edits
const LINES_PER_CHUNK = 20 // number of lines to search at a time
const applyCtrlLChangesToFile = throttle(
({ fileUri, newCurrentLine, oldCurrentLine, fullCompletedStr, oldFileStr, debug }: { fileUri: vscode.Uri, newCurrentLine: number, oldCurrentLine: number, fullCompletedStr: string, oldFileStr: string, debug?: string }) => {
// write the change to the file
const WRITE_TO_FILE = (
fullCompletedStr.split('\n').slice(0, newCurrentLine + 1).join('\n') // newFile[:newCurrentLine+1]
+ oldFileStr.split('\n').slice(oldCurrentLine + 1).join('\n') // oldFile[oldCurrentLine+1:]
)
const workspaceEdit = new vscode.WorkspaceEdit()
workspaceEdit.replace(fileUri, new vscode.Range(0, 0, Number.MAX_SAFE_INTEGER, 0), WRITE_TO_FILE)
vscode.workspace.applyEdit(workspaceEdit)
// highlight the `newCurrentLine` in white
// highlight the remaining part of the file in gray
},
THRTOTLE_TIME, { trailing: true }
)
const applyCtrlK = async ({ fileUri, startLine, endLine, instructions, voidConfig, abortRef }: { fileUri: vscode.Uri, startLine: number, endLine: number, instructions: string, voidConfig: VoidConfig, abortRef: AbortRef }) => {
const fileStr = await readFileContentOfUri(fileUri)
@ -47,24 +22,19 @@ const applyCtrlK = async ({ fileUri, startLine, endLine, instructions, voidConfi
The user wants to apply the following instructions to the selection:
${instructions}
Please rewrite the selection following the user's instructions.
Instructions to follow:
Instructions:
1. Follow the user's instructions
2. You may ONLY CHANGE the selection, and nothing else in the file
3. Make sure all brackets in the new selection are balanced the same was as in the original selection
3. Be careful not to duplicate or remove variables, comments, or other syntax by mistake
4. Be careful not to duplicate or remove variables, comments, or other syntax by mistake
Complete the following:
Please rewrite the complete the following code, following the user's instructions.
\`\`\`
<PRE>${prefix}</PRE>
<SUF>${suffix}</SUF>
<MID>`;
// TODO initialize stream
// update stream
sendLLMMessage({
messages: [{ role: 'user', content: promptContent, }],
onText: async (tokenStr, completionStr) => {
@ -97,4 +67,4 @@ Complete the following:
export { applyCtrlK }
export { applyCtrlK }

View file

@ -9,21 +9,23 @@ import { DiffProvider } from './DiffProvider';
import { readFileContentOfUri } from './extensionLib/readFileContentOfUri';
import { SidebarWebviewProvider } from './providers/SidebarWebviewProvider';
import { CtrlKWebviewProvider } from './providers/CtrlKWebviewProvider';
import { setupAutocomplete } from './autocomplete';
import { AutocompleteProvider } from './AutcompleteProvider';
// this comes from vscode.proposed.editorInsets.d.ts
declare module 'vscode' {
export interface WebviewEditorInset {
readonly editor: vscode.TextEditor;
readonly line: number;
readonly height: number;
readonly webview: vscode.Webview;
readonly onDidDispose: Event<void>;
dispose(): void;
}
export namespace window {
export function createWebviewTextEditorInset(editor: vscode.TextEditor, line: number, height: number, options?: vscode.WebviewOptions): WebviewEditorInset;
}
}
// // this comes from vscode.proposed.editorInsets.d.ts
// declare module 'vscode' {
// export interface WebviewEditorInset {
// readonly editor: vscode.TextEditor;
// readonly line: number;
// readonly height: number;
// readonly webview: vscode.Webview;
// readonly onDidDispose: Event<void>;
// dispose(): void;
// }
// export namespace window {
// export function createWebviewTextEditorInset(editor: vscode.TextEditor, line: number, height: number, options?: vscode.WebviewOptions): WebviewEditorInset;
// }
// }
const roundRangeToLines = (selection: vscode.Selection) => {
let endLine = selection.end.character === 0 ? selection.end.line - 1 : selection.end.line // e.g. if the user triple clicks, it selects column=0, line=line -> column=0, line=line+1
@ -112,7 +114,7 @@ export function activate(context: vscode.ExtensionContext) {
// Receive messages in the extension from the sidebar webview (messages are sent using `postMessage`)
webview.onDidReceiveMessage(async (m: MessageFromSidebar) => {
const abortApplyRef: AbortRef = { current: null }
const abortRef: AbortRef = { current: null }
if (m.type === 'requestFiles') {
@ -146,7 +148,7 @@ export function activate(context: vscode.ExtensionContext) {
const fileStr = await readFileContentOfUri(docUri)
const voidConfig = getVoidConfigFromPartial(context.globalState.get('partialVoidConfig') ?? {})
await applyDiffLazily({ docUri, oldFileStr: fileStr, diffRepr: m.diffRepr, voidConfig, diffProvider, diffArea, abortRef: abortApplyRef })
await applyDiffLazily({ docUri, oldFileStr: fileStr, diffRepr: m.diffRepr, voidConfig, diffProvider, diffArea, abortRef: abortRef })
}
else if (m.type === 'getPartialVoidConfig') {
const partialVoidConfig = context.globalState.get('partialVoidConfig') ?? {}
@ -180,6 +182,14 @@ export function activate(context: vscode.ExtensionContext) {
}
)
// 6. Autocomplete
const autocompleteProvider = new AutocompleteProvider(context);
context.subscriptions.push(vscode.languages.registerInlineCompletionItemProvider('*', autocompleteProvider));
const voidConfig = getVoidConfigFromPartial(context.globalState.get('partialVoidConfig') ?? {})
const abortRef: AbortRef = { current: null }
// setupAutocomplete({ voidConfig, abortRef })