diff --git a/extensions/void/package-lock.json b/extensions/void/package-lock.json index 2c69af18..fea021c3 100644 --- a/extensions/void/package-lock.json +++ b/extensions/void/package-lock.json @@ -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": { diff --git a/extensions/void/src/common/systemPrompts.ts b/extensions/void/src/common/systemPrompts.ts index 7e443053..edbfa03b 100644 --- a/extensions/void/src/common/systemPrompts.ts +++ b/extensions/void/src/common/systemPrompts.ts @@ -397,6 +397,13 @@ COMPLETION export default Sidebar;\`\`\` ` +// used for ctrl+l +const partialGenerationInstructions = `` + + +// used for ctrl+k, autocomplete +const fimInstructions = `` + export { diff --git a/extensions/void/src/extension/AutcompleteProvider.ts b/extensions/void/src/extension/AutcompleteProvider.ts new file mode 100644 index 00000000..e0f0cc9c --- /dev/null +++ b/extensions/void/src/extension/AutcompleteProvider.ts @@ -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 | 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 { + + 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((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((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 + + } + + + + +} diff --git a/extensions/void/src/extension/DiffProvider.ts b/extensions/void/src/extension/DiffProvider.ts index 9836dfde..1b7d8ce8 100644 --- a/extensions/void/src/extension/DiffProvider.ts +++ b/extensions/void/src/extension/DiffProvider.ts @@ -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) } diff --git a/extensions/void/src/extension/autocomplete.ts b/extensions/void/src/extension/autocomplete.ts new file mode 100644 index 00000000..abe18e34 --- /dev/null +++ b/extensions/void/src/extension/autocomplete.ts @@ -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, + }) + + + + + + + + }) + +} diff --git a/extensions/void/src/extension/ctrlK.ts b/extensions/void/src/extension/ctrlK.ts index 63aaf200..2d7e35cd 100644 --- a/extensions/void/src/extension/ctrlK.ts +++ b/extensions/void/src/extension/ctrlK.ts @@ -5,31 +5,6 @@ import { searchDiffChunkInstructions, writeFileWithDiffInstructions } from '../c import { throttle } from 'lodash'; import { readFileContentOfUri } from './extensionLib/readFileContentOfUri'; -type Res = ((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. \`\`\`
${prefix}
${suffix} `; - - // TODO initialize stream - - // update stream sendLLMMessage({ messages: [{ role: 'user', content: promptContent, }], onText: async (tokenStr, completionStr) => { @@ -97,4 +67,4 @@ Complete the following: -export { applyCtrlK } \ No newline at end of file +export { applyCtrlK } diff --git a/extensions/void/src/extension/extension.ts b/extensions/void/src/extension/extension.ts index cc7cbed6..92a4caba 100644 --- a/extensions/void/src/extension/extension.ts +++ b/extensions/void/src/extension/extension.ts @@ -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; - 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; +// 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 })