From 5593ff5d793364e64b1f995f6567b98f344d5aaa Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Sat, 25 Jan 2025 03:11:47 -0800 Subject: [PATCH 01/12] add key --- src/vs/platform/void/common/voidSettingsTypes.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/platform/void/common/voidSettingsTypes.ts b/src/vs/platform/void/common/voidSettingsTypes.ts index fe358757..cfed791c 100644 --- a/src/vs/platform/void/common/voidSettingsTypes.ts +++ b/src/vs/platform/void/common/voidSettingsTypes.ts @@ -252,7 +252,7 @@ export const displayInfoOfSettingName = (providerName: ProviderName, settingName title: 'API Key', placeholder: providerName === 'anthropic' ? 'sk-ant-key...' : // sk-ant-api03-key providerName === 'openAI' ? 'sk-proj-key...' : - providerName === 'deepseek' ? 'sk-...' : + providerName === 'deepseek' ? 'sk-key...' : providerName === 'openRouter' ? 'sk-or-key...' : // sk-or-v1-key providerName === 'gemini' ? 'key...' : providerName === 'groq' ? 'gsk_key...' : From 21708fd7e022e5b1fdbee58d39312dc6c2d7e464 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Sat, 25 Jan 2025 03:14:31 -0800 Subject: [PATCH 02/12] contrib --- CONTRIBUTING.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c7ef8bae..c42e3446 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,6 +1,6 @@ # Contributing to Void -### Welcome! 👋 -This is the official guide on how to contribute to Void. We want to make it as easy as possible to contribute, so if you have any questions or comments, reach out via email or discord! +### Welcome! 👋 +This is the official guide on how to contribute to Void. We want to make it as easy as possible to contribute, so if you have any questions or comments, reach out via email or discord! There are a few ways to contribute: @@ -12,7 +12,7 @@ There are a few ways to contribute: ### Codebase Guide -We highly recommend reading [this](https://github.com/microsoft/vscode/wiki/Source-Code-Organization) article on VSCode's sourcecode organization. +We highly recommend reading [this](https://github.com/microsoft/vscode/wiki/Source-Code-Organization) article on VSCode's sourcecode organization. We are currently putting together our own articles on VSCode and Void's sourcecode organization. The best way to get this information right now is by attending a weekly meeting. @@ -87,7 +87,7 @@ Alternatively, if you want to build Void from the terminal, instead of pressing #### Common Fixes - Make sure you followed the prerequisite steps. -- Make sure you have the same NodeJS version as `.nvmrc`. +- Make sure you have Node version `20.16.0` (the version in `.nvmrc`)! - If you get `"TypeError: Failed to fetch dynamically imported module"`, make sure all imports end with `.js`. - If you see missing styles, wait a few seconds and then reload. - If you have any questions, feel free to [submit an issue](https://github.com/voideditor/void/issues/new). You can also refer to VSCode's complete [How to Contribute](https://github.com/microsoft/vscode/wiki/How-to-Contribute) page. @@ -123,7 +123,7 @@ workspace/ ``` ### Distributing -Void's maintainers distribute Void on our website and in releases. If you'd like to see the scripts to convert `Mac .app -> .dmg`, `Windows folder -> .exe`, and `Linux folder -> appimage` for distribution, feel free to reach out. +Void's maintainers distribute Void on our website and in releases. If you'd like to see the scripts to convert `Mac .app -> .dmg`, `Windows folder -> .exe`, and `Linux folder -> appimage` for distribution, feel free to reach out. ## Pull Request Guidelines From c7a6bddfbeb51c95ec725492ec4be9d1a71416f0 Mon Sep 17 00:00:00 2001 From: Mathew Pareles Date: Sun, 26 Jan 2025 13:04:16 -0800 Subject: [PATCH 03/12] groq --- src/vs/platform/void/common/voidSettingsTypes.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/vs/platform/void/common/voidSettingsTypes.ts b/src/vs/platform/void/common/voidSettingsTypes.ts index cfed791c..bb57eb7f 100644 --- a/src/vs/platform/void/common/voidSettingsTypes.ts +++ b/src/vs/platform/void/common/voidSettingsTypes.ts @@ -86,9 +86,10 @@ export const defaultDeepseekModels = modelInfoOfDefaultNames([ // https://console.groq.com/docs/models export const defaultGroqModels = modelInfoOfDefaultNames([ - "mixtral-8x7b-32768", - "llama2-70b-4096", - "gemma-7b-it" + "distil-whisper-large-v3-en", + "llama-3.3-70b-versatile", + "llama-3.1-8b-instant", + "gemma2-9b-it" ]) From 01b1358a018ec90c14f8882b8ee07b31546cf94e Mon Sep 17 00:00:00 2001 From: Mathew Pareles Date: Sun, 26 Jan 2025 19:00:57 -0800 Subject: [PATCH 04/12] autocomplete refactor --- .../void/browser/autocompleteService.ts | 1570 +++++++++-------- 1 file changed, 786 insertions(+), 784 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/autocompleteService.ts b/src/vs/workbench/contrib/void/browser/autocompleteService.ts index a7516be2..79dbc739 100644 --- a/src/vs/workbench/contrib/void/browser/autocompleteService.ts +++ b/src/vs/workbench/contrib/void/browser/autocompleteService.ts @@ -1,826 +1,828 @@ -// /*-------------------------------------------------------------------------------------- -// * Copyright 2025 Glass Devtools, Inc. All rights reserved. -// * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. -// *--------------------------------------------------------------------------------------*/ +/*-------------------------------------------------------------------------------------- + * Copyright 2025 Glass Devtools, Inc. All rights reserved. + * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. + *--------------------------------------------------------------------------------------*/ -// 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 { 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 { ILLMMessageService } from '../../../../platform/void/common/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'; -// import { extractCodeFromRegular } from './helpers/extractCodeFromResult.js'; +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 { 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 { ILLMMessageService } from '../../../../platform/void/common/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'; +import { extractCodeFromRegular } from './helpers/extractCodeFromResult.js'; +import { isWindows } from '../../../../base/common/platform.js'; -// // The extension this was called from is here - https://github.com/voideditor/void/blob/autocomplete/extensions/void/src/extension/extension.ts +// 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: +/* +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 +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) +-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 +-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) +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 +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 -// - -// */ +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; +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'); + 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; + 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 Autocompletion = { + id: number, + prefix: string, + suffix: string, + llmPrefix: string, + llmSuffix: string, + startTime: number, + endTime: number | undefined, + status: 'pending' | 'finished' | 'error', + type: 'single-line' | 'single-line-redo-suffix' | 'multi-line' + 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 ? ' ' : ''); + +} + + +// trims the end of the prefix to improve cache hit rate +const removeLeftTabsAndTrimEnds = (s: string): string => { + const trimmedString = s.trimEnd(); + const trailingEnd = s.slice(trimmedString.length); + + // keep only a single trailing newline + if (trailingEnd.includes(_ln)) { + s = trimmedString + _ln; + } + + s = s.replace(/^\s+/gm, ''); // remove left tabs + + return s; +} + + + +const removeAllWhitespace = (str: string): string => str.replace(/\s+/g, ''); + +function isSubsequence({ of, subsequence }: { of: string, subsequence: string }): boolean { + if (subsequence.length === 0) return true; + if (of.length === 0) return false; + + let subsequenceIndex = 0; + + for (let i = 0; i < of.length; i++) { + if (of[i] === subsequence[subsequenceIndex]) { + subsequenceIndex++; + } + if (subsequenceIndex === subsequence.length) { + return true; + } + } + + return false; +} + + +function getStringUpToUnbalancedClosingParenthesis(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 = `{}()[]<>\`'"` + +// further trim the autocompletion +const postprocessAutocompletion = ({ autocompletionMatchup, autocompletion, prefixAndSuffix }: { autocompletionMatchup: AutocompletionMatchupBounds, autocompletion: Autocompletion, prefixAndSuffix: PrefixAndSuffixInfo }) => { + + const { prefix, prefixToTheLeftOfCursor, suffixToTheRightOfCursor } = prefixAndSuffix + + const generatedMiddle = autocompletion.insertText + + let startIdx = autocompletionMatchup.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(new RegExp(`^${_ln}+`))?.[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(_ln)[0] ?? '' + // condition to complete as a single line completion + if ( + prefixToTheLeftOfCursor.trim() + && !suffixToTheRightOfCursor.trim() + && restOfLineToGenerate.trim() + ) { + + const rawNewlineIdx = generatedMiddle.slice(startIdx).indexOf(_ln) + 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 = getStringUpToUnbalancedClosingParenthesis(completionStr, prefix) + // console.log('originalCompletionStr: ', JSON.stringify(generatedMiddle.slice(startIdx))) + // console.log('finalCompletionStr: ', JSON.stringify(completionStr)) + + + return completionStr + +} + +// returns the text in the autocompletion to display, assuming the prefix is already matched +const toInlineCompletions = ({ autocompletionMatchup, autocompletion, prefixAndSuffix, position, debug }: { autocompletionMatchup: AutocompletionMatchupBounds, autocompletion: Autocompletion, prefixAndSuffix: PrefixAndSuffixInfo, position: Position, debug?: boolean }): { insertText: string, range: Range }[] => { + + // postprocess the insertText + let trimmedInsertText = postprocessAutocompletion({ autocompletionMatchup, autocompletion, prefixAndSuffix, }) + + // postprocess `rangeToReplace` + let rangeToReplace: Range = new Range(position.lineNumber, position.column, position.lineNumber, position.column) + if (autocompletion.type === 'single-line-redo-suffix' // did we redo the line? if so, replace the whole suffix + && isSubsequence({ // check that the old text contains the same brackets + symbols as the new text + subsequence: removeAllWhitespace(prefixAndSuffix.suffixToTheRightOfCursor), + of: removeAllWhitespace(autocompletion.insertText), // should not be `trimmedInsertText` + }) + ) { + rangeToReplace = new Range(position.lineNumber, position.column, position.lineNumber, Number.MAX_SAFE_INTEGER) + } else { // did not matchup, do not show the autocompletion + return [{ + insertText: '', + range: rangeToReplace + }] + } + + return [{ + insertText: trimmedInsertText, + 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 // } -// 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 Autocompletion = { -// id: number, -// prefix: string, -// suffix: string, -// llmPrefix: string, -// llmSuffix: string, -// startTime: number, -// endTime: number | undefined, -// status: 'pending' | 'finished' | 'error', -// type: 'single-line' | 'single-line-redo-suffix' | 'multi-line' -// 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 isMatch = (originalPrefixTrimmed + generatedMiddle).startsWith(currentPrefixTrimmed) +// return isMatch // } -// // trims the end of the prefix to improve cache hit rate -// const removeLeftTabsAndTrimEnds = (s: string): string => { -// const trimmedString = s.trimEnd(); -// const trailingEnd = s.slice(trimmedString.length); +const allLinebreakSymbols = ['\r\n', '\n'] +const _ln = isWindows ? allLinebreakSymbols[0] : allLinebreakSymbols[1] + +type PrefixAndSuffixInfo = { prefix: string, suffix: string, prefixLines: string[], suffixLines: string[], prefixToTheLeftOfCursor: string, suffixToTheRightOfCursor: string } +const getPrefixAndSuffixInfo = (model: ITextModel, position: Position): PrefixAndSuffixInfo => { -// // keep only a single trailing newline -// if (trailingEnd.includes('\n')) { -// s = trimmedString + '\n'; -// } + const fullText = model.getValue(); -// s = s.replace(/^\s+/gm, ''); // remove left tabs - -// return s; -// } + const cursorOffset = model.getOffsetAt(position) + const prefix = fullText.substring(0, cursorOffset) + const suffix = fullText.substring(cursorOffset) + const prefixLines = prefix.split(_ln) + const suffixLines = suffix.split(_ln) -// const removeAllWhitespace = (str: string): string => str.replace(/\s+/g, ''); - -// function isSubsequence({ of, subsequence }: { of: string, subsequence: string }): boolean { -// if (subsequence.length === 0) return true; -// if (of.length === 0) return false; - -// let subsequenceIndex = 0; - -// for (let i = 0; i < of.length; i++) { -// if (of[i] === subsequence[subsequenceIndex]) { -// subsequenceIndex++; -// } -// if (subsequenceIndex === subsequence.length) { -// return true; -// } -// } - -// return false; -// } - - -// 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 = `{}()[]<>\`'"` - -// // further trim the autocompletion -// const postprocessAutocompletion = ({ autocompletionMatchup, autocompletion, prefixAndSuffix }: { autocompletionMatchup: AutocompletionMatchupBounds, autocompletion: Autocompletion, prefixAndSuffix: PrefixAndSuffixInfo }) => { - -// const { prefix, prefixToTheLeftOfCursor, suffixToTheRightOfCursor } = prefixAndSuffix - -// const generatedMiddle = autocompletion.insertText - -// let startIdx = autocompletionMatchup.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)) - - -// return completionStr + const prefixToTheLeftOfCursor = prefixLines.slice(-1)[0] ?? '' + const suffixToTheRightOfCursor = suffixLines[0] ?? '' -// } - -// // returns the text in the autocompletion to display, assuming the prefix is already matched -// const toInlineCompletions = ({ autocompletionMatchup, autocompletion, prefixAndSuffix, position, debug }: { autocompletionMatchup: AutocompletionMatchupBounds, autocompletion: Autocompletion, prefixAndSuffix: PrefixAndSuffixInfo, position: Position, debug?: boolean }): { insertText: string, range: Range }[] => { - -// let trimmedInsertText = postprocessAutocompletion({ autocompletionMatchup, autocompletion, prefixAndSuffix, }) - - -// // set the range to replace -// let rangeToReplace: Range = new Range(position.lineNumber, position.column, position.lineNumber, position.column) - -// if (autocompletion.type === 'single-line-redo-suffix' // did we redo the line? if so, replace the whole suffix -// && isSubsequence({ // check that the old text contains the same brackets + symbols as the new text -// subsequence: removeAllWhitespace(prefixAndSuffix.suffixToTheRightOfCursor), -// of: removeAllWhitespace(autocompletion.insertText), // should not be `trimmedInsertText` -// }) -// ) { -// rangeToReplace = new Range(position.lineNumber, position.column, position.lineNumber, Number.MAX_SAFE_INTEGER) -// } - - -// return [{ -// insertText: trimmedInsertText, -// 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 - -// // } + return { prefix, suffix, prefixLines, suffixLines, prefixToTheLeftOfCursor, suffixToTheRightOfCursor } +} -// type PrefixAndSuffixInfo = { prefix: string, suffix: string, prefixLines: string[], suffixLines: string[], prefixToTheLeftOfCursor: string, suffixToTheRightOfCursor: string } -// const getPrefixAndSuffixInfo = (model: ITextModel, position: Position): PrefixAndSuffixInfo => { +const getIndex = (str: string, line: number, char: number) => { + return str.split(_ln).slice(0, line).join(_ln).length + (line > 0 ? 1 : 0) + char; +} +const getLastLine = (s: string): string => { + const matches = s.match(new RegExp(`[^${_ln}]*$`)) + return matches ? matches[0] : '' +} -// const fullText = model.getValue(); +type AutocompletionMatchupBounds = { + startLine: number, + startCharacter: 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 getAutocompletionMatchup = ({ prefix, autocompletion }: { prefix: string, autocompletion: Autocompletion }): AutocompletionMatchupBounds | undefined => { + + const trimmedCurrentPrefix = removeLeftTabsAndTrimEnds(prefix) + const trimmedCompletionPrefix = removeLeftTabsAndTrimEnds(autocompletion.prefix) + const trimmedCompletionMiddle = removeLeftTabsAndTrimEnds(autocompletion.insertText) -// const cursorOffset = model.getOffsetAt(position) -// const prefix = fullText.substring(0, cursorOffset) -// const suffix = fullText.substring(cursorOffset) + // 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)) -// const prefixLines = prefix.split('\n') -// const suffixLines = suffix.split('\n') + if (trimmedCurrentPrefix.length < trimmedCompletionPrefix.length) { // user must write text beyond the original prefix at generation time + console.log('@undefined1') + return undefined + } -// const prefixToTheLeftOfCursor = prefixLines.slice(-1)[0] ?? '' -// const suffixToTheRightOfCursor = suffixLines[0] ?? '' + if ( // check that completion starts with the prefix + !(trimmedCompletionPrefix + trimmedCompletionMiddle) + .startsWith(trimmedCurrentPrefix) + ) { + console.log('@undefined2') + return undefined + } -// return { prefix, suffix, prefixLines, suffixLines, prefixToTheLeftOfCursor, suffixToTheRightOfCursor } + // reverse map to find position wrt `autocompletion.result` + const lineStart = + trimmedCurrentPrefix.split(_ln).length - + trimmedCompletionPrefix.split(_ln).length; -// } + if (lineStart < 0) { + console.log('@undefined3') -// 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] : '' -// } + console.error('Error: No line found.'); + return undefined; + } + const currentPrefixLine = getLastLine(trimmedCurrentPrefix) + const completionPrefixLine = lineStart === 0 ? getLastLine(trimmedCompletionPrefix) : '' + const completionMiddleLine = autocompletion.insertText.split(_ln)[lineStart] + const fullCompletionLine = completionPrefixLine + completionMiddleLine -// type AutocompletionMatchupBounds = { -// startLine: number, -// startCharacter: 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 getAutocompletionMatchup = ({ prefix, autocompletion }: { prefix: string, autocompletion: Autocompletion }): AutocompletionMatchupBounds | undefined => { + // console.log('currentPrefixLine', currentPrefixLine) + // console.log('completionPrefixLine', completionPrefixLine) + // console.log('completionMiddleLine', completionMiddleLine) -// const trimmedCurrentPrefix = removeLeftTabsAndTrimEnds(prefix) -// const trimmedCompletionPrefix = removeLeftTabsAndTrimEnds(autocompletion.prefix) -// const trimmedCompletionMiddle = removeLeftTabsAndTrimEnds(autocompletion.insertText) + const charMatchIdx = fullCompletionLine.indexOf(currentPrefixLine) + if (charMatchIdx < 0) { + console.log('@undefined4', charMatchIdx) -// // 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)) + console.error('Warning: Found character with negative index. This should never happen.') + return undefined + } -// if (trimmedCurrentPrefix.length < trimmedCompletionPrefix.length) { // user must write text beyond the original prefix at generation time -// console.log('@undefined1') -// return undefined -// } + const character = (charMatchIdx + + currentPrefixLine.length + - completionPrefixLine.length + ) -// if ( // check that completion starts with the prefix -// !(trimmedCompletionPrefix + trimmedCompletionMiddle) -// .startsWith(trimmedCurrentPrefix) -// ) { -// console.log('@undefined2') -// return undefined -// } + const startIdx = getIndex(autocompletion.insertText, lineStart, character) -// // 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 { -// startLine: lineStart, -// startCharacter: character, -// startIdx, -// } - - -// } - - - - -// const getCompletionOptions = (prefixAndSuffix: PrefixAndSuffixInfo) => { - -// const { prefix, suffix, prefixToTheLeftOfCursor, suffixToTheRightOfCursor, suffixLines } = prefixAndSuffix - -// // single line prediction unless the current line is blank -// let predictionType: Autocompletion['type'] -// let llmPrefix = prefix -// let llmSuffix = suffix - -// if (!prefixToTheLeftOfCursor.trim() && !suffixToTheRightOfCursor.trim()) { // line is empty -// predictionType = 'multi-line' -// sto ptokens here -// } else if (removeAllWhitespace(prefixAndSuffix.suffixToTheRightOfCursor).length < 4) { // suffix is less than 4 characters -// predictionType = 'single-line-redo-suffix' -// llmSuffix = '\n' + suffixLines.slice(1).join('\n') // ignore suffixToTheRightOfCursor -// } else { -// predictionType = 'single-line' -// } - -// // default parameters -// let shouldGenerate = true -// let stopTokens: string[] = ['\n\n', '\r\n\r\n'] // default to multi-line prediction - -// // Case 1: User is on a line with text to the left or right -// if (prefixToTheLeftOfCursor.trim() !== '' || suffixToTheRightOfCursor.trim() !== '') { -// stopTokens = ['\n', '\r\n'] // single line prediction -// } - -// // Don't generate if at the very beginning of a line -// if (prefixToTheLeftOfCursor === '') { -// shouldGenerate = false -// } - -// return { shouldGenerate, predictionType, stopTokens, llmPrefix, llmSuffix } - -// } - - - - -// 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 prefixAndSuffix = getPrefixAndSuffixInfo(model, position) -// const { prefix, suffix } = prefixAndSuffix - -// // 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._llmMessageService.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 autocompletionMatchup: AutocompletionMatchupBounds | undefined = undefined -// for (const autocompletion of this._autocompletionsOfDocument[docUriStr].items.values()) { -// // if the user's change matches with the autocompletion -// autocompletionMatchup = getAutocompletionMatchup({ prefix, autocompletion }) -// if (autocompletionMatchup !== undefined) { -// cachedAutocompletion = autocompletion -// break; -// } -// } - -// // if there is a cached autocompletion, return it -// if (cachedAutocompletion && autocompletionMatchup) { - -// // console.log('id: ' + cachedAutocompletion.id) - -// if (cachedAutocompletion.status === 'finished') { -// // console.log('A1') - -// const inlineCompletions = toInlineCompletions({ autocompletionMatchup, autocompletion: cachedAutocompletion, prefixAndSuffix, position, debug: true }) -// return inlineCompletions - -// } else if (cachedAutocompletion.status === 'pending') { -// // console.log('A2') - -// try { -// await cachedAutocompletion.llmPromise; -// const inlineCompletions = toInlineCompletions({ autocompletionMatchup, autocompletion: cachedAutocompletion, prefixAndSuffix, 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, predictionType, stopTokens, llmPrefix, llmSuffix } = getCompletionOptions(prefixAndSuffix) // TODO use stop tokens - -// 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, // the actual prefix and suffix -// suffix: suffix, -// llmPrefix: llmPrefix, // the prefix and suffix the llm sees -// llmSuffix: llmSuffix, -// startTime: Date.now(), -// endTime: undefined, -// type: predictionType, -// status: 'pending', -// llmPromise: undefined, -// insertText: '', -// requestId: null, -// } - -// // set parameters of `newAutocompletion` appropriately -// newAutocompletion.llmPromise = new Promise((resolve, reject) => { - -// const requestId = this._llmMessageService.sendLLMMessage({ -// prefix: llmPrefix, -// suffix: llmSuffix, -// stopTokens:stopTokens, -// logging: { loggingName: 'Autocomplete' }, -// messages: [], -// onText: async ({ newText, fullText }) => { - -// newAutocompletion.insertText = fullText - -// // if generation doesn't match the prefix for the first few tokens generated, reject it -// if (!getAutocompletionMatchup({ 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 -// const [text, _] = extractCodeFromRegular({ text: fullText, recentlyAddedTextLen: 0 }) -// newAutocompletion.insertText = postprocessResult(text) - -// resolve(newAutocompletion.insertText) - -// }, -// onError: ({ message }) => { -// newAutocompletion.endTime = Date.now() -// newAutocompletion.status = 'error' -// reject(message) -// }, -// useProviderFor: 'Autocomplete', -// range: { startLineNumber: position.lineNumber, startColumn: position.column, endLineNumber: position.lineNumber, endColumn: position.column }, -// }) -// 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 autocompletionMatchup: AutocompletionMatchupBounds = { startIdx: 0, startLine: 0, startCharacter: 0 } -// const inlineCompletions = toInlineCompletions({ autocompletionMatchup, autocompletion: newAutocompletion, prefixAndSuffix, position }) -// return inlineCompletions - -// } catch (e) { -// this._autocompletionsOfDocument[docUriStr].delete(newAutocompletion.id) -// console.error('Error creating autocompletion (2): ' + e) -// return [] -// } - -// } - -// constructor( -// @ILanguageFeaturesService private _langFeatureService: ILanguageFeaturesService, -// @ILLMMessageService private readonly _llmMessageService: ILLMMessageService, -// @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(); -// if (!this._autocompletionsOfDocument[docUriStr]) return; - -// const { prefix, } = getPrefixAndSuffixInfo(model, position) - -// // 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 (removeLeftTabsAndTrimEnds(prefix) -// === removeLeftTabsAndTrimEnds(autocompletion.prefix + autocompletion.insertText) -// ) { -// this._autocompletionsOfDocument[docUriStr].delete(autocompletion.id); -// } -// }); -// }); - -// }, -// }) - - -// } - - -// } - - -// registerSingleton(IAutocompleteService, AutocompleteService, InstantiationType.Eager); + return { + startLine: lineStart, + startCharacter: character, + startIdx, + } + + +} + +const getCompletionOptions = (prefixAndSuffix: PrefixAndSuffixInfo) => { + + const { prefix, suffix, prefixToTheLeftOfCursor, suffixToTheRightOfCursor, suffixLines } = prefixAndSuffix + + // single line prediction unless the current line is blank + let predictionType: Autocompletion['type'] + let llmPrefix = prefix + let llmSuffix = suffix + + if (!prefixToTheLeftOfCursor.trim() && !suffixToTheRightOfCursor.trim()) { // line is empty + predictionType = 'multi-line' + + } else if (removeAllWhitespace(prefixAndSuffix.suffixToTheRightOfCursor).length < 4) { // suffix is less than 4 characters + predictionType = 'single-line-redo-suffix' + const suffixLinesIgnoringThisLine = suffixLines.slice(1) + llmSuffix = suffixLinesIgnoringThisLine.length === 0 ? '' : _ln + suffixLinesIgnoringThisLine.join(_ln) + + } else { + predictionType = 'single-line' + } + + // default parameters + let shouldGenerate = true + let stopTokens: string[] = allLinebreakSymbols // default to multi-line prediction + + // Case 1: User is on a line with text to the left or right + if (prefixToTheLeftOfCursor.trim() !== '' || suffixToTheRightOfCursor.trim() !== '') { + stopTokens = allLinebreakSymbols // single line prediction + } + + // Don't generate if at the very beginning of a line + if (prefixToTheLeftOfCursor === '') { + shouldGenerate = false + } + + return { shouldGenerate, predictionType, stopTokens, llmPrefix, llmSuffix } + +} + +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 prefixAndSuffix = getPrefixAndSuffixInfo(model, position) + const { prefix, suffix } = prefixAndSuffix + + // 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._llmMessageService.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 autocompletionMatchup: AutocompletionMatchupBounds | undefined = undefined + for (const autocompletion of this._autocompletionsOfDocument[docUriStr].items.values()) { + // if the user's change matches with the autocompletion + autocompletionMatchup = getAutocompletionMatchup({ prefix, autocompletion }) + if (autocompletionMatchup !== undefined) { + cachedAutocompletion = autocompletion + break; + } + } + + // if there is a cached autocompletion, return it + if (cachedAutocompletion && autocompletionMatchup) { + + // console.log('id: ' + cachedAutocompletion.id) + + if (cachedAutocompletion.status === 'finished') { + // console.log('A1') + + const inlineCompletions = toInlineCompletions({ autocompletionMatchup, autocompletion: cachedAutocompletion, prefixAndSuffix, position, debug: true }) + return inlineCompletions + + } else if (cachedAutocompletion.status === 'pending') { + // console.log('A2') + + try { + await cachedAutocompletion.llmPromise; + const inlineCompletions = toInlineCompletions({ autocompletionMatchup, autocompletion: cachedAutocompletion, prefixAndSuffix, 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, predictionType, stopTokens, llmPrefix, llmSuffix } = getCompletionOptions(prefixAndSuffix) // TODO use stop tokens + + 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, // the actual prefix and suffix + suffix: suffix, + llmPrefix: llmPrefix, // the prefix and suffix the llm sees + llmSuffix: llmSuffix, + startTime: Date.now(), + endTime: undefined, + type: predictionType, + status: 'pending', + llmPromise: undefined, + insertText: '', + requestId: null, + } + + // set parameters of `newAutocompletion` appropriately + newAutocompletion.llmPromise = new Promise((resolve, reject) => { + + const requestId = this._llmMessageService.sendLLMMessage({ + prefix: llmPrefix, + suffix: llmSuffix, + stopTokens: stopTokens, + logging: { loggingName: 'Autocomplete' }, + messages: [], + onText: async ({ newText, fullText }) => { + + newAutocompletion.insertText = fullText + + // if generation doesn't match the prefix for the first few tokens generated, reject it + if (!getAutocompletionMatchup({ 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 + const [text, _] = extractCodeFromRegular({ text: fullText, recentlyAddedTextLen: 0 }) + newAutocompletion.insertText = postprocessResult(text) + + resolve(newAutocompletion.insertText) + + }, + onError: ({ message }) => { + newAutocompletion.endTime = Date.now() + newAutocompletion.status = 'error' + reject(message) + }, + useProviderFor: 'Autocomplete', + range: { startLineNumber: position.lineNumber, startColumn: position.column, endLineNumber: position.lineNumber, endColumn: position.column }, + }) + 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 autocompletionMatchup: AutocompletionMatchupBounds = { startIdx: 0, startLine: 0, startCharacter: 0 } + const inlineCompletions = toInlineCompletions({ autocompletionMatchup, autocompletion: newAutocompletion, prefixAndSuffix, position }) + return inlineCompletions + + } catch (e) { + this._autocompletionsOfDocument[docUriStr].delete(newAutocompletion.id) + console.error('Error creating autocompletion (2): ' + e) + return [] + } + + } + + constructor( + @ILanguageFeaturesService private _langFeatureService: ILanguageFeaturesService, + @ILLMMessageService private readonly _llmMessageService: ILLMMessageService, + @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(); + if (!this._autocompletionsOfDocument[docUriStr]) return; + + const { prefix, } = getPrefixAndSuffixInfo(model, position) + + // 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 (removeLeftTabsAndTrimEnds(prefix) + === removeLeftTabsAndTrimEnds(autocompletion.prefix + autocompletion.insertText) + ) { + this._autocompletionsOfDocument[docUriStr].delete(autocompletion.id); + } + }); + }); + + }, + }) + } + + +} + + +registerSingleton(IAutocompleteService, AutocompleteService, InstantiationType.Eager); From e4d747d0a6cd88644ab00d34c72f8a0c7b3a6784 Mon Sep 17 00:00:00 2001 From: Mathew Pareles Date: Sun, 26 Jan 2025 23:49:16 -0800 Subject: [PATCH 05/12] andrew ollamaFIM progress --- remote/package.json | 1 + .../platform/void/common/llmMessageService.ts | 25 ++-- .../platform/void/common/llmMessageTypes.ts | 82 ++++++------ .../void/electron-main/llmMessage/ollama.ts | 10 +- .../llmMessage/sendLLMMessage.ts | 36 +++-- .../void/electron-main/llmMessageChannel.ts | 6 +- .../void/browser/autocompleteService.ts | 125 +++++++++++++++--- .../void/browser/inlineDiffsService.ts | 17 ++- .../contrib/void/browser/prompt/prompts.ts | 2 +- 9 files changed, 205 insertions(+), 99 deletions(-) diff --git a/remote/package.json b/remote/package.json index 98776776..cf913ad2 100644 --- a/remote/package.json +++ b/remote/package.json @@ -26,6 +26,7 @@ "cookie": "^0.4.0", "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.2", + "debounced": "1.0.2", "jschardet": "3.1.3", "kerberos": "2.1.1", "minimist": "^1.2.6", diff --git a/src/vs/platform/void/common/llmMessageService.ts b/src/vs/platform/void/common/llmMessageService.ts index 40855c8d..fcec27f9 100644 --- a/src/vs/platform/void/common/llmMessageService.ts +++ b/src/vs/platform/void/common/llmMessageService.ts @@ -3,7 +3,7 @@ * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. *--------------------------------------------------------------------------------------*/ -import { EventLLMMessageOnTextParams, EventLLMMessageOnErrorParams, EventLLMMessageOnFinalMessageParams, ServiceSendLLMMessageParams, MainLLMMessageParams, MainLLMMessageAbortParams, ServiceModelListParams, EventModelListOnSuccessParams, EventModelListOnErrorParams, MainModelListParams, OllamaModelResponse, OpenaiCompatibleModelResponse, } from './llmMessageTypes.js'; +import { EventLLMMessageOnTextParams, EventLLMMessageOnErrorParams, EventLLMMessageOnFinalMessageParams, ServiceSendLLMMessageParams, MainSendLLMMessageParams, MainLLMMessageAbortParams, ServiceModelListParams, EventModelListOnSuccessParams, EventModelListOnErrorParams, MainModelListParams, OllamaModelResponse, OpenaiCompatibleModelResponse, } from './llmMessageTypes.js'; import { IChannel } from '../../../base/parts/ipc/common/ipc.js'; import { IMainProcessService } from '../../ipc/common/mainProcessService.js'; import { InstantiationType, registerSingleton } from '../../instantiation/common/extensions.js'; @@ -96,31 +96,29 @@ export class LLMMessageService extends Disposable implements ILLMMessageService onError({ message: 'Please add a Provider in Settings!', fullError: null }) return null } + const { providerName, modelName } = modelSelection - // add ai instructions here because we don't have access to voidSettingsService on the other side of the proxy - const aiInstructions = this.voidSettingsService.state.globalSettings.aiInstructions - if (aiInstructions) - proxyParams.messages.unshift({ role: 'system', content: aiInstructions }) - // add state for request id - const requestId_ = generateUuid(); - this.onTextHooks_llm[requestId_] = onText - this.onFinalMessageHooks_llm[requestId_] = onFinalMessage - this.onErrorHooks_llm[requestId_] = onError + const requestId = generateUuid(); + this.onTextHooks_llm[requestId] = onText + this.onFinalMessageHooks_llm[requestId] = onFinalMessage + this.onErrorHooks_llm[requestId] = onError + const { aiInstructions } = this.voidSettingsService.state.globalSettings const { settingsOfProvider } = this.voidSettingsService.state // params will be stripped of all its functions over the IPC channel this.channel.call('sendLLMMessage', { ...proxyParams, - requestId: requestId_, + aiInstructions, + requestId, providerName, modelName, settingsOfProvider, - } satisfies MainLLMMessageParams); + } satisfies MainSendLLMMessageParams); - return requestId_ + return requestId } @@ -147,6 +145,7 @@ export class LLMMessageService extends Disposable implements ILLMMessageService } satisfies MainModelListParams) } + openAICompatibleList = (params: ServiceModelListParams) => { const { onSuccess, onError, ...proxyParams } = params diff --git a/src/vs/platform/void/common/llmMessageTypes.ts b/src/vs/platform/void/common/llmMessageTypes.ts index 77b31c07..296c0a56 100644 --- a/src/vs/platform/void/common/llmMessageTypes.ts +++ b/src/vs/platform/void/common/llmMessageTypes.ts @@ -3,7 +3,6 @@ * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. *--------------------------------------------------------------------------------------*/ -import { IRange } from '../../../editor/common/core/range' import { ProviderName, SettingsOfProvider } from './voidSettingsTypes.js' @@ -36,66 +35,67 @@ export type _InternalLLMMessage = { } -export type ServiceSendLLMFeatureParams = { - useProviderFor: 'Ctrl+K'; - range: IRange; -} | { - useProviderFor: 'Ctrl+L'; -} | { - useProviderFor: 'Autocomplete'; - range: IRange; -} - -// params to the true sendLLMMessage function -export type LLMMMessageParams = { - onText: OnText; - onFinalMessage: OnFinalMessage; - onError: OnError; - abortRef: AbortRef; - +type SendLLMType = { + type: 'sendLLMMessage'; messages: LLMMessage[]; - - logging: { - loggingName: string, - }; - providerName: ProviderName; - modelName: string; - settingsOfProvider: SettingsOfProvider; +} | { + type: 'ollamaFIM'; + messages: { + prefix: string; + suffix: string; + } } +// service types export type ServiceSendLLMMessageParams = { onText: OnText; onFinalMessage: OnFinalMessage; onError: OnError; + logging: { loggingName: string, }; + useProviderFor: 'Ctrl+K' | 'Ctrl+L' | 'Autocomplete'; +} & SendLLMType + +// params to the true sendLLMMessage function +export type SendLLMMMessageParams = { + onText: OnText; + onFinalMessage: OnFinalMessage; + onError: OnError; + logging: { loggingName: string, }; + abortRef: AbortRef; + + aiInstructions: string; + + providerName: ProviderName; + modelName: string; + settingsOfProvider: SettingsOfProvider; +} & SendLLMType - messages: LLMMessage[]; - logging: { - loggingName: string, - }; -} & ServiceSendLLMFeatureParams // can't send functions across a proxy, use listeners instead export type BlockedMainLLMMessageParams = 'onText' | 'onFinalMessage' | 'onError' | 'abortRef' +export type MainSendLLMMessageParams = Omit & { requestId: string } & SendLLMType -export type MainLLMMessageParams = Omit & { requestId: string } export type MainLLMMessageAbortParams = { requestId: string } export type EventLLMMessageOnTextParams = Parameters[0] & { requestId: string } export type EventLLMMessageOnFinalMessageParams = Parameters[0] & { requestId: string } export type EventLLMMessageOnErrorParams = Parameters[0] & { requestId: string } -export type _InternalSendLLMMessageFnType = (params: { - messages: _InternalLLMMessage[]; - onText: OnText; - onFinalMessage: OnFinalMessage; - onError: OnError; - settingsOfProvider: SettingsOfProvider; - providerName: ProviderName; - modelName: string; +export type _InternalSendLLMMessageFnType = ( + params: { + onText: OnText; + onFinalMessage: OnFinalMessage; + onError: OnError; + messages: _InternalLLMMessage[]; - _setAborter: (aborter: () => void) => void; -}) => void + settingsOfProvider: SettingsOfProvider; + providerName: ProviderName; + modelName: string; + + _setAborter: (aborter: () => void) => void; + } +) => void // service -> main -> internal -> event (back to main) // (browser) diff --git a/src/vs/platform/void/electron-main/llmMessage/ollama.ts b/src/vs/platform/void/electron-main/llmMessage/ollama.ts index 95792700..eff63514 100644 --- a/src/vs/platform/void/electron-main/llmMessage/ollama.ts +++ b/src/vs/platform/void/electron-main/llmMessage/ollama.ts @@ -25,6 +25,7 @@ export const ollamaList: _InternalModelListFnType = async ( const ollama = new Ollama({ host: thisConfig.endpoint }) ollama.list() .then((response) => { + console.log('MODELS!!!!!!!!!!!!!!!!!', response) const { models } = response onSuccess({ models }) }) @@ -38,6 +39,7 @@ export const ollamaList: _InternalModelListFnType = async ( } + // Ollama export const sendOllamaMsg: _InternalSendLLMMessageFnType = ({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter }) => { @@ -68,14 +70,6 @@ export const sendOllamaMsg: _InternalSendLLMMessageFnType = ({ messages, onText, }) // when error/fail .catch((error) => { - // if (typeof error === 'object') { - // const e = error.error as ErrorResponse['error'] - // if (e) { - // const name = error.name ?? 'Error' - // onError({ error: `${name}: ${e}` }) - // return; - // } - // } onError({ message: error + '', fullError: error }) }) diff --git a/src/vs/platform/void/electron-main/llmMessage/sendLLMMessage.ts b/src/vs/platform/void/electron-main/llmMessage/sendLLMMessage.ts index 1f88b26c..b0cdedec 100644 --- a/src/vs/platform/void/electron-main/llmMessage/sendLLMMessage.ts +++ b/src/vs/platform/void/electron-main/llmMessage/sendLLMMessage.ts @@ -3,7 +3,7 @@ * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. *--------------------------------------------------------------------------------------*/ -import { LLMMMessageParams, OnText, OnFinalMessage, OnError, LLMMessage, _InternalLLMMessage } from '../../common/llmMessageTypes.js'; +import { SendLLMMMessageParams, OnText, OnFinalMessage, OnError, LLMMessage, _InternalLLMMessage } from '../../common/llmMessageTypes.js'; import { IMetricsService } from '../../common/metricsService.js'; import { sendAnthropicMsg } from './anthropic.js'; @@ -48,6 +48,8 @@ const cleanMessages = (messages: LLMMessage[]): _InternalLLMMessage[] => { export const sendLLMMessage = ({ + type, + aiInstructions, messages: messages_, onText: onText_, onFinalMessage: onFinalMessage_, @@ -57,21 +59,31 @@ export const sendLLMMessage = ({ settingsOfProvider, providerName, modelName, -}: LLMMMessageParams, +}: SendLLMMMessageParams, metricsService: IMetricsService ) => { - const messages = cleanMessages(messages_) + messages.unshift({ role: 'system', content: aiInstructions }) + + const messages = type === 'sendLLMMessage' ? cleanMessages(messages_) : [] + + + const prefixAndSuffix = type === 'ollamaFIM' ? messages_ : null // only captures number of messages and message "shape", no actual code, instructions, prompts, etc - const captureChatEvent = (eventId: string, extras?: object) => { + const captureLLMEvent = (eventId: string, extras?: object) => { metricsService.capture(eventId, { providerName, modelName, - numMessages: messages?.length, - messagesShape: messages?.map(msg => ({ role: msg.role, length: msg.content.length })), - origNumMessages: messages_?.length, - origMessagesShape: messages_?.map(msg => ({ role: msg.role, length: msg.content.length })), + ...type === 'sendLLMMessage' ? { + numMessages: messages?.length, + messagesShape: messages?.map(msg => ({ role: msg.role, length: msg.content.length })), + origNumMessages: messages_?.length, + origMessagesShape: messages_?.map(msg => ({ role: msg.role, length: msg.content.length })), + + } : type === 'ollamaFIM' ? { + + } : {}, ...extras, }) @@ -91,26 +103,26 @@ export const sendLLMMessage = ({ const onFinalMessage: OnFinalMessage = ({ fullText }) => { if (_didAbort) return - captureChatEvent(`${loggingName} - Received Full Message`, { messageLength: fullText.length, duration: new Date().getMilliseconds() - submit_time.getMilliseconds() }) + captureLLMEvent(`${loggingName} - Received Full Message`, { messageLength: fullText.length, duration: new Date().getMilliseconds() - submit_time.getMilliseconds() }) onFinalMessage_({ fullText }) } const onError: OnError = ({ message: error, fullError }) => { if (_didAbort) return console.error('sendLLMMessage onError:', error) - captureChatEvent(`${loggingName} - Error`, { error }) + captureLLMEvent(`${loggingName} - Error`, { error }) onError_({ message: error, fullError }) } const onAbort = () => { - captureChatEvent(`${loggingName} - Abort`, { messageLengthSoFar: _fullTextSoFar.length }) + captureLLMEvent(`${loggingName} - Abort`, { messageLengthSoFar: _fullTextSoFar.length }) try { _aborter?.() } // aborter sometimes automatically throws an error catch (e) { } _didAbort = true } abortRef_.current = onAbort - captureChatEvent(`${loggingName} - Sending Message`, { messageLength: messages[messages.length - 1]?.content.length }) + captureLLMEvent(`${loggingName} - Sending Message`, { messageLength: messages[messages.length - 1]?.content.length }) try { switch (providerName) { diff --git a/src/vs/platform/void/electron-main/llmMessageChannel.ts b/src/vs/platform/void/electron-main/llmMessageChannel.ts index 2430fce2..56235501 100644 --- a/src/vs/platform/void/electron-main/llmMessageChannel.ts +++ b/src/vs/platform/void/electron-main/llmMessageChannel.ts @@ -8,7 +8,7 @@ import { IServerChannel } from '../../../base/parts/ipc/common/ipc.js'; import { Emitter, Event } from '../../../base/common/event.js'; -import { EventLLMMessageOnTextParams, EventLLMMessageOnErrorParams, EventLLMMessageOnFinalMessageParams, MainLLMMessageParams, AbortRef, LLMMMessageParams, MainLLMMessageAbortParams, MainModelListParams, ModelListParams, EventModelListOnSuccessParams, EventModelListOnErrorParams, OllamaModelResponse, OpenaiCompatibleModelResponse, } from '../common/llmMessageTypes.js'; +import { EventLLMMessageOnTextParams, EventLLMMessageOnErrorParams, EventLLMMessageOnFinalMessageParams, MainSendLLMMessageParams, AbortRef, SendLLMMMessageParams, MainLLMMessageAbortParams, MainModelListParams, ModelListParams, EventModelListOnSuccessParams, EventModelListOnErrorParams, OllamaModelResponse, OpenaiCompatibleModelResponse, } from '../common/llmMessageTypes.js'; import { sendLLMMessage } from './llmMessage/sendLLMMessage.js' import { IMetricsService } from '../common/metricsService.js'; import { ollamaList } from './llmMessage/ollama.js'; @@ -91,13 +91,13 @@ export class LLMMessageChannel implements IServerChannel { } // the only place sendLLMMessage is actually called - private async _callSendLLMMessage(params: MainLLMMessageParams) { + private async _callSendLLMMessage(params: MainSendLLMMessageParams) { const { requestId } = params; if (!(requestId in this._abortRefOfRequestId_llm)) this._abortRefOfRequestId_llm[requestId] = { current: null } - const mainThreadParams: LLMMMessageParams = { + const mainThreadParams: SendLLMMMessageParams = { ...params, onText: ({ newText, fullText }) => { this._onText_llm.fire({ requestId, newText, fullText }); }, onFinalMessage: ({ fullText }) => { this._onFinalMessage_llm.fire({ requestId, fullText }); }, diff --git a/src/vs/workbench/contrib/void/browser/autocompleteService.ts b/src/vs/workbench/contrib/void/browser/autocompleteService.ts index 79dbc739..63c2e854 100644 --- a/src/vs/workbench/contrib/void/browser/autocompleteService.ts +++ b/src/vs/workbench/contrib/void/browser/autocompleteService.ts @@ -5,11 +5,10 @@ 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 { ITextModel } from '../../../../editor/common/model.js'; import { Position } from '../../../../editor/common/core/position.js'; -import { InlineCompletion, InlineCompletionContext } from '../../../../editor/common/languages.js'; +import { InlineCompletion, InlineCompletionContext, LocationLink } from '../../../../editor/common/languages.js'; import { CancellationToken } from '../../../../base/common/cancellation.js'; import { Range } from '../../../../editor/common/core/range.js'; import { ILLMMessageService } from '../../../../platform/void/common/llmMessageService.js'; @@ -19,6 +18,7 @@ import { EditorResourceAccessor } from '../../../common/editor.js'; import { IModelService } from '../../../../editor/common/services/model.js'; import { extractCodeFromRegular } from './helpers/extractCodeFromResult.js'; import { isWindows } from '../../../../base/common/platform.js'; +import { registerWorkbenchContribution2, WorkbenchPhase } from '../../../common/contributions.js'; // The extension this was called from is here - https://github.com/voideditor/void/blob/autocomplete/extensions/void/src/extension/extension.ts @@ -499,7 +499,7 @@ const getAutocompletionMatchup = ({ prefix, autocompletion }: { prefix: string, } -const getCompletionOptions = (prefixAndSuffix: PrefixAndSuffixInfo) => { +const getCompletionOptions = (prefixAndSuffix: PrefixAndSuffixInfo, relevantContext: string) => { const { prefix, suffix, prefixToTheLeftOfCursor, suffixToTheRightOfCursor, suffixLines } = prefixAndSuffix @@ -520,6 +520,8 @@ const getCompletionOptions = (prefixAndSuffix: PrefixAndSuffixInfo) => { predictionType = 'single-line' } + llmPrefix = llmPrefix + '\n\n/* Relevant context:\n' + relevantContext + '\n*/\n' + // default parameters let shouldGenerate = true let stopTokens: string[] = allLinebreakSymbols // default to multi-line prediction @@ -545,6 +547,9 @@ export interface IAutocompleteService { export const IAutocompleteService = createDecorator('AutocompleteService'); export class AutocompleteService extends Disposable implements IAutocompleteService { + + static readonly ID = 'void.autocompleteService' + _serviceBrand: undefined; private _autocompletionId: number = 0; @@ -562,11 +567,8 @@ export class AutocompleteService extends Disposable implements IAutocompleteServ token: CancellationToken, ): Promise { - const disabled = true const testMode = false - if (disabled) return []; - const docUriStr = model.uri.toString(); const prefixAndSuffix = getPrefixAndSuffixInfo(model, position) @@ -670,7 +672,15 @@ export class AutocompleteService extends Disposable implements IAutocompleteServ } } - const { shouldGenerate, predictionType, stopTokens, llmPrefix, llmSuffix } = getCompletionOptions(prefixAndSuffix) // TODO use stop tokens + + // NEW: gather relevant context from the code around the user's selection and definitions + const relevantContext = await this._gatherRelevantContextForPosition( + model, + position, + 3, //recursion depth + 1 // number of lines to view in each recursion + ); + const { shouldGenerate, predictionType, llmPrefix, llmSuffix } = getCompletionOptions(prefixAndSuffix, relevantContext) // TODO use stop tokens if (!shouldGenerate) return [] @@ -700,12 +710,14 @@ export class AutocompleteService extends Disposable implements IAutocompleteServ newAutocompletion.llmPromise = new Promise((resolve, reject) => { const requestId = this._llmMessageService.sendLLMMessage({ - prefix: llmPrefix, - suffix: llmSuffix, - stopTokens: stopTokens, + type: 'ollamaFIM', + // TODO: Incorporate relevant context directly into the prefix + messages: { + prefix: llmPrefix, + suffix: llmSuffix, + }, logging: { loggingName: 'Autocomplete' }, - messages: [], - onText: async ({ newText, fullText }) => { + onText: async ({ fullText }) => { newAutocompletion.insertText = fullText @@ -735,7 +747,6 @@ export class AutocompleteService extends Disposable implements IAutocompleteServ reject(message) }, useProviderFor: 'Autocomplete', - range: { startLineNumber: position.lineNumber, startColumn: position.column, endLineNumber: position.lineNumber, endColumn: position.column }, }) newAutocompletion.requestId = requestId @@ -770,6 +781,84 @@ export class AutocompleteService extends Disposable implements IAutocompleteServ } + // helper method to gather ~N lines above and below the user's current line, + // and recursively gather lines around any symbol definitions encountered. + private async _gatherRelevantContextForPosition( + model: ITextModel, + position: Position, + recursionDepth: number, + linesAround: number + ): Promise { + // We'll do a BFS-like approach: for each position or definition, gather lines around it, + // then attempt to find the definition of any symbols in that range, up to 'recursionDepth' times. + + // A set of "key" strings to avoid repeating the same location or line chunk + const visitedRanges = new Set(); + const collectedSnippets: string[] = []; + + // A queue of tasks, each being a tuple of: (model, position, depth) + const tasks: Array<{ model: ITextModel, position: Position, depth: number }> = []; + tasks.push({ model, position, depth: recursionDepth }); + + const getSnippetAroundLine = (model: ITextModel, lineNumber: number, linesAround: number): string => { + const startLine = Math.max(1, lineNumber - linesAround); + const endLine = Math.min(model.getLineCount(), lineNumber + linesAround); + const lines: string[] = []; + for (let i = startLine; i <= endLine; i++) { + lines.push(model.getLineContent(i)); + } + return lines.join('\n'); + }; + + while (tasks.length > 0) { + const { model: currentModel, position: currentPos, depth } = tasks.shift()!; + + if (depth < 0) { + continue; + } + + // Gather snippet around the current line + const snippet = getSnippetAroundLine(currentModel, currentPos.lineNumber, linesAround); + const snippetKey = `${currentModel.uri.toString()}:${currentPos.lineNumber}`; + if (!visitedRanges.has(snippetKey)) { + visitedRanges.add(snippetKey); + collectedSnippets.push(`-- Snippet around line ${currentPos.lineNumber} --\n${snippet}\n`); + } + + // Attempt to gather definitions for the symbol at this position + // We just pick all definition providers and see if any has a definition + const providers = this._langFeatureService.definitionProvider.ordered(currentModel); + for (const provider of providers) { + try { + const definitions = await provider.provideDefinition(currentModel, currentPos, CancellationToken.None); + if (!definitions) continue; + + // definitions can be a single LocationLink or an array + const defArray: LocationLink[] = Array.isArray(definitions) ? definitions : [definitions]; + for (const def of defArray) { + if (!def.uri) continue; + if (typeof def.range === 'undefined') continue; + const definitionModel = this._modelService.getModel(def.uri); + if (!definitionModel) continue; + + // We'll queue up a new task for that definition range + const defPos = new Position(def.range.startLineNumber, def.range.startColumn); + const defKey = `${def.uri.toString()}:${defPos.lineNumber}`; + if (!visitedRanges.has(defKey)) { + tasks.push({ model: definitionModel, position: defPos, depth: depth - 1 }); + } + } + } catch (err) { + // If a provider fails, ignore + } + } + } + + // Return the joined context + return collectedSnippets.join('\n'); + } + + constructor( @ILanguageFeaturesService private _langFeatureService: ILanguageFeaturesService, @ILLMMessageService private readonly _llmMessageService: ILLMMessageService, @@ -780,12 +869,14 @@ export class AutocompleteService extends Disposable implements IAutocompleteServ this._langFeatureService.inlineCompletionsProvider.register('*', { provideInlineCompletions: async (model, position, context, token) => { + console.log('AAAAAAAAA') const items = await this._provideInlineCompletionItems(model, position, context, token) // console.log('item: ', items?.[0]?.insertText) return { items: items, } }, freeInlineCompletions: (completions) => { + console.log('BBBBBBBB') // get the `docUriStr` and the `position` of the cursor const activePane = this._editorService.activeEditorPane; @@ -807,9 +898,7 @@ export class AutocompleteService extends Disposable implements IAutocompleteServ // autocompletion.prefix + autocompletion.insertedText ~== insertedText completions.items.forEach(item => { this._autocompletionsOfDocument[docUriStr].items.forEach((autocompletion: Autocompletion) => { - if (removeLeftTabsAndTrimEnds(prefix) - === removeLeftTabsAndTrimEnds(autocompletion.prefix + autocompletion.insertText) - ) { + if (removeLeftTabsAndTrimEnds(prefix) === removeLeftTabsAndTrimEnds(autocompletion.prefix + autocompletion.insertText)) { this._autocompletionsOfDocument[docUriStr].delete(autocompletion.id); } }); @@ -822,7 +911,7 @@ export class AutocompleteService extends Disposable implements IAutocompleteServ } - -registerSingleton(IAutocompleteService, AutocompleteService, InstantiationType.Eager); +registerWorkbenchContribution2(AutocompleteService.ID, AutocompleteService, WorkbenchPhase.BlockRestore); + diff --git a/src/vs/workbench/contrib/void/browser/inlineDiffsService.ts b/src/vs/workbench/contrib/void/browser/inlineDiffsService.ts index fdbf84bb..b7d43a6d 100644 --- a/src/vs/workbench/contrib/void/browser/inlineDiffsService.ts +++ b/src/vs/workbench/contrib/void/browser/inlineDiffsService.ts @@ -25,7 +25,7 @@ import * as dom from '../../../../base/browser/dom.js'; import { Widget } from '../../../../base/browser/ui/widget.js'; import { URI } from '../../../../base/common/uri.js'; import { IConsistentEditorItemService, IConsistentItemService } from './helperServices/consistentItemService.js'; -import { ctrlKStream_prefixAndSuffix, ctrlKStream_userMessage, ctrlKStream_systemMessage, fastApply_userMessage, fastApply_systemMessage, defaultFimTags } from './prompt/prompts.js'; +import { voidPrefixAndSuffix, ctrlKStream_userMessage, ctrlKStream_systemMessage, fastApply_userMessage, fastApply_systemMessage, defaultFimTags } from './prompt/prompts.js'; import { ILLMMessageService } from '../../../../platform/void/common/llmMessageService.js'; import { mountCtrlK } from '../browser/react/out/quick-edit-tsx/index.js' @@ -1304,13 +1304,24 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService { const instructions = _mountInfo?.textAreaRef.current?.value ?? '' // __TODO__ use Ollama's FIM api, if (isOllamaFIM) {...} else: - const { prefix, suffix } = ctrlKStream_prefixAndSuffix({ fullFileStr: currentFileStr, startLine, endLine }) + const { prefix, suffix } = voidPrefixAndSuffix({ fullFileStr: currentFileStr, startLine, endLine }) + // if (isOllamaFIM) { + // messages = { + // type: 'ollamaFIM', + // prefix, + // suffix, + // } + + // } + // else { const language = filenameToVscodeLanguage(uri.fsPath) ?? '' const userContent = ctrlKStream_userMessage({ selection: originalCode, instructions: instructions, prefix, suffix, isOllamaFIM: false, fimTags: modelFimTags, language }) + // type: 'messages', messages = [ { role: 'system', content: ctrlKStream_systemMessage({ fimTags: modelFimTags }), }, { role: 'user', content: userContent, } ] + // } } else { throw new Error(`featureName ${featureName} is invalid`) } @@ -1356,6 +1367,7 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService { let prevIgnoredSuffix = '' streamRequestIdRef.current = this._llmMessageService.sendLLMMessage({ + type: 'sendLLMMessage', useProviderFor: featureName, logging: { loggingName: `startApplying - ${featureName}` }, messages, @@ -1400,7 +1412,6 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService { onDone(true) }, - range: { startLineNumber: startLine, endLineNumber: endLine, startColumn: 1, endColumn: Number.MAX_SAFE_INTEGER }, }) return diffZone diff --git a/src/vs/workbench/contrib/void/browser/prompt/prompts.ts b/src/vs/workbench/contrib/void/browser/prompt/prompts.ts index 949e0604..7dc23d53 100644 --- a/src/vs/workbench/contrib/void/browser/prompt/prompts.ts +++ b/src/vs/workbench/contrib/void/browser/prompt/prompts.ts @@ -220,7 +220,7 @@ Please finish writing the new file by applying the change to the original file. -export const ctrlKStream_prefixAndSuffix = ({ fullFileStr, startLine, endLine }: { fullFileStr: string, startLine: number, endLine: number }) => { +export const voidPrefixAndSuffix = ({ fullFileStr, startLine, endLine }: { fullFileStr: string, startLine: number, endLine: number }) => { const fullFileLines = fullFileStr.split('\n') From e4bb15ef640308a28a7fa93b480a870ca81ce01d Mon Sep 17 00:00:00 2001 From: Mathew Pareles Date: Mon, 27 Jan 2025 22:20:19 -0800 Subject: [PATCH 06/12] autocomplete draft --- .../platform/void/common/llmMessageTypes.ts | 36 +++- .../void/electron-main/llmMessage/ollama.ts | 42 +++- .../llmMessage/sendLLMMessage.ts | 32 +-- .../void/electron-main/llmMessageChannel.ts | 4 +- .../void/browser/autocompleteService.ts | 188 ++++++++++++------ .../contrib/void/browser/chatThreadService.ts | 1 + 6 files changed, 217 insertions(+), 86 deletions(-) diff --git a/src/vs/platform/void/common/llmMessageTypes.ts b/src/vs/platform/void/common/llmMessageTypes.ts index 296c0a56..9b5f21a9 100644 --- a/src/vs/platform/void/common/llmMessageTypes.ts +++ b/src/vs/platform/void/common/llmMessageTypes.ts @@ -34,16 +34,18 @@ export type _InternalLLMMessage = { content: string; } +type _InternalOllamaFIMMessages = { + prefix: string; + suffix: string; + stopTokens: string[]; +} type SendLLMType = { type: 'sendLLMMessage'; messages: LLMMessage[]; } | { type: 'ollamaFIM'; - messages: { - prefix: string; - suffix: string; - } + messages: _InternalOllamaFIMMessages; } // service types @@ -56,7 +58,7 @@ export type ServiceSendLLMMessageParams = { } & SendLLMType // params to the true sendLLMMessage function -export type SendLLMMMessageParams = { +export type SendLLMMessageParams = { onText: OnText; onFinalMessage: OnFinalMessage; onError: OnError; @@ -74,7 +76,7 @@ export type SendLLMMMessageParams = { // can't send functions across a proxy, use listeners instead export type BlockedMainLLMMessageParams = 'onText' | 'onFinalMessage' | 'onError' | 'abortRef' -export type MainSendLLMMessageParams = Omit & { requestId: string } & SendLLMType +export type MainSendLLMMessageParams = Omit & { requestId: string } & SendLLMType export type MainLLMMessageAbortParams = { requestId: string } @@ -82,18 +84,32 @@ export type EventLLMMessageOnTextParams = Parameters[0] & { requestId: s export type EventLLMMessageOnFinalMessageParams = Parameters[0] & { requestId: string } export type EventLLMMessageOnErrorParams = Parameters[0] & { requestId: string } + export type _InternalSendLLMMessageFnType = ( params: { onText: OnText; onFinalMessage: OnFinalMessage; onError: OnError; - messages: _InternalLLMMessage[]; - - settingsOfProvider: SettingsOfProvider; providerName: ProviderName; + settingsOfProvider: SettingsOfProvider; modelName: string; - _setAborter: (aborter: () => void) => void; + + messages: _InternalLLMMessage[]; + } +) => void + +export type _InternalOllamaFIMMessageFnType = ( + params: { + onText: OnText; + onFinalMessage: OnFinalMessage; + onError: OnError; + providerName: ProviderName; + settingsOfProvider: SettingsOfProvider; + modelName: string; + _setAborter: (aborter: () => void) => void; + + messages: _InternalOllamaFIMMessages; } ) => void diff --git a/src/vs/platform/void/electron-main/llmMessage/ollama.ts b/src/vs/platform/void/electron-main/llmMessage/ollama.ts index eff63514..e5753973 100644 --- a/src/vs/platform/void/electron-main/llmMessage/ollama.ts +++ b/src/vs/platform/void/electron-main/llmMessage/ollama.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------*/ import { Ollama } from 'ollama'; -import { _InternalModelListFnType, _InternalSendLLMMessageFnType, OllamaModelResponse } from '../../common/llmMessageTypes.js'; +import { _InternalModelListFnType, _InternalOllamaFIMMessageFnType, _InternalSendLLMMessageFnType, OllamaModelResponse } from '../../common/llmMessageTypes.js'; import { defaultProviderSettings } from '../../common/voidSettingsTypes.js'; export const ollamaList: _InternalModelListFnType = async ({ onSuccess: onSuccess_, onError: onError_, settingsOfProvider }) => { @@ -25,7 +25,7 @@ export const ollamaList: _InternalModelListFnType = async ( const ollama = new Ollama({ host: thisConfig.endpoint }) ollama.list() .then((response) => { - console.log('MODELS!!!!!!!!!!!!!!!!!', response) + // console.log('MODELS!!!!!!!!!!!!!!!!!', response) const { models } = response onSuccess({ models }) }) @@ -39,6 +39,44 @@ export const ollamaList: _InternalModelListFnType = async ( } +export const sendOllamaFIM: _InternalOllamaFIMMessageFnType = ({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter }) => { + + const thisConfig = settingsOfProvider.ollama + // if endpoint is empty, normally ollama will send to 11434, but we want it to fail - the user should type it in + if (!thisConfig.endpoint) throw new Error(`Ollama Endpoint was empty (please enter ${defaultProviderSettings.ollama.endpoint} if you want the default).`) + + let fullText = '' + + const ollama = new Ollama({ host: thisConfig.endpoint }) + + ollama.generate({ + model: modelName, + prompt: messages.prefix, + suffix: messages.suffix, + options: { + stop: messages.stopTokens, + }, + raw: true, + stream: true, + // options: { num_predict: parseMaxTokensStr(thisConfig.maxTokens) } // this is max_tokens + }) + .then(async stream => { + _setAborter(() => stream.abort()) + // iterate through the stream + for await (const chunk of stream) { + const newText = chunk.response; + fullText += newText; + onText({ newText, fullText }); + } + onFinalMessage({ fullText }); + console.log('!!!!! OLLAMA RESULT', JSON.stringify(fullText)) + }) + // when error/fail + .catch((error) => { + onError({ message: error + '', fullError: error }) + }) +}; + // Ollama export const sendOllamaMsg: _InternalSendLLMMessageFnType = ({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter }) => { diff --git a/src/vs/platform/void/electron-main/llmMessage/sendLLMMessage.ts b/src/vs/platform/void/electron-main/llmMessage/sendLLMMessage.ts index b0cdedec..4b2884ca 100644 --- a/src/vs/platform/void/electron-main/llmMessage/sendLLMMessage.ts +++ b/src/vs/platform/void/electron-main/llmMessage/sendLLMMessage.ts @@ -3,11 +3,11 @@ * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. *--------------------------------------------------------------------------------------*/ -import { SendLLMMMessageParams, OnText, OnFinalMessage, OnError, LLMMessage, _InternalLLMMessage } from '../../common/llmMessageTypes.js'; +import { SendLLMMessageParams, OnText, OnFinalMessage, OnError, LLMMessage, _InternalLLMMessage } from '../../common/llmMessageTypes.js'; import { IMetricsService } from '../../common/metricsService.js'; import { sendAnthropicMsg } from './anthropic.js'; -import { sendOllamaMsg } from './ollama.js'; +import { sendOllamaFIM, sendOllamaMsg } from './ollama.js'; import { sendOpenAIMsg } from './openai.js'; import { sendGeminiMsg } from './gemini.js'; import { sendGroqMsg } from './groq.js'; @@ -59,16 +59,13 @@ export const sendLLMMessage = ({ settingsOfProvider, providerName, modelName, -}: SendLLMMMessageParams, +}: SendLLMMessageParams, metricsService: IMetricsService ) => { - messages.unshift({ role: 'system', content: aiInstructions }) + // messages.unshift({ role: 'system', content: aiInstructions }) - const messages = type === 'sendLLMMessage' ? cleanMessages(messages_) : [] - - - const prefixAndSuffix = type === 'ollamaFIM' ? messages_ : null + const messagesArr = type === 'sendLLMMessage' ? cleanMessages(messages_) : [] // only captures number of messages and message "shape", no actual code, instructions, prompts, etc const captureLLMEvent = (eventId: string, extras?: object) => { @@ -76,8 +73,8 @@ export const sendLLMMessage = ({ providerName, modelName, ...type === 'sendLLMMessage' ? { - numMessages: messages?.length, - messagesShape: messages?.map(msg => ({ role: msg.role, length: msg.content.length })), + numMessages: messagesArr?.length, + messagesShape: messagesArr?.map(msg => ({ role: msg.role, length: msg.content.length })), origNumMessages: messages_?.length, origMessagesShape: messages_?.map(msg => ({ role: msg.role, length: msg.content.length })), @@ -122,27 +119,30 @@ export const sendLLMMessage = ({ } abortRef_.current = onAbort - captureLLMEvent(`${loggingName} - Sending Message`, { messageLength: messages[messages.length - 1]?.content.length }) + captureLLMEvent(`${loggingName} - Sending Message`, { messageLength: messagesArr[messagesArr.length - 1]?.content.length }) try { switch (providerName) { case 'anthropic': - sendAnthropicMsg({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName }); + sendAnthropicMsg({ messages: messagesArr, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName }); break; case 'openAI': case 'openRouter': case 'deepseek': case 'openAICompatible': - sendOpenAIMsg({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName }); + sendOpenAIMsg({ messages: messagesArr, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName }); break; case 'gemini': - sendGeminiMsg({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName }); + sendGeminiMsg({ messages: messagesArr, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName }); break; case 'ollama': - sendOllamaMsg({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName }); + if (type === 'ollamaFIM') + sendOllamaFIM({ messages: messages_, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName }) + else + sendOllamaMsg({ messages: messagesArr, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName }); break; case 'groq': - sendGroqMsg({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName }); + sendGroqMsg({ messages: messagesArr, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName }); break; default: onError({ message: `Error: Void provider was "${providerName}", which is not recognized.`, fullError: null }) diff --git a/src/vs/platform/void/electron-main/llmMessageChannel.ts b/src/vs/platform/void/electron-main/llmMessageChannel.ts index 56235501..2c44e2ec 100644 --- a/src/vs/platform/void/electron-main/llmMessageChannel.ts +++ b/src/vs/platform/void/electron-main/llmMessageChannel.ts @@ -8,7 +8,7 @@ import { IServerChannel } from '../../../base/parts/ipc/common/ipc.js'; import { Emitter, Event } from '../../../base/common/event.js'; -import { EventLLMMessageOnTextParams, EventLLMMessageOnErrorParams, EventLLMMessageOnFinalMessageParams, MainSendLLMMessageParams, AbortRef, SendLLMMMessageParams, MainLLMMessageAbortParams, MainModelListParams, ModelListParams, EventModelListOnSuccessParams, EventModelListOnErrorParams, OllamaModelResponse, OpenaiCompatibleModelResponse, } from '../common/llmMessageTypes.js'; +import { EventLLMMessageOnTextParams, EventLLMMessageOnErrorParams, EventLLMMessageOnFinalMessageParams, MainSendLLMMessageParams, AbortRef, SendLLMMessageParams, MainLLMMessageAbortParams, MainModelListParams, ModelListParams, EventModelListOnSuccessParams, EventModelListOnErrorParams, OllamaModelResponse, OpenaiCompatibleModelResponse, } from '../common/llmMessageTypes.js'; import { sendLLMMessage } from './llmMessage/sendLLMMessage.js' import { IMetricsService } from '../common/metricsService.js'; import { ollamaList } from './llmMessage/ollama.js'; @@ -97,7 +97,7 @@ export class LLMMessageChannel implements IServerChannel { if (!(requestId in this._abortRefOfRequestId_llm)) this._abortRefOfRequestId_llm[requestId] = { current: null } - const mainThreadParams: SendLLMMMessageParams = { + const mainThreadParams: SendLLMMessageParams = { ...params, onText: ({ newText, fullText }) => { this._onText_llm.fire({ requestId, newText, fullText }); }, onFinalMessage: ({ fullText }) => { this._onFinalMessage_llm.fire({ requestId, fullText }); }, diff --git a/src/vs/workbench/contrib/void/browser/autocompleteService.ts b/src/vs/workbench/contrib/void/browser/autocompleteService.ts index 63c2e854..a6dfde14 100644 --- a/src/vs/workbench/contrib/void/browser/autocompleteService.ts +++ b/src/vs/workbench/contrib/void/browser/autocompleteService.ts @@ -135,6 +135,12 @@ class LRUCache { } } +type AutocompletionPredictionType = + | 'single-line-fill-middle' + | 'single-line-redo-suffix' + // | 'multi-line-start-here' + | 'multi-line-start-on-next-line' + | 'do-not-predict' type Autocompletion = { id: number, @@ -145,7 +151,7 @@ type Autocompletion = { startTime: number, endTime: number | undefined, status: 'pending' | 'finished' | 'error', - type: 'single-line' | 'single-line-redo-suffix' | 'multi-line' + type: AutocompletionPredictionType, llmPromise: Promise | undefined, insertText: string, requestId: string | null, @@ -345,25 +351,26 @@ const postprocessAutocompletion = ({ autocompletionMatchup, autocompletion, pref // returns the text in the autocompletion to display, assuming the prefix is already matched const toInlineCompletions = ({ autocompletionMatchup, autocompletion, prefixAndSuffix, position, debug }: { autocompletionMatchup: AutocompletionMatchupBounds, autocompletion: Autocompletion, prefixAndSuffix: PrefixAndSuffixInfo, position: Position, debug?: boolean }): { insertText: string, range: Range }[] => { - // postprocess the insertText let trimmedInsertText = postprocessAutocompletion({ autocompletionMatchup, autocompletion, prefixAndSuffix, }) - - // postprocess `rangeToReplace` let rangeToReplace: Range = new Range(position.lineNumber, position.column, position.lineNumber, position.column) - if (autocompletion.type === 'single-line-redo-suffix' // did we redo the line? if so, replace the whole suffix + + // handle special cases + + // if we are predicting starting on the next line, add a newline character + if (autocompletion.type === 'multi-line-start-on-next-line') { + trimmedInsertText = _ln + trimmedInsertText + } + // if we redid the suffix, replace the suffix + if (autocompletion.type === 'single-line-redo-suffix' && isSubsequence({ // check that the old text contains the same brackets + symbols as the new text subsequence: removeAllWhitespace(prefixAndSuffix.suffixToTheRightOfCursor), - of: removeAllWhitespace(autocompletion.insertText), // should not be `trimmedInsertText` + of: removeAllWhitespace(autocompletion.insertText), // note that this should not be `trimmedInsertText` }) ) { rangeToReplace = new Range(position.lineNumber, position.column, position.lineNumber, Number.MAX_SAFE_INTEGER) - } else { // did not matchup, do not show the autocompletion - return [{ - insertText: '', - range: rangeToReplace - }] } + return [{ insertText: trimmedInsertText, range: rangeToReplace, @@ -499,44 +506,86 @@ const getAutocompletionMatchup = ({ prefix, autocompletion }: { prefix: string, } -const getCompletionOptions = (prefixAndSuffix: PrefixAndSuffixInfo, relevantContext: string) => { +// const x = [] +// const +// c[[]] +// asd[[]] = +// const [{{}}] +// +type CompletionOptions = { + predictionType: AutocompletionPredictionType, + shouldGenerate: boolean, + llmPrefix: string, + llmSuffix: string, + stopTokens: string[], +} +const getCompletionOptions = (prefixAndSuffix: PrefixAndSuffixInfo, relevantContext: string, justAcceptedAutocompletion: boolean): CompletionOptions => { const { prefix, suffix, prefixToTheLeftOfCursor, suffixToTheRightOfCursor, suffixLines } = prefixAndSuffix - // single line prediction unless the current line is blank - let predictionType: Autocompletion['type'] - let llmPrefix = prefix - let llmSuffix = suffix + let completionOptions: CompletionOptions - if (!prefixToTheLeftOfCursor.trim() && !suffixToTheRightOfCursor.trim()) { // line is empty - predictionType = 'multi-line' + // if line is empty, do multiline completion + const isLineEmpty = !prefixToTheLeftOfCursor.trim() && !suffixToTheRightOfCursor.trim() + const isLinePrefixEmpty = removeAllWhitespace(prefixToTheLeftOfCursor).length === 0 + const isLineSuffixEmpty = removeAllWhitespace(suffixToTheRightOfCursor).length === 0 - } else if (removeAllWhitespace(prefixAndSuffix.suffixToTheRightOfCursor).length < 4) { // suffix is less than 4 characters - predictionType = 'single-line-redo-suffix' + // TODO add context to prefix + // llmPrefix = '\n\n/* Relevant context:\n' + relevantContext + '\n*/\n' + llmPrefix + + // if we just accepted an autocompletion, predict a multiline completion starting on the next line + if (justAcceptedAutocompletion && isLineSuffixEmpty) { + const prefixWithNewline = prefix + _ln + completionOptions = { + predictionType: 'multi-line-start-on-next-line', + shouldGenerate: true, + llmPrefix: prefixWithNewline, + llmSuffix: suffix, + stopTokens: [`${_ln}${_ln}`] // double newlines + } + } + // if the current line is empty, predict a single-line completion + else if (isLineEmpty) { + completionOptions = { + predictionType: 'single-line-fill-middle', + shouldGenerate: true, + llmPrefix: prefix, + llmSuffix: suffix, + stopTokens: allLinebreakSymbols + } + } + // if suffix is 3 or less characters, attempt to complete the line ignorning it + else if (removeAllWhitespace(suffixToTheRightOfCursor).length <= 3) { const suffixLinesIgnoringThisLine = suffixLines.slice(1) - llmSuffix = suffixLinesIgnoringThisLine.length === 0 ? '' : _ln + suffixLinesIgnoringThisLine.join(_ln) - + const suffixStringIgnoringThisLine = suffixLinesIgnoringThisLine.length === 0 ? '' : _ln + suffixLinesIgnoringThisLine.join(_ln) + completionOptions = { + predictionType: 'single-line-redo-suffix', + shouldGenerate: true, + llmPrefix: prefix, + llmSuffix: suffixStringIgnoringThisLine, + stopTokens: allLinebreakSymbols + } + } + // else attempt to complete the middle of the line if there is a prefix (the completion looks bad if there is no prefix) + else if (!isLinePrefixEmpty) { + completionOptions = { + predictionType: 'single-line-fill-middle', + shouldGenerate: true, + llmPrefix: prefix, + llmSuffix: suffix, + stopTokens: allLinebreakSymbols + } } else { - predictionType = 'single-line' + completionOptions = { + predictionType: 'do-not-predict', + shouldGenerate: false, + llmPrefix: prefix, + llmSuffix: suffix, + stopTokens: [] + } } - llmPrefix = llmPrefix + '\n\n/* Relevant context:\n' + relevantContext + '\n*/\n' - - // default parameters - let shouldGenerate = true - let stopTokens: string[] = allLinebreakSymbols // default to multi-line prediction - - // Case 1: User is on a line with text to the left or right - if (prefixToTheLeftOfCursor.trim() !== '' || suffixToTheRightOfCursor.trim() !== '') { - stopTokens = allLinebreakSymbols // single line prediction - } - - // Don't generate if at the very beginning of a line - if (prefixToTheLeftOfCursor === '') { - shouldGenerate = false - } - - return { shouldGenerate, predictionType, stopTokens, llmPrefix, llmSuffix } + return completionOptions } @@ -555,8 +604,9 @@ export class AutocompleteService extends Disposable implements IAutocompleteServ private _autocompletionId: number = 0; private _autocompletionsOfDocument: { [docUriStr: string]: LRUCache } = {} - private _lastCompletionTime = 0 - private _lastPrefix: string = '' + private _lastCompletionStart = 0 + private _lastCompletionAccept = 0 + // private _lastPrefix: string = '' // used internally by vscode // fires after every keystroke and returns the completion to show @@ -567,6 +617,8 @@ export class AutocompleteService extends Disposable implements IAutocompleteServ token: CancellationToken, ): Promise { + console.log('START1') + const testMode = false const docUriStr = model.uri.toString(); @@ -585,7 +637,8 @@ export class AutocompleteService extends Disposable implements IAutocompleteServ } ) } - this._lastPrefix = prefix + // this._lastPrefix = prefix + console.log('START2') // print all pending autocompletions // let _numPending = 0 @@ -604,19 +657,24 @@ export class AutocompleteService extends Disposable implements IAutocompleteServ } } + console.log('START3') + // if there is a cached autocompletion, return it if (cachedAutocompletion && autocompletionMatchup) { + console.log('AAA') + + // console.log('id: ' + cachedAutocompletion.id) if (cachedAutocompletion.status === 'finished') { - // console.log('A1') + console.log('A1') const inlineCompletions = toInlineCompletions({ autocompletionMatchup, autocompletion: cachedAutocompletion, prefixAndSuffix, position, debug: true }) return inlineCompletions } else if (cachedAutocompletion.status === 'pending') { - // console.log('A2') + console.log('A2') try { await cachedAutocompletion.llmPromise; @@ -629,7 +687,9 @@ export class AutocompleteService extends Disposable implements IAutocompleteServ } } else if (cachedAutocompletion.status === 'error') { - // console.log('A3') + console.log('A3') + } else { + console.log('A4') } return [] @@ -638,10 +698,10 @@ export class AutocompleteService extends Disposable implements IAutocompleteServ // 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 + this._lastCompletionStart = thisTime const didTypingHappenDuringDebounce = await new Promise((resolve, reject) => setTimeout(() => { - if (this._lastCompletionTime === thisTime) { + if (this._lastCompletionStart === thisTime) { resolve(false) } else { resolve(true) @@ -651,6 +711,8 @@ export class AutocompleteService extends Disposable implements IAutocompleteServ // if more typing happened, then do not go forwards with the request if (didTypingHappenDuringDebounce) { + console.log('START4') + return [] } @@ -667,20 +729,24 @@ export class AutocompleteService extends Disposable implements IAutocompleteServ if (numPending >= MAX_PENDING_REQUESTS) { // cancel the oldest pending request and remove it from cache this._autocompletionsOfDocument[docUriStr].delete(oldestPending.id) + console.log('START5') break } } } + console.log('START6') - // NEW: gather relevant context from the code around the user's selection and definitions + // gather relevant context from the code around the user's selection and definitions const relevantContext = await this._gatherRelevantContextForPosition( model, position, 3, //recursion depth 1 // number of lines to view in each recursion ); - const { shouldGenerate, predictionType, llmPrefix, llmSuffix } = getCompletionOptions(prefixAndSuffix, relevantContext) // TODO use stop tokens + const justAcceptedAutocompletion = thisTime - this._lastCompletionAccept < 500 + + const { shouldGenerate, predictionType, llmPrefix, llmSuffix, stopTokens } = getCompletionOptions(prefixAndSuffix, relevantContext, justAcceptedAutocompletion) if (!shouldGenerate) return [] @@ -688,6 +754,8 @@ export class AutocompleteService extends Disposable implements IAutocompleteServ return [] } + + // console.log('B') // create a new autocompletion and add it to cache @@ -706,15 +774,20 @@ export class AutocompleteService extends Disposable implements IAutocompleteServ requestId: null, } + console.log('BBBBBBB') + console.log('PREFIX', JSON.stringify(llmPrefix)) + console.log('SUFFIX', JSON.stringify(llmSuffix)) + console.log('PREDICTION_TYPE', predictionType) + // set parameters of `newAutocompletion` appropriately newAutocompletion.llmPromise = new Promise((resolve, reject) => { const requestId = this._llmMessageService.sendLLMMessage({ type: 'ollamaFIM', - // TODO: Incorporate relevant context directly into the prefix messages: { prefix: llmPrefix, suffix: llmSuffix, + stopTokens: stopTokens, }, logging: { loggingName: 'Autocomplete' }, onText: async ({ fullText }) => { @@ -722,12 +795,13 @@ export class AutocompleteService extends Disposable implements IAutocompleteServ newAutocompletion.insertText = fullText // if generation doesn't match the prefix for the first few tokens generated, reject it - if (!getAutocompletionMatchup({ prefix: this._lastPrefix, autocompletion: newAutocompletion })) { - reject('LLM response did not match user\'s text.') - } + // if (!getAutocompletionMatchup({ prefix: this._lastPrefix, autocompletion: newAutocompletion })) { + // reject('LLM response did not match user\'s text.') + // } }, onFinalMessage: ({ fullText }) => { + console.log('FULL TEXT', JSON.stringify(fullText)) // newAutocompletion.prefix = prefix // newAutocompletion.suffix = suffix // newAutocompletion.startTime = Date.now() @@ -738,6 +812,8 @@ export class AutocompleteService extends Disposable implements IAutocompleteServ const [text, _] = extractCodeFromRegular({ text: fullText, recentlyAddedTextLen: 0 }) newAutocompletion.insertText = postprocessResult(text) + console.log('RESULT', JSON.stringify(newAutocompletion.insertText)) + resolve(newAutocompletion.insertText) }, @@ -869,14 +945,12 @@ export class AutocompleteService extends Disposable implements IAutocompleteServ this._langFeatureService.inlineCompletionsProvider.register('*', { provideInlineCompletions: async (model, position, context, token) => { - console.log('AAAAAAAAA') const items = await this._provideInlineCompletionItems(model, position, context, token) // console.log('item: ', items?.[0]?.insertText) return { items: items, } }, freeInlineCompletions: (completions) => { - console.log('BBBBBBBB') // get the `docUriStr` and the `position` of the cursor const activePane = this._editorService.activeEditorPane; @@ -899,6 +973,8 @@ export class AutocompleteService extends Disposable implements IAutocompleteServ completions.items.forEach(item => { this._autocompletionsOfDocument[docUriStr].items.forEach((autocompletion: Autocompletion) => { if (removeLeftTabsAndTrimEnds(prefix) === removeLeftTabsAndTrimEnds(autocompletion.prefix + autocompletion.insertText)) { + console.log('ACCEPT AUTCOMPLETE', autocompletion.id) + this._lastCompletionAccept = Date.now() this._autocompletionsOfDocument[docUriStr].delete(autocompletion.id); } }); diff --git a/src/vs/workbench/contrib/void/browser/chatThreadService.ts b/src/vs/workbench/contrib/void/browser/chatThreadService.ts index 8c9ad0e5..468708b3 100644 --- a/src/vs/workbench/contrib/void/browser/chatThreadService.ts +++ b/src/vs/workbench/contrib/void/browser/chatThreadService.ts @@ -202,6 +202,7 @@ class ChatThreadService extends Disposable implements IChatThreadService { this._setStreamState(threadId, { error: undefined }) const llmCancelToken = this._llmMessageService.sendLLMMessage({ + type: 'sendLLMMessage', logging: { loggingName: 'Chat' }, messages: [ { role: 'system', content: chat_systemMessage }, From d7a6611d891dc6374e6a1a5b57eb284bf5659606 Mon Sep 17 00:00:00 2001 From: Andrew Pareles <43356051+andrewpareles@users.noreply.github.com> Date: Tue, 28 Jan 2025 01:23:40 -0800 Subject: [PATCH 07/12] read --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f1594fd4..15249823 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ This repo contains the full sourcecode for Void. We are currently in [open beta] 2. To get started working on Void, see [Contributing](https://github.com/voideditor/void/blob/main/CONTRIBUTING.md). -3. We're open to collaborations of all types - just reach out. +3. We're open to collaborations and suggestions of all types - just reach out. ## Reference From a3e62ee6f4d3c5812852ee65f84207a83b770a20 Mon Sep 17 00:00:00 2001 From: Mathew Pareles Date: Tue, 28 Jan 2025 01:43:46 -0800 Subject: [PATCH 08/12] autocompletion multiline works --- .../void/browser/autocompleteService.ts | 93 +++++++++---------- 1 file changed, 44 insertions(+), 49 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/autocompleteService.ts b/src/vs/workbench/contrib/void/browser/autocompleteService.ts index a6dfde14..eb4d9f7b 100644 --- a/src/vs/workbench/contrib/void/browser/autocompleteService.ts +++ b/src/vs/workbench/contrib/void/browser/autocompleteService.ts @@ -163,7 +163,7 @@ const MAX_CACHE_SIZE = 20 const MAX_PENDING_REQUESTS = 2 // postprocesses the result -const postprocessResult = (result: string) => { +const joinSpaces = (result: string) => { // trim all whitespace except for a single leading/trailing space // return result.trim() @@ -251,7 +251,6 @@ function getStringUpToUnbalancedClosingParenthesis(s: string, prefix: string): s } -const parenthesisChars = `{}()[]<>\`'"` // further trim the autocompletion const postprocessAutocompletion = ({ autocompletionMatchup, autocompletion, prefixAndSuffix }: { autocompletionMatchup: AutocompletionMatchupBounds, autocompletion: Autocompletion, prefixAndSuffix: PrefixAndSuffixInfo }) => { @@ -291,15 +290,15 @@ const postprocessAutocompletion = ({ autocompletionMatchup, autocompletion, pref 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 + // if the generated FIM text matches with the suffix on the current line, stop + if (autocompletion.type === 'single-line-fill-middle' && 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)) { + if (`{}()[]<>\`'"`.includes(matchChar)) { endIdx = Math.min(endIdx, matchIdx) } } @@ -356,21 +355,20 @@ const toInlineCompletions = ({ autocompletionMatchup, autocompletion, prefixAndS // handle special cases - // if we are predicting starting on the next line, add a newline character - if (autocompletion.type === 'multi-line-start-on-next-line') { - trimmedInsertText = _ln + trimmedInsertText - } // if we redid the suffix, replace the suffix - if (autocompletion.type === 'single-line-redo-suffix' - && isSubsequence({ // check that the old text contains the same brackets + symbols as the new text - subsequence: removeAllWhitespace(prefixAndSuffix.suffixToTheRightOfCursor), - of: removeAllWhitespace(autocompletion.insertText), // note that this should not be `trimmedInsertText` - }) - ) { - rangeToReplace = new Range(position.lineNumber, position.column, position.lineNumber, Number.MAX_SAFE_INTEGER) + if (autocompletion.type === 'single-line-redo-suffix') { + if (isSubsequence({ // check that the old text contains the same brackets + symbols as the new text + subsequence: removeAllWhitespace(prefixAndSuffix.suffixToTheRightOfCursor), // old suffix + of: removeAllWhitespace(autocompletion.insertText), // new suffix (note that this should not be `trimmedInsertText`) + })) { + rangeToReplace = new Range(position.lineNumber, position.column, position.lineNumber, Number.MAX_SAFE_INTEGER) + } + else { + // TODO redo the autocompletion + trimmedInsertText = '' // for now set the mismatched text to '' + } } - return [{ insertText: trimmedInsertText, range: rangeToReplace, @@ -450,7 +448,7 @@ const getAutocompletionMatchup = ({ prefix, autocompletion }: { prefix: string, // 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') + // console.log('@undefined1') return undefined } @@ -458,7 +456,7 @@ const getAutocompletionMatchup = ({ prefix, autocompletion }: { prefix: string, !(trimmedCompletionPrefix + trimmedCompletionMiddle) .startsWith(trimmedCurrentPrefix) ) { - console.log('@undefined2') + // console.log('@undefined2') return undefined } @@ -468,7 +466,7 @@ const getAutocompletionMatchup = ({ prefix, autocompletion }: { prefix: string, trimmedCompletionPrefix.split(_ln).length; if (lineStart < 0) { - console.log('@undefined3') + // console.log('@undefined3') console.error('Error: No line found.'); return undefined; @@ -484,7 +482,7 @@ const getAutocompletionMatchup = ({ prefix, autocompletion }: { prefix: string, const charMatchIdx = fullCompletionLine.indexOf(currentPrefixLine) if (charMatchIdx < 0) { - console.log('@undefined4', charMatchIdx) + // console.log('@undefined4', charMatchIdx) console.error('Warning: Found character with negative index. This should never happen.') return undefined @@ -617,7 +615,7 @@ export class AutocompleteService extends Disposable implements IAutocompleteServ token: CancellationToken, ): Promise { - console.log('START1') + console.log('START_0') const testMode = false @@ -626,8 +624,8 @@ export class AutocompleteService extends Disposable implements IAutocompleteServ const prefixAndSuffix = getPrefixAndSuffixInfo(model, position) const { prefix, suffix } = prefixAndSuffix - // initialize cache and other variables - // note that whenever an autocompletion is rejected, it is removed from cache + // initialize cache if it doesnt exist + // note that whenever an autocompletion is accepted, it is removed from cache if (!this._autocompletionsOfDocument[docUriStr]) { this._autocompletionsOfDocument[docUriStr] = new LRUCache( MAX_CACHE_SIZE, @@ -638,7 +636,6 @@ export class AutocompleteService extends Disposable implements IAutocompleteServ ) } // this._lastPrefix = prefix - console.log('START2') // print all pending autocompletions // let _numPending = 0 @@ -657,12 +654,10 @@ export class AutocompleteService extends Disposable implements IAutocompleteServ } } - console.log('START3') - // if there is a cached autocompletion, return it if (cachedAutocompletion && autocompletionMatchup) { - console.log('AAA') + console.log('AA') // console.log('id: ' + cachedAutocompletion.id) @@ -696,8 +691,12 @@ export class AutocompleteService extends Disposable implements IAutocompleteServ } // 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() + + const justAcceptedAutocompletion = thisTime - this._lastCompletionAccept < 500 + this._lastCompletionStart = thisTime const didTypingHappenDuringDebounce = await new Promise((resolve, reject) => setTimeout(() => { @@ -711,8 +710,6 @@ export class AutocompleteService extends Disposable implements IAutocompleteServ // if more typing happened, then do not go forwards with the request if (didTypingHappenDuringDebounce) { - console.log('START4') - return [] } @@ -729,13 +726,11 @@ export class AutocompleteService extends Disposable implements IAutocompleteServ if (numPending >= MAX_PENDING_REQUESTS) { // cancel the oldest pending request and remove it from cache this._autocompletionsOfDocument[docUriStr].delete(oldestPending.id) - console.log('START5') break } } } - console.log('START6') // gather relevant context from the code around the user's selection and definitions const relevantContext = await this._gatherRelevantContextForPosition( @@ -744,7 +739,6 @@ export class AutocompleteService extends Disposable implements IAutocompleteServ 3, //recursion depth 1 // number of lines to view in each recursion ); - const justAcceptedAutocompletion = thisTime - this._lastCompletionAccept < 500 const { shouldGenerate, predictionType, llmPrefix, llmSuffix, stopTokens } = getCompletionOptions(prefixAndSuffix, relevantContext, justAcceptedAutocompletion) @@ -774,10 +768,8 @@ export class AutocompleteService extends Disposable implements IAutocompleteServ requestId: null, } - console.log('BBBBBBB') - console.log('PREFIX', JSON.stringify(llmPrefix)) - console.log('SUFFIX', JSON.stringify(llmSuffix)) - console.log('PREDICTION_TYPE', predictionType) + console.log('BB') + console.log(predictionType) // set parameters of `newAutocompletion` appropriately newAutocompletion.llmPromise = new Promise((resolve, reject) => { @@ -801,7 +793,8 @@ export class AutocompleteService extends Disposable implements IAutocompleteServ }, onFinalMessage: ({ fullText }) => { - console.log('FULL TEXT', JSON.stringify(fullText)) + console.log('____res: ', JSON.stringify(newAutocompletion.insertText)) + // newAutocompletion.prefix = prefix // newAutocompletion.suffix = suffix // newAutocompletion.startTime = Date.now() @@ -810,9 +803,12 @@ export class AutocompleteService extends Disposable implements IAutocompleteServ newAutocompletion.status = 'finished' // newAutocompletion.promise = undefined const [text, _] = extractCodeFromRegular({ text: fullText, recentlyAddedTextLen: 0 }) - newAutocompletion.insertText = postprocessResult(text) + newAutocompletion.insertText = joinSpaces(text) - console.log('RESULT', JSON.stringify(newAutocompletion.insertText)) + // handle special case for predicting starting on the next line, add a newline character + if (newAutocompletion.type === 'multi-line-start-on-next-line') { + newAutocompletion.insertText = _ln + newAutocompletion.insertText + } resolve(newAutocompletion.insertText) @@ -951,7 +947,6 @@ export class AutocompleteService extends Disposable implements IAutocompleteServ return { items: items, } }, freeInlineCompletions: (completions) => { - // get the `docUriStr` and the `position` of the cursor const activePane = this._editorService.activeEditorPane; if (!activePane) return; @@ -970,14 +965,14 @@ export class AutocompleteService extends Disposable implements IAutocompleteServ // 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 (removeLeftTabsAndTrimEnds(prefix) === removeLeftTabsAndTrimEnds(autocompletion.prefix + autocompletion.insertText)) { - console.log('ACCEPT AUTCOMPLETE', autocompletion.id) - this._lastCompletionAccept = Date.now() - this._autocompletionsOfDocument[docUriStr].delete(autocompletion.id); - } - }); + this._autocompletionsOfDocument[docUriStr].items.forEach((autocompletion: Autocompletion) => { + // const matchup = getAutocompletionMatchup({ prefix, autocompletion }) + const matchup = removeAllWhitespace(prefix) === removeAllWhitespace(autocompletion.prefix + autocompletion.insertText) + if (matchup) { + console.log('ACCEPT', autocompletion.id) + this._lastCompletionAccept = Date.now() + this._autocompletionsOfDocument[docUriStr].delete(autocompletion.id); + } }); }, From 27d009314bb7d6f97c5df783517be6efc1991f9c Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Wed, 29 Jan 2025 15:55:18 -0800 Subject: [PATCH 09/12] finish merge --- src/vs/platform/void/electron-main/llmMessage/sendLLMMessage.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/platform/void/electron-main/llmMessage/sendLLMMessage.ts b/src/vs/platform/void/electron-main/llmMessage/sendLLMMessage.ts index 52a53d37..ab64fd85 100644 --- a/src/vs/platform/void/electron-main/llmMessage/sendLLMMessage.ts +++ b/src/vs/platform/void/electron-main/llmMessage/sendLLMMessage.ts @@ -146,7 +146,7 @@ export const sendLLMMessage = ({ sendGroqMsg({ messages: messagesArr, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName }); break; case 'mistral': - sendMistralMsg({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName }); + sendMistralMsg({ messages: messagesArr, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName }); break; default: onError({ message: `Error: Void provider was "${providerName}", which is not recognized.`, fullError: null }) From d9cf21448c8ed81118747347af6979a76cc8a911 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Thu, 30 Jan 2025 15:07:02 -0800 Subject: [PATCH 10/12] add model selection + misc UI improvements (notably, change how model dropdown works) --- package-lock.json | 60 +++++ package.json | 1 + .../void/common/voidSettingsService.ts | 7 +- .../platform/void/common/voidSettingsTypes.ts | 20 +- .../browser/react/src/markdown/BlockCode.tsx | 5 +- .../react/src/sidebar-tsx/SidebarChat.tsx | 234 +++++++++--------- .../void/browser/react/src/util/inputs.tsx | 167 ++++++------- .../src/void-settings-tsx/ModelDropdown.tsx | 2 +- .../react/src/void-settings-tsx/Settings.tsx | 17 +- 9 files changed, 296 insertions(+), 217 deletions(-) diff --git a/package-lock.json b/package-lock.json index 966a06e8..bc045891 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "license": "MIT", "dependencies": { "@anthropic-ai/sdk": "^0.32.1", + "@floating-ui/react": "^0.27.3", "@google/generative-ai": "^0.21.0", "@microsoft/1ds-core-js": "^3.2.13", "@microsoft/1ds-post-js": "^3.2.13", @@ -1551,6 +1552,65 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, + "node_modules/@floating-ui/core": { + "version": "1.6.9", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.9.tgz", + "integrity": "sha512-uMXCuQ3BItDUbAMhIXw7UPXRfAlOAvZzdK9BWpE60MCn+Svt3aLn9jsPTi/WNGlRUu2uI0v5S7JiIUsbsvh3fw==", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.9" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.6.13", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.13.tgz", + "integrity": "sha512-umqzocjDgNRGTuO7Q8CU32dkHkECqI8ZdMZ5Swb6QAM0t5rnlrN3lGo1hdpscRd3WS8T6DKYK4ephgIH9iRh3w==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.6.0", + "@floating-ui/utils": "^0.2.9" + } + }, + "node_modules/@floating-ui/react": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@floating-ui/react/-/react-0.27.3.tgz", + "integrity": "sha512-CLHnes3ixIFFKVQDdICjel8muhFLOBdQH7fgtHNPY8UbCNqbeKZ262G7K66lGQOUQWWnYocf7ZbUsLJgGfsLHg==", + "license": "MIT", + "dependencies": { + "@floating-ui/react-dom": "^2.1.2", + "@floating-ui/utils": "^0.2.9", + "tabbable": "^6.0.0" + }, + "peerDependencies": { + "react": ">=17.0.0", + "react-dom": ">=17.0.0" + } + }, + "node_modules/@floating-ui/react-dom": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.2.tgz", + "integrity": "sha512-06okr5cgPzMNBy+Ycse2A6udMi4bqwW/zgBF/rwjcNqWkyr82Mcg8b0vjX8OJpZFy/FKjJmw6wV7t44kK6kW7A==", + "license": "MIT", + "dependencies": { + "@floating-ui/dom": "^1.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/react/node_modules/tabbable": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.2.0.tgz", + "integrity": "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==", + "license": "MIT" + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.9.tgz", + "integrity": "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==", + "license": "MIT" + }, "node_modules/@google/generative-ai": { "version": "0.21.0", "resolved": "https://registry.npmjs.org/@google/generative-ai/-/generative-ai-0.21.0.tgz", diff --git a/package.json b/package.json index c8f0159b..a4ee38bb 100644 --- a/package.json +++ b/package.json @@ -79,6 +79,7 @@ }, "dependencies": { "@anthropic-ai/sdk": "^0.32.1", + "@floating-ui/react": "^0.27.3", "@google/generative-ai": "^0.21.0", "@microsoft/1ds-core-js": "^3.2.13", "@microsoft/1ds-post-js": "^3.2.13", diff --git a/src/vs/platform/void/common/voidSettingsService.ts b/src/vs/platform/void/common/voidSettingsService.ts index c92810ab..811f4337 100644 --- a/src/vs/platform/void/common/voidSettingsService.ts +++ b/src/vs/platform/void/common/voidSettingsService.ts @@ -81,7 +81,7 @@ let _computeModelOptions = (settingsOfProvider: SettingsOfProvider) => { const defaultState = () => { const d: VoidSettingsState = { settingsOfProvider: deepClone(defaultSettingsOfProvider), - modelSelectionOfFeature: { 'Ctrl+L': null, 'Ctrl+K': null, 'Autocomplete': null }, + modelSelectionOfFeature: { 'Ctrl+L': null, 'Ctrl+K': null, 'Autocomplete': null, 'FastApply': null }, globalSettings: deepClone(defaultGlobalSettings), _modelOptions: _computeModelOptions(defaultSettingsOfProvider), // computed } @@ -137,6 +137,11 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService { ...defaultSettingsOfProvider.gemini.models.filter(m => /* if cant find the model in readS (yes this is O(n^2), very small) */ !readS.settingsOfProvider.gemini.models.find(m2 => m2.modelName === m.modelName)) ] } + }, + modelSelectionOfFeature: { + // A HACK BECAUSE WE ADDED FastApply + ...{ 'FastApply': null }, + ...readS.modelSelectionOfFeature, } } diff --git a/src/vs/platform/void/common/voidSettingsTypes.ts b/src/vs/platform/void/common/voidSettingsTypes.ts index f0f2624a..43a29f5b 100644 --- a/src/vs/platform/void/common/voidSettingsTypes.ts +++ b/src/vs/platform/void/common/voidSettingsTypes.ts @@ -432,14 +432,22 @@ export const modelSelectionsEqual = (m1: ModelSelection, m2: ModelSelection) => } // this is a state -export type ModelSelectionOfFeature = { - 'Ctrl+L': ModelSelection | null, - 'Ctrl+K': ModelSelection | null, - 'Autocomplete': ModelSelection | null, -} +export const featureNames = ['Ctrl+L', 'Ctrl+K', 'Autocomplete', 'FastApply'] as const +export type ModelSelectionOfFeature = Record<(typeof featureNames)[number], ModelSelection | null> export type FeatureName = keyof ModelSelectionOfFeature -export const featureNames = ['Ctrl+L', 'Ctrl+K', 'Autocomplete'] as const +export const displayInfoOfFeatureName = (featureName: FeatureName) => { + if (featureName === 'Autocomplete') + return 'Autocomplete' + else if (featureName === 'Ctrl+K') + return 'Quick Edit' + else if (featureName === 'Ctrl+L') + return 'Sidebar Chat' + else if (featureName === 'FastApply') + return 'Fast Apply' + else + throw new Error(`Feature Name ${featureName} not allowed`) +} diff --git a/src/vs/workbench/contrib/void/browser/react/src/markdown/BlockCode.tsx b/src/vs/workbench/contrib/void/browser/react/src/markdown/BlockCode.tsx index 43abd5b1..4c2cbea4 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/markdown/BlockCode.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/markdown/BlockCode.tsx @@ -13,10 +13,9 @@ export const BlockCode = ({ buttonsOnHover, ...codeEditorProps }: { buttonsOnHov return ( <> -
+
{buttonsOnHover === null ? null : ( -
+
{buttonsOnHover}
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 8cb4864f..0ea5febb 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 @@ -436,7 +436,7 @@ const ChatBubble_ = ({ isEditMode, isLoading, children, role }: { role: ChatMess className={` relative ${isEditMode ? 'px-2 w-full max-w-full' - : role === 'user' ? `px-2 self-end w-fit max-w-full` + : role === 'user' ? `px-2 self-end w-fit max-w-full whitespace-pre-wrap` // user words should be pre : role === 'assistant' ? `px-2 self-start w-full max-w-full` : '' } `} @@ -444,7 +444,7 @@ const ChatBubble_ = ({ isEditMode, isLoading, children, role }: { role: ChatMess
{ ) }, [previousMessages]) - return
- {/* thread selector */} -
- -
- - {/* previous messages + current stream */} - - {/* previous messages */} - {prevMessagesHTML} - - {/* message stream */} - + +
- {/* error message */} - {latestError === undefined ? null : -
- { chatThreadsService.dismissStreamError(currentThread.id) }} - showDismiss={true} - /> + const messagesHTML = + {/* previous messages */} + {prevMessagesHTML} - { commandService.executeCommand(VOID_OPEN_SETTINGS_ACTION_ID) }} text='Open settings' /> -
- } - - + {/* message stream */} + - {/* input box */} -
0 ? 'absolute bottom-0' : ''}`} - > -
{ - textAreaRef.current?.focus() - }} - > - {/* top row */} - <> - {/* selections */} - - - - {/* middle row */} -
- - {/* text input */} - { setInstructionsAreEmpty(!newStr) }, [setInstructionsAreEmpty])} - onKeyDown={(e) => { - if (e.key === 'Enter' && !e.shiftKey) { - onSubmit() - } - }} - ref={textAreaRef} - fnsRef={textAreaFnsRef} - multiline={true} - /> -
- - {/* bottom row */} -
- {/* submit options */} -
- -
- - {/* submit / stop button */} - {isStreaming ? - // stop button - - : - // submit button (up arrow) - - } -
- + {/* error message */} + {latestError === undefined ? null : +
+ { chatThreadsService.dismissStreamError(currentThread.id) }} + showDismiss={true} + /> + { commandService.executeCommand(VOID_OPEN_SETTINGS_ACTION_ID) }} text='Open settings' />
-
-
+ } + + + + const inputBox =
0 ? 'absolute bottom-0' : ''}`} + > +
{ + textAreaRef.current?.focus() + }} + > + {/* top row */} + <> + {/* selections */} + + + + {/* middle row */} +
+ + {/* text input */} + { setInstructionsAreEmpty(!newStr) }, [setInstructionsAreEmpty])} + onKeyDown={(e) => { + if (e.key === 'Enter' && !e.shiftKey) { + onSubmit() + } + }} + ref={textAreaRef} + fnsRef={textAreaFnsRef} + multiline={true} + /> +
+ + {/* bottom row */} +
+ {/* submit options */} +
+ +
+ + {/* submit / stop button */} + {isStreaming ? + // stop button + + : + // submit button (up arrow) + + } +
+
+
+ + return
+ {threadSelector} + + {messagesHTML} + + {inputBox} + +
} diff --git a/src/vs/workbench/contrib/void/browser/react/src/util/inputs.tsx b/src/vs/workbench/contrib/void/browser/react/src/util/inputs.tsx index b21df36f..357640c0 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/util/inputs.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/util/inputs.tsx @@ -15,6 +15,7 @@ import { useAccessor } from './services.js'; import { ITextModel } from '../../../../../../../editor/common/model.js'; import { asCssVariable } from '../../../../../../../platform/theme/common/colorUtils.js'; import { inputBackground, inputForeground } from '../../../../../../../platform/theme/common/colorRegistry.js'; +import { useFloating, autoUpdate, offset, flip, shift, size, autoPlacement } from '@floating-ui/react'; // type guard @@ -296,6 +297,7 @@ export const VoidCheckBox = ({ label, value, onClick, className }: { label: stri } + export const VoidCustomSelectBox = ({ options, selectedOption: selectedOption_, @@ -306,7 +308,6 @@ export const VoidCustomSelectBox = ({ className, arrowTouchesText = true, matchInputWidth = false, - isMenuPositionFixed = true, gap = 0, }: { options: T[]; @@ -318,18 +319,58 @@ export const VoidCustomSelectBox = ({ className?: string; arrowTouchesText?: boolean; matchInputWidth?: boolean; - isMenuPositionFixed?: boolean; gap?: number; }) => { const [isOpen, setIsOpen] = useState(false); - const [readyToShow, setReadyToShow] = useState(false); - const [position, setPosition] = useState({ top: 0, left: 0, width: 0 }); - const containerRef = useRef(null); - const buttonRef = useRef(null); - const measureRef = useRef(null); + const measureRef = useRef(null); + // Replace manual positioning with floating-ui + const { + x, + y, + strategy, + refs, + middlewareData, + update + } = useFloating({ + open: isOpen, + onOpenChange: setIsOpen, + placement:'bottom-start', - // if the selected option is null, use the 0th option as the selected, and set the option to options[0] + middleware: [ + offset(gap), + flip({ + boundary: document.body, + padding: 8 + }), + shift({ + boundary: document.body, + padding: 8, + }), + size({ + apply({ availableHeight, elements, rects }) { + const maxHeight = Math.min(availableHeight) + + Object.assign(elements.floating.style, { + maxHeight: `${maxHeight}px`, + overflowY: 'auto', + // Ensure the width isn't constrained by the parent + width: `${Math.max( + rects.reference.width, + measureRef.current?.offsetWidth ?? 0 + )}px` + }); + }, + padding: 8, + // Use viewport as boundary instead of any parent element + boundary: document.body, + }), + ], + whileElementsMounted: autoUpdate, + strategy:'fixed', + }); + + // if the selected option is null, use the 0th option useEffect(() => { if (!options[0]) return if (!selectedOption_) { @@ -338,84 +379,33 @@ export const VoidCustomSelectBox = ({ }, [selectedOption_, options]) const selectedOption = !selectedOption_ ? options[0] : selectedOption_ - - const updatePosition = useCallback(() => { - if (!buttonRef.current || !containerRef.current || !measureRef.current) return; - - const buttonRect = buttonRef.current.getBoundingClientRect(); - const containerRect = containerRef.current.getBoundingClientRect(); - const containerWidth = containerRef.current.offsetWidth; - const viewportHeight = window.innerHeight; - const spaceBelow = viewportHeight - buttonRect.bottom; - const spaceNeeded = options.length * 28; - const showAbove = spaceBelow < spaceNeeded && buttonRect.top > spaceBelow; - - // Calculate the menu width - let menuWidth = matchInputWidth ? containerWidth : buttonRect.width; - - // If not matchInputWidth, calculate content width from measurement div - if (!matchInputWidth) { - const contentWidth = measureRef.current.offsetWidth; - menuWidth = Math.max(buttonRect.width, contentWidth); - } - - if (isMenuPositionFixed) { - // Fixed positioning (relative to viewport) - setPosition({ - top: showAbove - ? buttonRect.top - spaceNeeded - : buttonRect.bottom + gap, - left: buttonRect.left, - width: menuWidth, - }); - } else { - // Absolute positioning (relative to parent container) - setPosition({ - top: showAbove - ? -(spaceNeeded + gap) - : buttonRect.height + gap, - left: 0, - width: menuWidth, - }); - } - - setReadyToShow(true); - }, [gap, matchInputWidth, options.length, isMenuPositionFixed]); - + // Handle clicks outside useEffect(() => { - if (isOpen) { - setReadyToShow(false); - updatePosition(); - window.addEventListener('scroll', updatePosition, true); - window.addEventListener('resize', updatePosition); + if (!isOpen) return; - return () => { - window.removeEventListener('scroll', updatePosition, true); - window.removeEventListener('resize', updatePosition); - }; - } else { - setReadyToShow(false); - } - }, [isOpen, updatePosition]); - - useEffect(() => { const handleClickOutside = (event: MouseEvent) => { - if (containerRef.current && !containerRef.current.contains(event.target as Node)) { + const target = event.target as Node; + const floating = refs.floating.current; + const reference = refs.reference.current; + + // Check if reference is an HTML element before using contains + const isReferenceHTMLElement = reference && 'contains' in reference; + + if ( + floating && + (!isReferenceHTMLElement || !reference.contains(target)) && + !floating.contains(target) + ) { setIsOpen(false); } }; - if (isOpen) { - document.addEventListener('mousedown', handleClickOutside); - return () => document.removeEventListener('mousedown', handleClickOutside); - } - }, [isOpen]); + document.addEventListener('mousedown', handleClickOutside); + return () => document.removeEventListener('mousedown', handleClickOutside); + }, [isOpen, refs.floating, refs.reference]); return ( -
+
{/* Hidden measurement div */}
({ {/* Select Button */} {/* Dropdown Menu */} - {isOpen && readyToShow && ( + {isOpen && (
{options.map((option) => { diff --git a/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/ModelDropdown.tsx b/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/ModelDropdown.tsx index 67a9a9c8..d6ca33b2 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/ModelDropdown.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/ModelDropdown.tsx @@ -42,7 +42,7 @@ const ModelSelectBox = ({ options, featureName }: { options: ModelOption[], feat getOptionsEqual={(a, b) => optionsEqual([a], [b])} className={`text-xs text-void-fg-3 px-1`} matchInputWidth={false} - isMenuPositionFixed={featureName === 'Ctrl+K' ? false : true} + // isMenuPositionFixed={false} /> } // const ModelSelectBox = ({ options, featureName }: { options: ModelOption[], featureName: FeatureName }) => { diff --git a/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx b/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx index 62de3bed..7c5751d7 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx @@ -5,7 +5,7 @@ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { InputBox } from '../../../../../../../base/browser/ui/inputbox/inputBox.js' -import { ProviderName, SettingName, displayInfoOfSettingName, providerNames, VoidModelInfo, globalSettingNames, customSettingNamesOfProvider, RefreshableProviderName, refreshableProviderNames, displayInfoOfProviderName, defaultProviderSettings, nonlocalProviderNames, localProviderNames, GlobalSettingName } from '../../../../../../../platform/void/common/voidSettingsTypes.js' +import { ProviderName, SettingName, displayInfoOfSettingName, providerNames, VoidModelInfo, globalSettingNames, customSettingNamesOfProvider, RefreshableProviderName, refreshableProviderNames, displayInfoOfProviderName, defaultProviderSettings, nonlocalProviderNames, localProviderNames, GlobalSettingName, featureNames, displayInfoOfFeatureName } from '../../../../../../../platform/void/common/voidSettingsTypes.js' import ErrorBoundary from '../sidebar-tsx/ErrorBoundary.js' import { VoidButton, VoidCheckBox, VoidCustomSelectBox, VoidInputBox, VoidInputBox2, VoidSwitch } from '../util/inputs.js' import { useAccessor, useIsDark, useRefreshModelListener, useRefreshModelState, useSettingsState } from '../util/services.js' @@ -14,7 +14,7 @@ import { useScrollbarStyles } from '../util/useScrollbarStyles.js' import { isWindows, isLinux, isMacintosh } from '../../../../../../../base/common/platform.js' import { URI } from '../../../../../../../base/common/uri.js' import { env } from '../../../../../../../base/common/process.js' -import { WarningBox } from './ModelDropdown.js' +import { WarningBox, ModelDropdown } from './ModelDropdown.js' import { ChatMarkdownRender } from '../markdown/ChatMarkdownRender.js' const SubtleButton = ({ onClick, text, icon, disabled }: { onClick: () => void, text: string, icon: React.ReactNode, disabled: boolean }) => { @@ -392,7 +392,7 @@ export const AIInstructionsBox = () => { const voidSettingsService = accessor.get('IVoidSettingsService') const voidSettingsState = useSettingsState() return {
+
+

Model Selection

+ {featureNames.map(featureName => +
+

{displayInfoOfFeatureName(featureName)}

+ +
+ )} +
} From 9ec13d2ad888a3d5606e944b93b611f2ad573c5a Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Thu, 30 Jan 2025 16:37:44 -0800 Subject: [PATCH 11/12] rm comments --- src/vs/platform/void/electron-main/llmMessage/ollama.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/vs/platform/void/electron-main/llmMessage/ollama.ts b/src/vs/platform/void/electron-main/llmMessage/ollama.ts index e5753973..e76b6186 100644 --- a/src/vs/platform/void/electron-main/llmMessage/ollama.ts +++ b/src/vs/platform/void/electron-main/llmMessage/ollama.ts @@ -25,7 +25,6 @@ export const ollamaList: _InternalModelListFnType = async ( const ollama = new Ollama({ host: thisConfig.endpoint }) ollama.list() .then((response) => { - // console.log('MODELS!!!!!!!!!!!!!!!!!', response) const { models } = response onSuccess({ models }) }) @@ -69,7 +68,6 @@ export const sendOllamaFIM: _InternalOllamaFIMMessageFnType = ({ messages, onTex onText({ newText, fullText }); } onFinalMessage({ fullText }); - console.log('!!!!! OLLAMA RESULT', JSON.stringify(fullText)) }) // when error/fail .catch((error) => { From 9cd8295daba4810ab6076d3789ffb8d91638e0ae Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Thu, 30 Jan 2025 16:38:51 -0800 Subject: [PATCH 12/12] fix comment --- .../void/browser/react/src/void-settings-tsx/ModelDropdown.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/ModelDropdown.tsx b/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/ModelDropdown.tsx index d6ca33b2..bef7f449 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/ModelDropdown.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/ModelDropdown.tsx @@ -42,7 +42,7 @@ const ModelSelectBox = ({ options, featureName }: { options: ModelOption[], feat getOptionsEqual={(a, b) => optionsEqual([a], [b])} className={`text-xs text-void-fg-3 px-1`} matchInputWidth={false} - // isMenuPositionFixed={false} + // isMenuPositionFixed={featureName === 'Ctrl+K' ? false : true} /> } // const ModelSelectBox = ({ options, featureName }: { options: ModelOption[], featureName: FeatureName }) => {