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-main/app.ts b/src/vs/code/electron-main/app.ts index 2cd9bffa..e86dca1d 100644 --- a/src/vs/code/electron-main/app.ts +++ b/src/vs/code/electron-main/app.ts @@ -120,8 +120,6 @@ import { AuxiliaryWindowsMainService } from '../../platform/auxiliaryWindow/elec import { normalizeNFC } from '../../base/common/normalization.js'; import { ICSSDevelopmentService, CSSDevelopmentService } from '../../platform/cssDev/node/cssDevService.js'; import { ExtensionSignatureVerificationService, IExtensionSignatureVerificationService } from '../../platform/extensionManagement/node/extensionSignatureVerificationService.js'; - -import { LLMMessageChannel } from '../../platform/void/electron-main/llmMessageChannel.js'; import { IMetricsService } from '../../platform/void/common/metricsService.js'; import { MetricsMainService } from '../../platform/void/electron-main/metricsMainService.js'; @@ -1242,13 +1240,6 @@ export class CodeApplication extends Disposable { mainProcessElectronServer.registerChannel('logger', loggerChannel); sharedProcessClient.then(client => client.registerChannel('logger', loggerChannel)); - // Void - const metricsChannel = ProxyChannel.fromService(accessor.get(IMetricsService), disposables); - mainProcessElectronServer.registerChannel('void-channel-metrics', metricsChannel); - - const sendLLMMessageChannel = new LLMMessageChannel(accessor.get(IMetricsService)); - mainProcessElectronServer.registerChannel('void-channel-sendLLMMessage', sendLLMMessageChannel); - // Extension Host Debug Broadcasting const electronExtensionHostDebugBroadcastChannel = new ElectronExtensionHostDebugBroadcastChannel(accessor.get(IWindowsMainService)); mainProcessElectronServer.registerChannel('extensionhostdebugservice', electronExtensionHostDebugBroadcastChannel); 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 9e3e3155..70168aa5 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 @@ -159,7 +159,6 @@ export const SidebarChat = () => { const sendLLMMessageService = useService('sendLLMMessageService') - // state of current message const [instructions, setInstructions] = useState('') // the user's instructions const onChangeText = useCallback((newStr: string) => { setInstructions(newStr) }, [setInstructions]) diff --git a/src/vs/workbench/contrib/void/browser/react/src/util/ErrorDisplay.tsx b/src/vs/workbench/contrib/void/browser/react/src/util/ErrorDisplay.tsx new file mode 100644 index 00000000..ab11ff18 --- /dev/null +++ b/src/vs/workbench/contrib/void/browser/react/src/util/ErrorDisplay.tsx @@ -0,0 +1,161 @@ +import React, { useState } from 'react'; +import { AlertCircle, ChevronDown, ChevronUp, X } from 'lucide-react'; + +import { getCmdKey } from '../../../getCmdKey.js'; + +// const opaqueMessage = `\ +// Unfortunately, Void can't see the full error. However, you should be able to find more details by pressing ${getCmdKey()}+Shift+P, typing "Toggle Developer Tools", and looking at the console.\n +// This error often means you have an incorrect API key. If you're self-hosting your own server, it might mean your CORS headers are off, and you should make sure your server's response has the header "Access-Control-Allow-Origins" set to "*", or at least allows "vscode-file://vscode-app".` +// if ((error instanceof Error) && (error.cause + '').includes('TypeError: Failed to fetch')) { +// e = error as any +// e['Void Team'] = opaqueMessage +// } + + +type Details = { + message: string, + name: string, + stack: string | null, + cause: string | null, + code: string | null, + additional: Record +} + +// Get detailed error information +const getErrorDetails = (error: unknown) => { + + let details: Details; + + let e: Error & { [other: string]: undefined | any } + + // If fetch() fails, it gives an opaque message. We add extra details to the error. + if (error instanceof Error) { + e = error + } + // sometimes error is an object but not an Error + else if (typeof error === 'object') { + e = new Error(`The server didn't give a very useful error message. More details below.`, { cause: JSON.stringify(error) }) + + } + else { + e = new Error(String(error)) + } + // console.log('error display', JSON.stringify(e)) + + const message = e.message && e.error ? + (e.message + ':\n' + e.error) + : e.message || e.error || JSON.stringify(error) + + details = { + name: e.name || 'Error', + message: message, + stack: null, // e.stack is ignored because it's ugly and not very useful + cause: e.cause ? String(e.cause) : null, + code: e.code || null, + additional: {} + } + + + // Collect any additional properties from the e + for (let prop of Object.getOwnPropertyNames(e).filter((prop) => !Object.keys(details).includes(prop))) + details.additional[prop] = (e as any)[prop] + + return details; +}; + + + +export const ErrorDisplay = ({ + error, + onDismiss = null, + showDismiss = true, + className = '' +}: { + error: Error | object | string, + onDismiss: (() => void) | null, + showDismiss?: boolean, + className?: string +}) => { + const [isExpanded, setIsExpanded] = useState(false); + + const details = getErrorDetails(error); + const hasDetails = details.cause || Object.keys(details.additional).length > 0; + + return ( +
+ {/* Header */} +
+
+ +
+

