From 48426403f315236cee90d8f16bfb61a0bfbf1f64 Mon Sep 17 00:00:00 2001 From: mp Date: Tue, 26 Nov 2024 15:15:05 -0800 Subject: [PATCH] Re-added autocomplete --- src/main.js | 2 +- .../processExplorer-dev.esm.html | 1 + .../processExplorer/processExplorer-dev.html | 1 + .../processExplorer/processExplorer.esm.html | 1 + .../processExplorer/processExplorer.html | 1 + .../workbench/workbench-dev.esm.html | 1 + .../workbench/workbench-dev.html | 1 + .../workbench/workbench.esm.html | 1 + .../electron-sandbox/workbench/workbench.html | 1 + .../issueReporter-dev.esm.html | 1 + .../react/src/sidebar-tsx/SidebarChat.tsx | 1 + .../browser/react/src/util/sendLLMMessage.tsx | 329 ++--- .../void/browser/registerAutocomplete.ts | 1159 ++++++++++------- .../void/browser/registerInlineDiffs.ts | 1 + .../contrib/void/browser/registerThreads.ts | 3 + .../webWorkerExtensionHostIframe.esm.html | 2 +- 16 files changed, 873 insertions(+), 633 deletions(-) diff --git a/src/main.js b/src/main.js index 10bd5033..03391f64 100644 --- a/src/main.js +++ b/src/main.js @@ -123,7 +123,7 @@ protocol.registerSchemesAsPrivileged([ }, { scheme: 'vscode-file', - privileges: { secure: true, standard: true, supportFetchAPI: true, corsEnabled: true, codeCache: true } + privileges: { secure: true, standard: true, supportFetchAPI: true, corsEnabled: true, codeCache: true, } } ]); diff --git a/src/vs/code/electron-sandbox/processExplorer/processExplorer-dev.esm.html b/src/vs/code/electron-sandbox/processExplorer/processExplorer-dev.esm.html index 19d194fc..45e9a557 100644 --- a/src/vs/code/electron-sandbox/processExplorer/processExplorer-dev.esm.html +++ b/src/vs/code/electron-sandbox/processExplorer/processExplorer-dev.esm.html @@ -25,6 +25,7 @@ connect-src 'self' https: + * ; font-src 'self' diff --git a/src/vs/code/electron-sandbox/processExplorer/processExplorer-dev.html b/src/vs/code/electron-sandbox/processExplorer/processExplorer-dev.html index 5bdf62c8..2163c57a 100644 --- a/src/vs/code/electron-sandbox/processExplorer/processExplorer-dev.html +++ b/src/vs/code/electron-sandbox/processExplorer/processExplorer-dev.html @@ -23,6 +23,7 @@ connect-src 'self' https: + * ; font-src 'self' diff --git a/src/vs/code/electron-sandbox/processExplorer/processExplorer.esm.html b/src/vs/code/electron-sandbox/processExplorer/processExplorer.esm.html index d2747202..fbae8f91 100644 --- a/src/vs/code/electron-sandbox/processExplorer/processExplorer.esm.html +++ b/src/vs/code/electron-sandbox/processExplorer/processExplorer.esm.html @@ -25,6 +25,7 @@ connect-src 'self' https: + * ; font-src 'self' diff --git a/src/vs/code/electron-sandbox/processExplorer/processExplorer.html b/src/vs/code/electron-sandbox/processExplorer/processExplorer.html index 845d024e..8d18858b 100644 --- a/src/vs/code/electron-sandbox/processExplorer/processExplorer.html +++ b/src/vs/code/electron-sandbox/processExplorer/processExplorer.html @@ -23,6 +23,7 @@ connect-src 'self' https: + * ; font-src 'self' diff --git a/src/vs/code/electron-sandbox/workbench/workbench-dev.esm.html b/src/vs/code/electron-sandbox/workbench/workbench-dev.esm.html index ea5cbee8..0292dee0 100644 --- a/src/vs/code/electron-sandbox/workbench/workbench-dev.esm.html +++ b/src/vs/code/electron-sandbox/workbench/workbench-dev.esm.html @@ -38,6 +38,7 @@ 'self' https: ws: + * ; font-src 'self' diff --git a/src/vs/code/electron-sandbox/workbench/workbench-dev.html b/src/vs/code/electron-sandbox/workbench/workbench-dev.html index b1be2b75..1123a128 100644 --- a/src/vs/code/electron-sandbox/workbench/workbench-dev.html +++ b/src/vs/code/electron-sandbox/workbench/workbench-dev.html @@ -37,6 +37,7 @@ 'self' https: ws: + * ; font-src 'self' diff --git a/src/vs/code/electron-sandbox/workbench/workbench.esm.html b/src/vs/code/electron-sandbox/workbench/workbench.esm.html index 1e89448e..a85792c1 100644 --- a/src/vs/code/electron-sandbox/workbench/workbench.esm.html +++ b/src/vs/code/electron-sandbox/workbench/workbench.esm.html @@ -37,6 +37,7 @@ 'self' https: ws: + * ; font-src 'self' diff --git a/src/vs/code/electron-sandbox/workbench/workbench.html b/src/vs/code/electron-sandbox/workbench/workbench.html index eb525bd5..2d9e89f9 100644 --- a/src/vs/code/electron-sandbox/workbench/workbench.html +++ b/src/vs/code/electron-sandbox/workbench/workbench.html @@ -37,6 +37,7 @@ 'self' https: ws: + * ; font-src 'self' diff --git a/src/vs/workbench/contrib/issue/electron-sandbox/issueReporter-dev.esm.html b/src/vs/workbench/contrib/issue/electron-sandbox/issueReporter-dev.esm.html index f14661a2..d879a450 100644 --- a/src/vs/workbench/contrib/issue/electron-sandbox/issueReporter-dev.esm.html +++ b/src/vs/workbench/contrib/issue/electron-sandbox/issueReporter-dev.esm.html @@ -25,6 +25,7 @@ connect-src 'self' https: + * ; font-src 'self' diff --git a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx b/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx index 3b0dffe6..34375c89 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx @@ -237,6 +237,7 @@ export const SidebarChat = () => { }, voidConfig, abortRef: abortFnRef, + options: {}, }) diff --git a/src/vs/workbench/contrib/void/browser/react/src/util/sendLLMMessage.tsx b/src/vs/workbench/contrib/void/browser/react/src/util/sendLLMMessage.tsx index 039c16c9..bde4bb50 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/util/sendLLMMessage.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/util/sendLLMMessage.tsx @@ -21,8 +21,15 @@ export type LLMMessage = { content: string; } +export type LLMMessageOptions = { + stopTokens?: string[], + prefix?: string, + suffix?: string, +} + type SendLLMMessageFnTypeInternal = (params: { messages: LLMMessage[]; + options: LLMMessageOptions; onText: OnText; onFinalMessage: OnFinalMessage; onError: (error: string) => void; @@ -33,6 +40,7 @@ type SendLLMMessageFnTypeInternal = (params: { type SendLLMMessageFnTypeExternal = (params: { messages: LLMMessage[]; + options: LLMMessageOptions; onText: OnText; onFinalMessage: (fullText: string) => void; onError: (error: string) => void; @@ -213,34 +221,65 @@ const sendOpenAIMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinal }; // Ollama -export const sendOllamaMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinalMessage, onError, voidConfig, _setAborter }) => { +export const sendOllamaMsg: SendLLMMessageFnTypeInternal = ({ messages, options, onText, onFinalMessage, onError, voidConfig, _setAborter }) => { let fullText = '' const ollama = new Ollama({ host: voidConfig.ollama.endpoint }) - ollama.chat({ - model: voidConfig.ollama.model, - messages: messages, - stream: true, - options: { num_predict: parseMaxTokensStr(voidConfig.default.maxTokens) } // this is max_tokens - }) - .then(async stream => { - _setAborter(() => stream.abort()) - // iterate through the stream - for await (const chunk of stream) { - const newText = chunk.message.content; - fullText += newText; - onText(newText, fullText); + if (options.prefix !== '' || options.suffix !== '') { + ollama.generate({ + model: voidConfig.ollama.model, + prompt: options.prefix ?? '', + suffix: options.suffix ?? '', + stream: true, + options: { + num_predict: parseMaxTokensStr(voidConfig.default.maxTokens), + stop: options.stopTokens, } - onFinalMessage(fullText); - - }) - // when error/fail - .catch(error => { - onError(error) }) + .then(async stream => { + _setAborter(() => stream.abort()) + for await (const chunk of stream) { + const newText = chunk.response; + fullText += newText; + onText(newText, fullText); + } + onFinalMessage(fullText); + }) + // when error/fail + .catch(error => { + onError(error) + }) + + } else { + + ollama.chat({ + model: voidConfig.ollama.model, + messages: messages, + stream: true, + options: { + num_predict: parseMaxTokensStr(voidConfig.default.maxTokens), // this is max_tokens + stop: options.stopTokens, + } + }) + .then(async stream => { + _setAborter(() => stream.abort()) + // iterate through the stream + for await (const chunk of stream) { + const newText = chunk.message.content; + fullText += newText; + onText(newText, fullText); + } + onFinalMessage(fullText); + + }) + // when error/fail + .catch(error => { + onError(error) + }) + } }; // Greptile @@ -309,15 +348,29 @@ const sendGreptileMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFin export const sendLLMMessage: SendLLMMessageFnTypeExternal = ({ messages, + options, onText: onText_, onFinalMessage: onFinalMessage_, onError: onError_, abortRef: abortRef_, voidConfig, - logging: { loggingName } + logging: { loggingName }, }) => { if (!voidConfig) return; + // set messages appropriately if fill in middle mode + if (options.prefix || options.suffix) { + const prefix = (options.prefix ?? '').split('\n').slice(-20).join('\n') + const suffix = (options.suffix ?? '').split('\n').slice(0, 20).join('\n') + options.prefix = prefix + options.suffix = suffix + if (!prefix.trim() && !suffix.trim()) return; + const system = getFimSystem({ voidConfig, prefix, suffix }) + const prompt = getFimPrompt({ voidConfig, prefix, suffix }) + if (system) messages.push({ role: 'system', content: system }) + if (prompt) messages.push({ role: 'user', content: prompt }) + } + // trim message content (Anthropic and other providers give an error if there is trailing whitespace) messages = messages.map(m => ({ ...m, content: m.content.trim() })) @@ -368,21 +421,21 @@ export const sendLLMMessage: SendLLMMessageFnTypeExternal = ({ try { switch (voidConfig.default.whichApi) { case 'anthropic': - sendAnthropicMsg({ messages, onText, onFinalMessage, onError, voidConfig, _setAborter, }); + sendAnthropicMsg({ messages, options, onText, onFinalMessage, onError, voidConfig, _setAborter, }); break; case 'openAI': case 'openRouter': case 'openAICompatible': - sendOpenAIMsg({ messages, onText, onFinalMessage, onError, voidConfig, _setAborter, }); + sendOpenAIMsg({ messages, options, onText, onFinalMessage, onError, voidConfig, _setAborter, }); break; case 'gemini': - sendGeminiMsg({ messages, onText, onFinalMessage, onError, voidConfig, _setAborter, }); + sendGeminiMsg({ messages, options, onText, onFinalMessage, onError, voidConfig, _setAborter, }); break; case 'ollama': - sendOllamaMsg({ messages, onText, onFinalMessage, onError, voidConfig, _setAborter, }); + sendOllamaMsg({ messages, options, onText, onFinalMessage, onError, voidConfig, _setAborter, }); break; case 'greptile': - sendGreptileMsg({ messages, onText, onFinalMessage, onError, voidConfig, _setAborter, }); + sendGreptileMsg({ messages, options, onText, onFinalMessage, onError, voidConfig, _setAborter, }); break; default: onError(`Error: whichApi was ${voidConfig.default.whichApi}, which is not recognized!`) @@ -405,16 +458,101 @@ export const sendLLMMessage: SendLLMMessageFnTypeExternal = ({ +type getFimPrompt = ({ voidConfig, prefix, suffix }: { voidConfig: VoidConfig, prefix: string, suffix: string, }) => string | undefined + +export const getFimSystem: getFimPrompt = ({ voidConfig, prefix, suffix }) => { + + switch (voidConfig.default.whichApi) { + case 'ollama': + return undefined + case 'anthropic': + case 'openAI': + case 'gemini': + case 'greptile': + case 'openRouter': + case 'openAICompatible': + case 'azure': + default: + return `\ +You are given the START and END to a piece of code. Please FILL IN THE MIDDLE between the START and END. + +Instruction summary: +1. Return the MIDDLE of the code between the START and END. +2. Do not give an explanation, description, or any other code besides the middle. +3. Do not return duplicate code from either START or END. +4. Make sure the MIDDLE piece of code has balanced brackets that match the START and END. +5. The MIDDLE begins on the same line as START. Please include a newline character if you want to begin on the next line. +6. Around 90% of the time, you should return just one or a few lines of code. You should keep your outputs short unless you are confident the user is trying to write boilderplate code. + +# EXAMPLE + +## START: +\`\`\` python +def add(a,b): + return a + b +def subtract(a,b): + return a - b +\`\`\` +## END: +\`\`\` python +def divide(a,b): + return a / b +\`\`\` +## EXPECTED OUTPUT: +\`\`\` python + +def multiply(a,b): + return a * b +\`\`\` + +# EXAMPLE +## START: +\`\`\` javascript +const x = 1 + +const y +\`\`\` +## END: +\`\`\` javascript + +const z = 3 +\`\`\` +## EXPECTED OUTPUT: +\`\`\` javascript += 2 +\`\`\` +` + } +} +export const getFimPrompt: getFimPrompt = ({ voidConfig, prefix, suffix }) => { - - - - - + switch (voidConfig.default.whichApi) { + case 'ollama': + return undefined + case 'anthropic': + case 'openAI': + case 'gemini': + case 'greptile': + case 'openRouter': + case 'openAICompatible': + case 'azure': + default: + return `\ +## START: +\`\`\` +${prefix} +\`\`\` +## END: +\`\`\` +${suffix} +\`\`\` +` + } +} // // 6. Autocomplete @@ -439,135 +577,6 @@ export const sendLLMMessage: SendLLMMessageFnTypeExternal = ({ -// import { configFields, VoidConfig } from "../webviews/common/contextForConfig" -// import { FimInfo } from "./sendLLMMessage" - - -// type GetFIMPrompt = ({ voidConfig, fimInfo }: { voidConfig: VoidConfig, fimInfo: FimInfo, }) => string - -// export const getFIMSystem: GetFIMPrompt = ({ voidConfig, fimInfo }) => { - -// switch (voidConfig.default.whichApi) { -// case 'ollama': -// return '' -// case 'anthropic': -// case 'openAI': -// case 'gemini': -// case 'greptile': -// case 'openRouter': -// case 'openAICompatible': -// case 'azure': -// default: -// return `You are given the START and END to a piece of code. Please FILL IN THE MIDDLE between the START and END. - -// Instruction summary: -// 1. Return the MIDDLE of the code between the START and END. -// 2. Do not give an explanation, description, or any other code besides the middle. -// 3. Do not return duplicate code from either START or END. -// 4. Make sure the MIDDLE piece of code has balanced brackets that match the START and END. -// 5. The MIDDLE begins on the same line as START. Please include a newline character if you want to begin on the next line. -// 6. Around 90% of the time, you should return just one or a few lines of code. You should keep your outputs short unless you are confident the user is trying to write boilderplate code. - -// # EXAMPLE - -// ## START: -// \`\`\` python -// def add(a,b): -// return a + b -// def subtract(a,b): -// return a - b -// \`\`\` -// ## END: -// \`\`\` python -// def divide(a,b): -// return a / b -// \`\`\` -// ## EXPECTED OUTPUT: -// \`\`\` python - -// def multiply(a,b): -// return a * b -// \`\`\` - -// # EXAMPLE -// ## START: -// \`\`\` javascript -// const x = 1 - -// const y -// \`\`\` -// ## END: -// \`\`\` javascript - -// const z = 3 -// \`\`\` -// ## EXPECTED OUTPUT: -// \`\`\` javascript -// = 2 -// \`\`\` -// ` -// } - - -// } - - -// export const getFIMPrompt: GetFIMPrompt = ({ voidConfig, fimInfo }) => { - -// const { prefix: fullPrefix, suffix: fullSuffix } = fimInfo -// const prefix = fullPrefix.split('\n').slice(-20).join('\n') -// const suffix = fullSuffix.split('\n').slice(0, 20).join('\n') - - -// console.log('prefix', JSON.stringify(prefix)) -// console.log('suffix', JSON.stringify(suffix)) - -// if (!prefix.trim() && !suffix.trim()) return '' - -// // TODO may want to trim the prefix and suffix -// switch (voidConfig.default.whichApi) { -// case 'ollama': -// if (voidConfig.ollama.model === 'codestral') { -// return `[SUFFIX]${suffix}[PREFIX] ${prefix}` -// } else if (voidConfig.ollama.model.includes('qwen')) { -// return `<|fim_prefix|>${prefix}<|fim_suffix|>${suffix}<|fim_middle|>` -// } -// return '' -// case 'anthropic': -// case 'openAI': -// case 'gemini': -// case 'greptile': -// case 'openRouter': -// case 'openAICompatible': -// case 'azure': -// default: -// return `## START: -// \`\`\` -// ${prefix} -// \`\`\` -// ## END: -// \`\`\` -// ${suffix} -// \`\`\` -// ` -// } -// } - - - - - - - - - - - - - - - - // Mathew - sendLLMMessage diff --git a/src/vs/workbench/contrib/void/browser/registerAutocomplete.ts b/src/vs/workbench/contrib/void/browser/registerAutocomplete.ts index 112bafe2..a2f7e6b5 100644 --- a/src/vs/workbench/contrib/void/browser/registerAutocomplete.ts +++ b/src/vs/workbench/contrib/void/browser/registerAutocomplete.ts @@ -1,222 +1,343 @@ -// /*--------------------------------------------------------------------------------------------- -// * Copyright (c) Glass Devtools, Inc. All rights reserved. -// * Void Editor additions licensed under the AGPLv3 License. -// *--------------------------------------------------------------------------------------------*/ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Glass Devtools, Inc. All rights reserved. + * Void Editor additions licensed under the AGPLv3 License. + *--------------------------------------------------------------------------------------------*/ + +import { Disposable } from '../../../../base/common/lifecycle.js'; +import { ILanguageFeaturesService } from '../../../../editor/common/services/languageFeatures.js'; +import { registerSingleton, InstantiationType } from '../../../../platform/instantiation/common/extensions.js'; +import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; +import { IVoidConfigStateService } from './registerConfig.js'; +import { sendLLMMessage } from './react/out/util/sendLLMMessage.js'; +import { ITextModel } from '../../../../editor/common/model.js'; +import { Position } from '../../../../editor/common/core/position.js'; +import { InlineCompletion, InlineCompletionContext } from '../../../../editor/common/languages.js'; +import { CancellationToken } from '../../../../base/common/cancellation.js'; +import { Range } from '../../../../editor/common/core/range.js'; + +// The extension this was called from is here - https://github.com/voideditor/void/blob/autocomplete/extensions/void/src/extension/extension.ts + + +/* +A summary of autotab: + +Postprocessing +-one common problem for all models is outputting unbalanced parentheses +we solve this by trimming all extra closing parentheses from the generated string +in future, should make sure parentheses are always balanced + +-another problem is completing the middle of a string, eg. "const [x, CURSOR] = useState()" +we complete up to first matchup character +but should instead complete the whole line / block (difficult because of parenthesis accuracy) + +-too much info is bad. usually we want to show the user 1 line, and have a preloaded response afterwards +this should happen automatically with caching system +should break preloaded responses into \n\n chunks + +Preprocessing +- we don't generate if cursor is at end / beginning of a line (no spaces) +- we generate 1 line if there is text to the right of cursor +- we generate 1 line if variable declaration +- (in many cases want to show 1 line but generate multiple) + +State +- cache based on prefix (and do some trimming first) +- when press tab on one line, should have an immediate followup response +to do this, show autocompletes before they're fully finished +- [todo] remove each autotab when accepted +- [todo] treat windows \r\n separately from \n +!- [todo] provide type information + +Details +-generated results are trimmed up to 1 leading/trailing space +-prefixes are cached up to 1 trailing newline +- +*/ + +class LRUCache { + public items: Map; + private keyOrder: K[]; + private maxSize: number; + private disposeCallback?: (value: V, key?: K) => void; + + constructor(maxSize: number, disposeCallback?: (value: V, key?: K) => void) { + if (maxSize <= 0) throw new Error('Cache size must be greater than 0'); + + this.items = new Map(); + this.keyOrder = []; + this.maxSize = maxSize; + this.disposeCallback = disposeCallback; + } + + set(key: K, value: V): void { + // If key exists, remove it from the order list + if (this.items.has(key)) { + this.keyOrder = this.keyOrder.filter(k => k !== key); + } + // If cache is full, remove least recently used item + else if (this.items.size >= this.maxSize) { + const key = this.keyOrder[0]; + const value = this.items.get(key); + + // Call dispose callback if it exists + if (this.disposeCallback && value !== undefined) { + this.disposeCallback(value, key); + } + + this.items.delete(key); + this.keyOrder.shift(); + } + + // Add new item + this.items.set(key, value); + this.keyOrder.push(key); + } + + delete(key: K): boolean { + const value = this.items.get(key); + + if (value !== undefined) { + // Call dispose callback if it exists + if (this.disposeCallback) { + this.disposeCallback(value, key); + } + + this.items.delete(key); + this.keyOrder = this.keyOrder.filter(k => k !== key); + return true; + } + + return false; + } + + clear(): void { + // Call dispose callback for all items if it exists + if (this.disposeCallback) { + for (const [key, value] of this.items.entries()) { + this.disposeCallback(value, key); + } + } + + this.items.clear(); + this.keyOrder = []; + } + + get size(): number { + return this.items.size; + } + + has(key: K): boolean { + return this.items.has(key); + } +} + +type AutocompletionStatus = 'pending' | 'finished' | 'error'; +type Autocompletion = { + id: number, + prefix: string, + suffix: string, + startTime: number, + endTime: number | undefined, + abortRef: { current: () => void }, + status: AutocompletionStatus, + llmPromise: Promise | undefined, + result: string, +} + +const DEBOUNCE_TIME = 500 +const TIMEOUT_TIME = 60000 +const MAX_CACHE_SIZE = 20 +const MAX_PENDING_REQUESTS = 2 + +// postprocesses the result +const postprocessResult = (result: string) => { + + // trim all whitespace except for a single leading/trailing space + // return result.trim() + + const hasLeadingSpace = result.startsWith(' '); + const hasTrailingSpace = result.endsWith(' '); + return (hasLeadingSpace ? ' ' : '') + + result.trim() + + (hasTrailingSpace ? ' ' : ''); + +} + +const extractCodeFromResult = (result: string) => { + // Match either: + // 1. ```language\n``` + // 2. `````` + const match = result.match(/```(?:\w+\n)?([\s\S]*?)```|```([\s\S]*?)```/); + + if (!match) { + return result; + } + + // Return whichever group matched (non-empty) + return match[1] ?? match[2] ?? result; +} + + +// trims the end of the prefix to improve cache hit rate +const removeLeftTabsAndTrimEnd = (s: string): string => { + const trimmedString = s.trimEnd(); + const trailingEnd = s.slice(trimmedString.length); + + // keep only a single trailing newline + if (trailingEnd.includes('\n')) { + s = trimmedString + '\n'; + } + + s = s.replace(/^\s+/gm, ''); // remove left tabs + + return s; +} + + + +function getStringUpToUnbalancedParenthesis(s: string, prefix: string): string { + + const pairs: Record = { ')': '(', '}': '{', ']': '[' }; + + // process all bracets in prefix + let stack: string[] = [] + const firstOpenIdx = prefix.search(/[[({]/); + if (firstOpenIdx !== -1) { + const brackets = prefix.slice(firstOpenIdx).split('').filter(c => '()[]{}'.includes(c)); + + for (const bracket of brackets) { + if (bracket === '(' || bracket === '{' || bracket === '[') { + stack.push(bracket); + } else { + if (stack.length > 0 && stack[stack.length - 1] === pairs[bracket]) { + stack.pop(); + } else { + stack.push(bracket); + } + } + } + } + + // iterate through each character + for (let i = 0; i < s.length; i++) { + const char = s[i]; + + if (char === '(' || char === '{' || char === '[') { stack.push(char); } + else if (char === ')' || char === '}' || char === ']') { + if (stack.length === 0 || stack.pop() !== pairs[char]) { return s.substring(0, i); } + } + } + return s; +} + + +const parenthesisChars = `{}()[]<>\`'"` + +// returns the text in the autocompletion to display, assuming the prefix is already matched +const toInlineCompletions = ({ matchInfo, prefix, suffix, autocompletion, position, debug }: { matchInfo: matchInfo, prefix: string, suffix: string, autocompletion: Autocompletion, position: Position, debug?: boolean }): InlineCompletion[] => { + + + const suffixLines = suffix.split('\n') + const prefixLines = prefix.split('\n') + const suffixToTheRightOfCursor = suffixLines[0] + const prefixToTheLeftOfCursor = prefixLines[prefixLines.length - 1] + const generatedMiddle = autocompletion.result + + let startIdx = matchInfo.startIdx + let endIdx = generatedMiddle.length // exclusive bounds + + // const naiveReturnValue = generatedMiddle.slice(startIdx) + // console.log('insertText: ', naiveReturnValue) + // return [{ insertText: naiveReturnValue, }] + + // do postprocessing for better ux + // this is a bit hacky but may change a lot + + // if there is space at the start of the completion, show it if the user has not added it + const rawFirstNonspaceIdx = generatedMiddle.slice(startIdx).search(/[^ \t]/) + if (rawFirstNonspaceIdx > -1) { + const firstNonspaceIdx = rawFirstNonspaceIdx + startIdx; + console.log('p0', startIdx, rawFirstNonspaceIdx) + startIdx = Math.max(startIdx, firstNonspaceIdx) + } + + // if user is on a blank line and the generation starts with newline(s), remove them + const numStartingNewlines = generatedMiddle.slice(startIdx).match(/^\n+/)?.[0].length || 0; + if ( + !prefixToTheLeftOfCursor.trim() + && !suffixToTheRightOfCursor.trim() + && numStartingNewlines > 0 + ) { + console.log('p1') + startIdx += numStartingNewlines + } + + // if the generated text matches with the suffix on the current line, stop + if (suffixToTheRightOfCursor.trim()) { // completing in the middle of a line + // complete until there is a match + const rawMatchIndex = generatedMiddle.slice(startIdx).lastIndexOf(suffixToTheRightOfCursor.trim()[0]) + if (rawMatchIndex > -1) { + console.log('p2', rawMatchIndex, startIdx, suffixToTheRightOfCursor.trim()[0], 'AAA', generatedMiddle.slice(startIdx)) + const matchIdx = rawMatchIndex + startIdx; + const matchChar = generatedMiddle[matchIdx] + if (parenthesisChars.includes(matchChar)) { + endIdx = Math.min(endIdx, matchIdx) + } + } + } + + const restOfLineToGenerate = generatedMiddle.slice(startIdx).split('\n')[0] ?? '' + // condition to complete as a single line completion + if ( + prefixToTheLeftOfCursor.trim() + && !suffixToTheRightOfCursor.trim() + && restOfLineToGenerate.trim() + ) { + + const rawNewlineIdx = generatedMiddle.slice(startIdx).indexOf('\n') + if (rawNewlineIdx > -1) { + console.log('p3', startIdx, rawNewlineIdx) + const newlineIdx = rawNewlineIdx + startIdx; + endIdx = Math.min(endIdx, newlineIdx) + } + } + + // // if a generated line matches with a suffix line, stop + // if (suffixLines.length > 1) { + // console.log('4') + // const lines = [] + // for (const generatedLine of generatedLines) { + // if (suffixLines.slice(0, 10).some(suffixLine => + // generatedLine.trim() !== '' && suffixLine.trim() !== '' + // && generatedLine.trim().startsWith(suffixLine.trim()) + // )) break; + // lines.push(generatedLine) + // } + // endIdx = lines.join('\n').length // this is hacky, remove or refactor in future + // } + + console.log('pEnd', startIdx, endIdx) + let completionStr = generatedMiddle.slice(startIdx, endIdx) + + // filter out unbalanced parentheses + completionStr = getStringUpToUnbalancedParenthesis(completionStr, prefix) + // console.log('originalCompletionStr: ', JSON.stringify(generatedMiddle.slice(startIdx))) + // console.log('finalCompletionStr: ', JSON.stringify(completionStr)) + + let rangeToReplace: Range = new Range(position.lineNumber, position.column, position.lineNumber, position.column) + + return [{ + insertText: completionStr, + range: rangeToReplace, + }] + +} - - - -// import * as vscode from 'vscode'; -// import { AbortRef, LLMMessage, sendLLMMessage } from '../common/sendLLMMessage'; -// import { getVoidConfigFromPartial, VoidConfig } from '../webviews/common/contextForConfig'; -// import { LRUCache } from 'lru-cache'; - - - -// // The extension this was called from is here - https://github.com/voideditor/void/blob/autocomplete/extensions/void/src/extension/extension.ts - - -// /* -// A summary of autotab: - -// Postprocessing -// -one common problem for all models is outputting unbalanced parentheses -// we solve this by trimming all extra closing parentheses from the generated string -// in future, should make sure parentheses are always balanced - -// -another problem is completing the middle of a string, eg. "const [x, CURSOR] = useState()" -// we complete up to first matchup character -// but should instead complete the whole line / block (difficult because of parenthesis accuracy) - -// -too much info is bad. usually we want to show the user 1 line, and have a preloaded response afterwards -// this should happen automatically with caching system -// should break preloaded responses into \n\n chunks - -// Preprocessing -// - we don't generate if cursor is at end / beginning of a line (no spaces) -// - we generate 1 line if there is text to the right of cursor -// - we generate 1 line if variable declaration -// - (in many cases want to show 1 line but generate multiple) - -// State -// - cache based on prefix (and do some trimming first) -// - when press tab on one line, should have an immediate followup response -// to do this, show autocompletes before they're fully finished -// - [todo] remove each autotab when accepted -// - [todo] treat windows \r\n separately from \n -// !- [todo] provide type information - -// Details -// -generated results are trimmed up to 1 leading/trailing space -// -prefixes are cached up to 1 trailing newline -// - -// */ - - - - - -// type AutocompletionStatus = 'pending' | 'finished' | 'error'; -// type Autocompletion = { -// id: number, -// prefix: string, -// suffix: string, -// startTime: number, -// endTime: number | undefined, -// abortRef: AbortRef, -// status: AutocompletionStatus, -// llmPromise: Promise | undefined, -// result: string, -// } - -// const DEBOUNCE_TIME = 500 -// const TIMEOUT_TIME = 60000 -// const MAX_CACHE_SIZE = 20 -// const MAX_PENDING_REQUESTS = 2 - -// // postprocesses the result -// const postprocessResult = (result: string) => { - -// console.log('result: ', JSON.stringify(result)) - -// // trim all whitespace except for a single leading/trailing space -// const hasLeadingSpace = result.startsWith(' '); -// const hasTrailingSpace = result.endsWith(' '); -// return (hasLeadingSpace ? ' ' : '') -// + result.trim() -// + (hasTrailingSpace ? ' ' : ''); - -// } - -// const extractCodeFromResult = (result: string) => { - -// // extract the code between triple backticks -// const parts = result.split(/```(?:\s*\w+)?\n?/); - -// // if there is no ``` then return the raw result -// if (parts.length === 1) { -// return result; -// } - -// // else return the code between the triple backticks -// return parts[1] - -// } - -// // trims the end of the prefix to improve cache hit rate -// const trimPrefix = (prefix: string) => { -// const trimmedPrefix = prefix.trimEnd() -// const trailingEnd = prefix.substring(trimmedPrefix.length) - -// // keep only a single trailing newline -// if (trailingEnd.includes('\n')) { -// return trimmedPrefix + '\n' -// } - -// // else ignore all spaces and return the trimmed prefix -// return trimmedPrefix -// } - -// function getStringUpToUnbalancedParenthesis(s: string, prefixToTheLeft: string): string { - -// const pairs: Record = { ')': '(', '}': '{', ']': '[' }; - -// // todo find first open bracket in prefix and get all brackets beyond it in prefix -// // get all bracets in prefix -// let stack: string[] = [] -// const firstOpenIdx = prefixToTheLeft.search(/[[({]/); -// if (firstOpenIdx !== -1) stack = prefixToTheLeft.slice(firstOpenIdx).split('').filter(c => '()[]{}'.includes(c)) - -// // Iterate through each character -// for (let i = 0; i < s.length; i++) { -// const char = s[i]; - -// if (char === '(' || char === '{' || char === '[') { stack.push(char); } -// else if (char === ')' || char === '}' || char === ']') { -// if (stack.length === 0 || stack.pop() !== pairs[char]) { return s.substring(0, i); } -// } -// } -// return s; -// } - -// // finds the text in the autocompletion to display, assuming the prefix is already matched -// // 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 toInlineCompletion = ({ prefix, suffix, autocompletion, position }: { prefix: string, suffix: string, autocompletion: Autocompletion, position: vscode.Position }): vscode.InlineCompletionItem => { -// const originalPrefix = autocompletion.prefix -// const generatedMiddle = autocompletion.result - -// const trimmedOriginalPrefix = trimPrefix(originalPrefix) -// const trimmedCurrentPrefix = trimPrefix(prefix) - -// const suffixLines = suffix.split('\n') -// const prefixLines = trimmedCurrentPrefix.split('\n') -// const suffixToTheRightOfCursor = suffixLines[0].trim() -// const prefixToTheLeftOfCursor = prefixLines[prefixLines.length - 1].trim() - -// const generatedLines = generatedMiddle.split('\n') - -// // compute startIdx -// let startIdx = trimmedCurrentPrefix.length - trimmedOriginalPrefix.length -// if (startIdx < 0) { -// return new vscode.InlineCompletionItem('') -// } - -// // compute endIdx -// // hacks to get the suffix to render properly with lower quality models -// // if the generated text matches with the suffix on the current line, stop -// let endIdx: number | undefined = generatedMiddle.length // exclusive bounds - -// if (suffixToTheRightOfCursor !== '') { // completing in the middle of a line -// console.log('1') -// // complete until there is a match -// const matchIndex = generatedMiddle.lastIndexOf(suffixToTheRightOfCursor[0]) -// if (matchIndex > 0) { endIdx = matchIndex } -// } - -// if (prefixToTheLeftOfCursor !== '') { // completing the end of a line -// console.log('2') -// // show a single line -// const newlineIdx = generatedMiddle.indexOf('\n') -// if (newlineIdx > -1) { endIdx = newlineIdx } -// } - -// // // if a generated line matches with a suffix line, stop -// // if (suffixLines.length > 1) { -// // console.log('3') -// // const lines = [] -// // for (const generatedLine of generatedLines) { -// // if (suffixLines.slice(0, 10).some(suffixLine => -// // generatedLine.trim() !== '' && suffixLine.trim() !== '' -// // && generatedLine.trim().startsWith(suffixLine.trim()) -// // )) break; -// // lines.push(generatedLine) -// // } -// // endIdx = lines.join('\n').length // this is hacky, remove or refactor in future -// // } - -// let completionStr = generatedMiddle.slice(startIdx, endIdx) - -// // filter out unbalanced parentheses -// console.log('completionStrBeforeParens: ', JSON.stringify(completionStr)) -// completionStr = getStringUpToUnbalancedParenthesis(completionStr, prefixLines.slice(-2).join('\n')) - -// console.log('originalCompletionStr: ', JSON.stringify(generatedMiddle.slice(startIdx))) -// console.log('finalCompletionStr: ', JSON.stringify(completionStr)) - -// return new vscode.InlineCompletionItem(completionStr, new vscode.Range(position, position)) - -// } - -// // returns whether this autocompletion is in the cache +// returns whether this autocompletion is in the cache // const doesPrefixMatchAutocompletion = ({ prefix, autocompletion }: { prefix: string, autocompletion: Autocompletion }): boolean => { // const originalPrefix = autocompletion.prefix @@ -233,285 +354,381 @@ // } -// const getCompletionOptions = ({ prefix, suffix }: { prefix: string, suffix: string }) => { -// const prefixLines = prefix.split('\n') -// const suffixLines = suffix.split('\n') -// const prefixToLeftOfCursor = prefixLines.slice(-1)[0] ?? '' -// const suffixToRightOfCursor = suffixLines[0] +const getIndex = (str: string, line: number, char: number) => { + return str.split('\n').slice(0, line).join('\n').length + (line > 0 ? 1 : 0) + char; +} +const getLastLine = (s: string): string => { + const matches = s.match(/[^\n]*$/) + return matches ? matches[0] : '' +} -// // default parameters -// let shouldGenerate = true -// let stopTokens: string[] = ['\n\n', '\r\n\r\n'] +// returns the startIdx of the match if there is a match, or undefined if there is no match +// all results are wrt `autocompletion.result` +type matchInfo = { + lineStart: number, + character: number, + startIdx: number, +} +const getPrefixAutocompletionMatch = ({ prefix, autocompletion }: { prefix: string, autocompletion: Autocompletion }): matchInfo | undefined => { -// // specific cases -// if (suffixToRightOfCursor.trim() !== '') { // typing between something -// stopTokens = ['\n', '\r\n'] -// } + const trimmedCurrentPrefix = removeLeftTabsAndTrimEnd(prefix) + const trimmedCompletionPrefix = removeLeftTabsAndTrimEnd(autocompletion.prefix) + const trimmedCompletionMiddle = removeLeftTabsAndTrimEnd(autocompletion.result) -// // if (prefixToLeftOfCursor.trim() === '' && suffixToRightOfCursor.trim() === '') { // at an empty line -// // stopTokens = ['\n\n', '\r\n\r\n'] -// // } + console.log('@result: ', JSON.stringify(autocompletion.result)) + console.log('@trimmedCurrentPrefix: ', JSON.stringify(trimmedCurrentPrefix)) + console.log('@trimmedCompletionPrefix: ', JSON.stringify(trimmedCompletionPrefix)) + console.log('@trimmedCompletionMiddle: ', JSON.stringify(trimmedCompletionMiddle)) -// if (prefixToLeftOfCursor === '' || suffixToRightOfCursor === '') { // at beginning or end of line -// shouldGenerate = false -// } + if (trimmedCurrentPrefix.length < trimmedCompletionPrefix.length) { // user must write text beyond the original prefix at generation time + console.log('@undefined1') + return undefined + } -// console.log('shouldGenerate:', shouldGenerate, stopTokens) + if ( // check that completion starts with the prefix + !(trimmedCompletionPrefix + trimmedCompletionMiddle) + .startsWith(trimmedCurrentPrefix) + ) { + console.log('@undefined2') + return undefined + } -// return { shouldGenerate, stopTokens } + // reverse map to find position wrt `autocompletion.result` + const lineStart = + trimmedCurrentPrefix.split('\n').length - + trimmedCompletionPrefix.split('\n').length; -// } + if (lineStart < 0) { + console.log('@undefined3') + console.error('Error: No line found.'); + return undefined; + } + const currentPrefixLine = getLastLine(trimmedCurrentPrefix) + const completionPrefixLine = lineStart === 0 ? getLastLine(trimmedCompletionPrefix) : '' + const completionMiddleLine = autocompletion.result.split('\n')[lineStart] + const fullCompletionLine = completionPrefixLine + completionMiddleLine + // console.log('currentPrefixLine', currentPrefixLine) + // console.log('completionPrefixLine', completionPrefixLine) + // console.log('completionMiddleLine', completionMiddleLine) -// import { Disposable } from '../../../../base/common/lifecycle.js'; -// import { ILanguageFeaturesService } from '../../../../editor/common/services/languageFeatures.js'; -// import { registerSingleton, InstantiationType } from '../../../../platform/instantiation/common/extensions.js'; -// import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; + const charMatchIdx = fullCompletionLine.indexOf(currentPrefixLine) + if (charMatchIdx < 0) { + console.log('@undefined4', charMatchIdx) -// interface IAutocompleteService { -// readonly _serviceBrand: undefined; -// } + console.error('Warning: Found character with negative index. This should never happen.') + return undefined + } -// const IAutocompleteService = createDecorator('autocompleteService'); -// class AutocompleteService extends Disposable implements IAutocompleteService { -// _serviceBrand: undefined; - -// private _extensionContext: vscode.ExtensionContext; - -// private _autocompletionId: number = 0; -// private _autocompletionsOfDocument: { [docUriStr: string]: LRUCache } = {} - -// private _lastCompletionTime = 0 -// private _lastPrefix: string = '' + const character = (charMatchIdx + + currentPrefixLine.length + - completionPrefixLine.length + ) + const startIdx = getIndex(autocompletion.result, lineStart, character) -// // used internally by vscode -// // fires after every keystroke and returns the completion to show -// async provideInlineCompletionItems( -// document: vscode.TextDocument, -// position: vscode.Position, -// context: vscode.InlineCompletionContext, -// token: vscode.CancellationToken, -// ): Promise { - -// const disabled = false -// if (disabled) { return []; } - -// 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) -// const voidConfig = getVoidConfigFromPartial(this._extensionContext.globalState.get('partialVoidConfig') ?? {}) - -// // initialize cache and other variables -// // note that whenever an autocompletion is rejected, it is removed from cache -// if (!this._autocompletionsOfDocument[docUriStr]) { -// this._autocompletionsOfDocument[docUriStr] = new LRUCache({ -// max: MAX_CACHE_SIZE, -// dispose: (autocompletion) => { -// autocompletion.abortRef.current() -// } -// }) -// } -// this._lastPrefix = prefix - -// // get all pending autocompletions -// let __c = 0 -// this._autocompletionsOfDocument[docUriStr].forEach(a => { if (a.status === 'pending') __c += 1 }) -// console.log('pending: ' + __c) - -// // get autocompletion from cache -// let cachedAutocompletion: Autocompletion | undefined = undefined -// for (const autocompletion of this._autocompletionsOfDocument[docUriStr].values()) { -// // if the user's change matches up with the generated text -// if (doesPrefixMatchAutocompletion({ prefix, autocompletion })) { -// cachedAutocompletion = autocompletion -// break -// } -// } - -// // if there is a cached autocompletion, return it -// if (cachedAutocompletion) { - -// if (cachedAutocompletion.status === 'finished') { -// console.log('A1') - -// const inlineCompletion = toInlineCompletion({ autocompletion: cachedAutocompletion, prefix, suffix, position }) -// return [inlineCompletion] - -// } else if (cachedAutocompletion.status === 'pending') { -// console.log('A2') - -// try { -// await cachedAutocompletion.llmPromise; -// console.log('id: ' + cachedAutocompletion.id) -// const inlineCompletion = toInlineCompletion({ autocompletion: cachedAutocompletion, prefix, suffix, position }) -// return [inlineCompletion] - -// } catch (e) { -// this._autocompletionsOfDocument[docUriStr].delete(cachedAutocompletion.id) -// console.error('Error creating autocompletion (1): ' + e) -// } - -// } else if (cachedAutocompletion.status === 'error') { -// console.log('A3') -// } - -// return [] -// } - -// // else if no more typing happens, then go forwards with the request -// // wait DEBOUNCE_TIME for the user to stop typing -// const thisTime = Date.now() -// this._lastCompletionTime = thisTime -// const didTypingHappenDuringDebounce = await new Promise((resolve, reject) => -// setTimeout(() => { -// if (this._lastCompletionTime === thisTime) { -// resolve(false) -// } else { -// resolve(true) -// } -// }, DEBOUNCE_TIME) -// ) - -// // if more typing happened, then do not go forwards with the request -// if (didTypingHappenDuringDebounce) { -// return [] -// } - -// console.log('B') - -// // if there are too many pending requests, cancel the oldest one -// let numPending = 0 -// let oldestPending: Autocompletion | undefined = undefined -// for (const autocompletion of this._autocompletionsOfDocument[docUriStr].values()) { -// if (autocompletion.status === 'pending') { -// numPending += 1 -// if (oldestPending === undefined) { -// oldestPending = autocompletion -// } -// if (numPending >= MAX_PENDING_REQUESTS) { -// // cancel the oldest pending request and remove it from cache -// this._autocompletionsOfDocument[docUriStr].delete(oldestPending.id) -// break -// } -// } -// } - -// const { shouldGenerate, stopTokens } = getCompletionOptions({ prefix, suffix }) - -// if (!shouldGenerate) return [] - -// // create a new autocompletion and add it to cache -// const newAutocompletion: Autocompletion = { -// id: this._autocompletionId++, -// prefix: prefix, -// suffix: suffix, -// startTime: Date.now(), -// endTime: undefined, -// abortRef: { current: () => { } }, -// status: 'pending', -// llmPromise: undefined, -// result: '', -// } - -// // set parameters of `newAutocompletion` appropriately -// newAutocompletion.llmPromise = new Promise((resolve, reject) => { - -// sendLLMMessage({ -// mode: 'fim', -// fimInfo: { prefix, suffix }, -// options: { stopTokens }, -// onText: async (tokenStr, completionStr) => { - -// newAutocompletion.result = completionStr - -// // if generation doesn't match the prefix for the first few tokens generated, reject it -// if (!doesPrefixMatchAutocompletion({ prefix: this._lastPrefix, autocompletion: newAutocompletion })) { -// reject('LLM response did not match user\'s text.') -// } -// }, -// onFinalMessage: (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 = postprocessResult(extractCodeFromResult(finalMessage)) - -// resolve(newAutocompletion.result) - -// }, -// onError: (e) => { -// newAutocompletion.endTime = Date.now() -// newAutocompletion.status = 'error' -// reject(e) -// }, -// voidConfig, -// abortRef: newAutocompletion.abortRef, -// }) - -// // if the request hasnt resolved in TIMEOUT_TIME seconds, reject it -// setTimeout(() => { -// if (newAutocompletion.status === 'pending') { -// reject('Timeout receiving message to LLM.') -// } -// }, TIMEOUT_TIME) - - -// }) - -// // add autocompletion to cache -// this._autocompletionsOfDocument[docUriStr].set(newAutocompletion.id, newAutocompletion) - -// // show autocompletion -// try { -// await newAutocompletion.llmPromise -// console.log('id: ' + newAutocompletion.id) - -// const inlineCompletion = toInlineCompletion({ autocompletion: newAutocompletion, prefix, suffix, position }) -// return [inlineCompletion] - -// } catch (e) { -// this._autocompletionsOfDocument[docUriStr].delete(newAutocompletion.id) -// console.error('Error creating autocompletion (2): ' + e) -// return [] -// } - -// } - - - -// constructor( -// @ILanguageFeaturesService private readonly _langFeatureService: ILanguageFeaturesService -// ) { -// super() -// // this._extensionContext = context + return { + lineStart, + character, + startIdx, + } -// this._langFeatureService.inlineCompletionsProvider.register('*', { -// provideInlineCompletions: (model, position, context, token) => { -// return this.provideInlineCompletionItems(model) -// }, -// freeInlineCompletions(completions) { +} -// }, -// }) -// } +const getCompletionOptions = ({ prefix, suffix }: { prefix: string, suffix: string }) => { -// } + const prefixLines = prefix.split('\n') + const suffixLines = suffix.split('\n') -// registerSingleton(IAutocompleteService, AutocompleteService, InstantiationType.Eager); + const prefixToLeftOfCursor = prefixLines.slice(-1)[0] ?? '' + const suffixToRightOfCursor = suffixLines[0] ?? '' + + // default parameters + let shouldGenerate = true + let stopTokens: string[] = ['\n\n', '\r\n\r\n'] + + // specific cases + if (suffixToRightOfCursor.trim() !== '') { // typing between something + stopTokens = ['\n', '\r\n'] + } + + // if (prefixToLeftOfCursor.trim() === '' && suffixToRightOfCursor.trim() === '') { // at an empty line + // stopTokens = ['\n\n', '\r\n\r\n'] + // } + + if (prefixToLeftOfCursor === '') { // at beginning or end of line + shouldGenerate = false + } + + return { shouldGenerate, stopTokens } + +} + + + + +export interface IAutocompleteService { + readonly _serviceBrand: undefined; +} + +export const IAutocompleteService = createDecorator('AutocompleteService'); + +export class AutocompleteService extends Disposable implements IAutocompleteService { + _serviceBrand: undefined; + + private _autocompletionId: number = 0; + private _autocompletionsOfDocument: { [docUriStr: string]: LRUCache } = {} + + private _lastCompletionTime = 0 + private _lastPrefix: string = '' + + // used internally by vscode + // fires after every keystroke and returns the completion to show + async _provideInlineCompletionItems( + model: ITextModel, + position: Position, + context: InlineCompletionContext, + token: CancellationToken, + ): Promise { + + const disabled = false + if (disabled) { return []; } + + const docUriStr = model.uri.toString(); + const fullText = model.getValue(); + + + const cursorOffset = model.getOffsetAt(position) + const prefix = fullText.substring(0, cursorOffset) + const suffix = fullText.substring(cursorOffset) + + // initialize cache and other variables + // note that whenever an autocompletion is rejected, it is removed from cache + if (!this._autocompletionsOfDocument[docUriStr]) { + this._autocompletionsOfDocument[docUriStr] = new LRUCache( + MAX_CACHE_SIZE, + (autocompletion: Autocompletion) => { + autocompletion.abortRef.current() + } + ) + } + this._lastPrefix = prefix + + // get all pending autocompletions + let __c = 0 + this._autocompletionsOfDocument[docUriStr].items.forEach((a: Autocompletion) => { if (a.status === 'pending') __c += 1 }) + console.log('pending: ' + __c) + + // get autocompletion from cache + let cachedAutocompletion: Autocompletion | undefined = undefined + let matchInfo: matchInfo | undefined = undefined + for (const autocompletion of this._autocompletionsOfDocument[docUriStr].items.values()) { + // if the user's change matches up with the generated text + matchInfo = getPrefixAutocompletionMatch({ prefix, autocompletion }) + if (matchInfo !== undefined) { + cachedAutocompletion = autocompletion + break; + } + } + + // if there is a cached autocompletion, return it + if (cachedAutocompletion && matchInfo) { + + console.log('id: ' + cachedAutocompletion.id) + + if (cachedAutocompletion.status === 'finished') { + console.log('A1') + + const inlineCompletions = toInlineCompletions({ matchInfo, autocompletion: cachedAutocompletion, prefix, suffix, position, debug: true }) + return inlineCompletions + + } else if (cachedAutocompletion.status === 'pending') { + console.log('A2') + + try { + await cachedAutocompletion.llmPromise; + const inlineCompletions = toInlineCompletions({ matchInfo, autocompletion: cachedAutocompletion, prefix, suffix, position }) + return inlineCompletions + + } catch (e) { + this._autocompletionsOfDocument[docUriStr].delete(cachedAutocompletion.id) + console.error('Error creating autocompletion (1): ' + e) + } + + } else if (cachedAutocompletion.status === 'error') { + console.log('A3') + } + + return [] + } + + // else if no more typing happens, then go forwards with the request + // wait DEBOUNCE_TIME for the user to stop typing + const thisTime = Date.now() + this._lastCompletionTime = thisTime + const didTypingHappenDuringDebounce = await new Promise((resolve, reject) => + setTimeout(() => { + if (this._lastCompletionTime === thisTime) { + resolve(false) + } else { + resolve(true) + } + }, DEBOUNCE_TIME) + ) + + // if more typing happened, then do not go forwards with the request + if (didTypingHappenDuringDebounce) { + return [] + } + + + // if there are too many pending requests, cancel the oldest one + let numPending = 0 + let oldestPending: Autocompletion | undefined = undefined + for (const autocompletion of this._autocompletionsOfDocument[docUriStr].items.values()) { + if (autocompletion.status === 'pending') { + numPending += 1 + if (oldestPending === undefined) { + oldestPending = autocompletion + } + if (numPending >= MAX_PENDING_REQUESTS) { + // cancel the oldest pending request and remove it from cache + this._autocompletionsOfDocument[docUriStr].delete(oldestPending.id) + break + } + } + } + + const { shouldGenerate, stopTokens } = getCompletionOptions({ prefix, suffix }) + + if (!shouldGenerate) return [] + + if (this._autocompletionId !== 0) { // TODO remove this + return [] + } + + console.log('B') + + // create a new autocompletion and add it to cache + const newAutocompletion: Autocompletion = { + id: this._autocompletionId++, + prefix: prefix, + suffix: suffix, + startTime: Date.now(), + endTime: undefined, + abortRef: { current: () => { } }, + status: 'pending', + llmPromise: undefined, + result: '', + } + + // set parameters of `newAutocompletion` appropriately + newAutocompletion.llmPromise = new Promise((resolve, reject) => { + + sendLLMMessage({ + logging: { loggingName: 'Autocomplete' }, + messages: [], + options: { prefix, suffix, stopTokens, }, + onText: async (tokenStr: string, completionStr: string) => { + + newAutocompletion.result = completionStr + + // if generation doesn't match the prefix for the first few tokens generated, reject it + if (!getPrefixAutocompletionMatch({ prefix: this._lastPrefix, autocompletion: newAutocompletion })) { + reject('LLM response did not match user\'s text.') + } + }, + onFinalMessage: (finalMessage: string) => { + + // 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 = postprocessResult(extractCodeFromResult(finalMessage)) + + resolve(newAutocompletion.result) + + }, + onError: (e: any) => { + newAutocompletion.endTime = Date.now() + newAutocompletion.status = 'error' + reject(e) + }, + voidConfig: this._voidConfigStateService.state.voidConfig, + abortRef: newAutocompletion.abortRef, + }) + + // if the request hasnt resolved in TIMEOUT_TIME seconds, reject it + setTimeout(() => { + if (newAutocompletion.status === 'pending') { + reject('Timeout receiving message to LLM.') + } + }, TIMEOUT_TIME) + + + }) + + // add autocompletion to cache + this._autocompletionsOfDocument[docUriStr].set(newAutocompletion.id, newAutocompletion) + + // show autocompletion + try { + await newAutocompletion.llmPromise + console.log('id: ' + newAutocompletion.id) + + const matchInfo: matchInfo = { startIdx: 0, lineStart: 0, character: 0 } + const inlineCompletions = toInlineCompletions({ matchInfo, autocompletion: newAutocompletion, prefix, suffix, position }) + return inlineCompletions + + } catch (e) { + this._autocompletionsOfDocument[docUriStr].delete(newAutocompletion.id) + console.error('Error creating autocompletion (2): ' + e) + return [] + } + + } + + + constructor( + @ILanguageFeaturesService private _langFeatureService: ILanguageFeaturesService, + @IVoidConfigStateService private readonly _voidConfigStateService: IVoidConfigStateService, + ) { + super() + + this._langFeatureService.inlineCompletionsProvider.register('*', { + provideInlineCompletions: async (model, position, context, token) => { + const items = await this._provideInlineCompletionItems(model, position, context, token) + + console.log('item: ', items?.[0]?.insertText) + return { items: items, } + }, + freeInlineCompletions(completions) { + }, + handlePartialAccept(completions, item, acceptedCharacters, info) { + + // TODO remove partially accepted chars + // TODO windows \r\n vs \n + // make it so spaces count as 1 space only(need to alter findMatch a bit) + + } + }) + + + } + + +} + + +registerSingleton(IAutocompleteService, AutocompleteService, InstantiationType.Eager); diff --git a/src/vs/workbench/contrib/void/browser/registerInlineDiffs.ts b/src/vs/workbench/contrib/void/browser/registerInlineDiffs.ts index fb56a880..72acfee2 100644 --- a/src/vs/workbench/contrib/void/browser/registerInlineDiffs.ts +++ b/src/vs/workbench/contrib/void/browser/registerInlineDiffs.ts @@ -763,6 +763,7 @@ Please finish writing the new file by applying the diff to the original file. Re }, voidConfig, abortRef, + options: {}, }) }) diff --git a/src/vs/workbench/contrib/void/browser/registerThreads.ts b/src/vs/workbench/contrib/void/browser/registerThreads.ts index e942a079..c6bd3cc7 100644 --- a/src/vs/workbench/contrib/void/browser/registerThreads.ts +++ b/src/vs/workbench/contrib/void/browser/registerThreads.ts @@ -10,6 +10,7 @@ import { IStorageService, StorageScope, StorageTarget } from '../../../../platfo import { URI } from '../../../../base/common/uri.js'; import { Emitter, Event } from '../../../../base/common/event.js'; +import { IAutocompleteService } from './registerAutocomplete.js'; // if selectionStr is null, it means just send the whole file export type CodeSelection = { @@ -97,8 +98,10 @@ class ThreadHistoryService extends Disposable implements IThreadHistoryService { constructor( @IStorageService private readonly _storageService: IStorageService, + @IAutocompleteService private readonly _autocomplete: IAutocompleteService, ) { super() + this._autocomplete this.state = { allThreads: this._readAllThreads(), diff --git a/src/vs/workbench/services/extensions/worker/webWorkerExtensionHostIframe.esm.html b/src/vs/workbench/services/extensions/worker/webWorkerExtensionHostIframe.esm.html index 53bc54bb..94881a6b 100644 --- a/src/vs/workbench/services/extensions/worker/webWorkerExtensionHostIframe.esm.html +++ b/src/vs/workbench/services/extensions/worker/webWorkerExtensionHostIframe.esm.html @@ -5,7 +5,7 @@ default-src 'none'; child-src 'self' data: blob:; script-src 'self' 'unsafe-eval' 'sha256-YVBiNCLDtlDv8TpTuATV/fJ9rcBWIq9O9zBL2ndqAgw=' https: http://localhost:* blob:; - connect-src 'self' https: wss: http://localhost:* http://127.0.0.1:* ws://localhost:* ws://127.0.0.1:*;"/> + connect-src 'self' https: wss: http://localhost:* http://127.0.0.1:* ws://localhost:* ws://127.0.0.1:* *;"/>