+ {details.name} +

+

+ {details.message} +

+
+
+ +
+ {hasDetails && ( + + )} + {showDismiss && onDismiss && ( + + )} +
+
+ + {/* Expandable Details */} + {isExpanded && hasDetails && ( +
+ {details.code && ( +
+ Error Code: + {details.code} +
+ )} + + {details.cause && ( +
+ Cause: + {details.cause} +
+ )} + + {Object.keys(details.additional).length > 0 && ( +
+ Additional Information: +
+								{Object.keys(details.additional).map(key => `${key}:\n${details.additional[key]}`).join('\n')}
+							
+
+ )} + {/* {details.stack && ( +
+ Stack Trace: +
+								{details.stack}
+							
+
+ )} */} +
+ )} +
+ ); +}; diff --git a/src/vs/workbench/contrib/void/browser/registerAutocomplete.ts b/src/vs/workbench/contrib/void/browser/registerAutocomplete.ts new file mode 100644 index 00000000..6b340473 --- /dev/null +++ b/src/vs/workbench/contrib/void/browser/registerAutocomplete.ts @@ -0,0 +1,772 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { 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'; +import { ISendLLMMessageService } from '../../../../platform/void/browser/llmMessageService.js'; +import { IEditorService } from '../../../services/editor/common/editorService.js'; +import { isCodeEditor } from '../../../../editor/browser/editorBrowser.js'; +import { EditorResourceAccessor } from '../../../common/editor.js'; +import { IModelService } from '../../../../editor/common/services/model.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] 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, + status: AutocompletionStatus, + llmPromise: Promise | undefined, + insertText: string, + requestId: string | null, +} + +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 }): { insertText: string, range: Range }[] => { + + + const suffixLines = suffix.split('\n') + const prefixLines = prefix.split('\n') + const suffixToTheRightOfCursor = suffixLines[0] + const prefixToTheLeftOfCursor = prefixLines[prefixLines.length - 1] + const generatedMiddle = autocompletion.insertText + + let startIdx = matchInfo.startIdx + let endIdx = generatedMiddle.length // exclusive bounds + + // const naiveReturnValue = generatedMiddle.slice(startIdx) + // console.log('naiveReturnValue: ', JSON.stringify(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 and user has added it, remove it + const charToLeftOfCursor = prefixToTheLeftOfCursor.slice(-1)[0] || '' + const userHasAddedASpace = charToLeftOfCursor === ' ' || charToLeftOfCursor === '\t' + const rawFirstNonspaceIdx = generatedMiddle.slice(startIdx).search(/[^\t ]/) + if (rawFirstNonspaceIdx > -1 && userHasAddedASpace) { + 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', numStartingNewlines) + 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('pFinal', 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, + }] + +} + + + + + +// returns whether this autocompletion is in the cache +// const doesPrefixMatchAutocompletion = ({ prefix, autocompletion }: { prefix: string, autocompletion: Autocompletion }): boolean => { + +// const originalPrefix = autocompletion.prefix +// const generatedMiddle = autocompletion.result +// const originalPrefixTrimmed = trimPrefix(originalPrefix) +// const currentPrefixTrimmed = trimPrefix(prefix) + +// if (currentPrefixTrimmed.length < originalPrefixTrimmed.length) { +// return false +// } + +// const isMatch = (originalPrefixTrimmed + generatedMiddle).startsWith(currentPrefixTrimmed) +// return isMatch + +// } + +const getPrefixAndSuffix = (model: ITextModel, position: Position) => { + + const fullText = model.getValue(); + + const cursorOffset = model.getOffsetAt(position) + const prefix = fullText.substring(0, cursorOffset) + const suffix = fullText.substring(cursorOffset) + + return { prefix, suffix } + +} + +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] : '' +} + +type matchInfo = { + lineStart: number, + character: number, + startIdx: number, +} +// returns the startIdx of the match if there is a match, or undefined if there is no match +// all results are wrt `autocompletion.result` +const getPrefixAutocompletionMatch = ({ prefix, autocompletion }: { prefix: string, autocompletion: Autocompletion }): matchInfo | undefined => { + + const trimmedCurrentPrefix = removeLeftTabsAndTrimEnd(prefix) + const trimmedCompletionPrefix = removeLeftTabsAndTrimEnd(autocompletion.prefix) + const trimmedCompletionMiddle = removeLeftTabsAndTrimEnd(autocompletion.insertText) + + // console.log('@result: ', JSON.stringify(autocompletion.insertText)) + // console.log('@trimmedCurrentPrefix: ', JSON.stringify(trimmedCurrentPrefix)) + // console.log('@trimmedCompletionPrefix: ', JSON.stringify(trimmedCompletionPrefix)) + // console.log('@trimmedCompletionMiddle: ', JSON.stringify(trimmedCompletionMiddle)) + + if (trimmedCurrentPrefix.length < trimmedCompletionPrefix.length) { // user must write text beyond the original prefix at generation time + console.log('@undefined1') + return undefined + } + + if ( // check that completion starts with the prefix + !(trimmedCompletionPrefix + trimmedCompletionMiddle) + .startsWith(trimmedCurrentPrefix) + ) { + console.log('@undefined2') + return undefined + } + + // 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.insertText.split('\n')[lineStart] + const fullCompletionLine = completionPrefixLine + completionMiddleLine + + // console.log('currentPrefixLine', currentPrefixLine) + // console.log('completionPrefixLine', completionPrefixLine) + // console.log('completionMiddleLine', completionMiddleLine) + + const charMatchIdx = fullCompletionLine.indexOf(currentPrefixLine) + if (charMatchIdx < 0) { + console.log('@undefined4', charMatchIdx) + + console.error('Warning: Found character with negative index. This should never happen.') + return undefined + } + + const character = (charMatchIdx + + currentPrefixLine.length + - completionPrefixLine.length + ) + + const startIdx = getIndex(autocompletion.insertText, lineStart, character) + + return { + lineStart, + character, + startIdx, + } + + +} + + + + +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] ?? '' + + // 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 = true + const testMode = false + + if (disabled) { return []; } + + const docUriStr = model.uri.toString(); + + const { prefix, suffix } = getPrefixAndSuffix(model, position) + // 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) => { + if (autocompletion.requestId) + this._sendLLMMessageService.abort(autocompletion.requestId) + } + ) + } + this._lastPrefix = prefix + + // print all pending autocompletions + // let _numPending = 0 + // this._autocompletionsOfDocument[docUriStr].items.forEach((a: Autocompletion) => { if (a.status === 'pending') _numPending += 1 }) + // console.log('@numPending: ' + _numPending) + + // 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 (testMode && 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, + status: 'pending', + llmPromise: undefined, + insertText: '', + requestId: null, + } + + // set parameters of `newAutocompletion` appropriately + newAutocompletion.llmPromise = new Promise((resolve, reject) => { + + const requestId = this._sendLLMMessageService.sendLLMMessage({ + logging: { loggingName: 'Autocomplete' }, + messages: [], + options: { prefix, suffix, stopTokens, }, + onText: async ({ newText, fullText }) => { + + newAutocompletion.insertText = fullText + + // 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: ({ fullText }) => { + + // newAutocompletion.prefix = prefix + // newAutocompletion.suffix = suffix + // newAutocompletion.startTime = Date.now() + newAutocompletion.endTime = Date.now() + // newAutocompletion.abortRef = { current: () => { } } + newAutocompletion.status = 'finished' + // newAutocompletion.promise = undefined + newAutocompletion.insertText = postprocessResult(extractCodeFromResult(fullText)) + + resolve(newAutocompletion.insertText) + + }, + onError: ({ error }) => { + newAutocompletion.endTime = Date.now() + newAutocompletion.status = 'error' + reject(error) + }, + voidConfig: this._voidConfigStateService.state.voidConfig, + }) + newAutocompletion.requestId = requestId + + // 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, + @ISendLLMMessageService private readonly _sendLLMMessageService: ISendLLMMessageService, + @IEditorService private readonly _editorService: IEditorService, + @IModelService private readonly _modelService: IModelService, + ) { + 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) => { + + // get the `docUriStr` and the `position` of the cursor + const activePane = this._editorService.activeEditorPane; + if (!activePane) return; + const control = activePane.getControl(); + if (!control || !isCodeEditor(control)) return; + const position = control.getPosition(); + if (!position) return; + const resource = EditorResourceAccessor.getCanonicalUri(this._editorService.activeEditor); + if (!resource) return; + const model = this._modelService.getModel(resource) + if (!model) return; + const docUriStr = resource.toString(); + + const { prefix, } = getPrefixAndSuffix(model, position) + + if (!this._autocompletionsOfDocument[docUriStr]) return; + + // go through cached items and remove matching ones + // autocompletion.prefix + autocompletion.insertedText ~== insertedText + completions.items.forEach(item => { + this._autocompletionsOfDocument[docUriStr].items.forEach((autocompletion: Autocompletion) => { + if (removeLeftTabsAndTrimEnd(prefix) + === removeLeftTabsAndTrimEnd(autocompletion.prefix + autocompletion.insertText) + ) { + this._autocompletionsOfDocument[docUriStr].delete(autocompletion.id); + } + }); + }); + + }, + }) + + + } + + +} + + +registerSingleton(IAutocompleteService, AutocompleteService, InstantiationType.Eager); diff --git a/src/vs/workbench/contrib/void/browser/registerThreads.ts b/src/vs/workbench/contrib/void/browser/registerThreads.ts index 2d1f15d3..7911ca03 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 = { @@ -99,8 +100,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/contrib/void/browser/void.contribution.ts b/src/vs/workbench/contrib/void/browser/void.contribution.ts index 16be4b9c..b5491a52 100644 --- a/src/vs/workbench/contrib/void/browser/void.contribution.ts +++ b/src/vs/workbench/contrib/void/browser/void.contribution.ts @@ -18,5 +18,8 @@ import './registerSidebar.js' // register Thread History import './registerThreads.js' +// register Autocomplete +import './registerAutocomplete.js' + // register css import './media/void.css' 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:* *;"/>