diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3d73f87f..151badfc 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -8,7 +8,7 @@ Void's code mostly lives in `src/vs/workbench/contrib/void/` and `src/vs/platfor There are a few ways to contribute: -- 👨‍💻 Build new features - see [Roadmap](https://github.com/orgs/voideditor/projects/2/views/3) or [Issues](https://github.com/voideditor/void/issues). +- 👨‍💻 Build new features - see [Roadmap](https://github.com/orgs/voideditor/projects/2/views/3) and [Issues](https://github.com/voideditor/void/issues). - 💡 Make suggestions in our [Discord](https://discord.gg/RSNjgaugJs). diff --git a/README.md b/README.md index 613b772f..ca38d5b7 100644 --- a/README.md +++ b/README.md @@ -11,23 +11,22 @@ Void is the open-source Cursor alternative. -This repo contains the full sourcecode for Void. We are currently in [open beta](https://voideditor.com/) for Discord members (see `#announcements` to download), and we have a waitlist for our official release. If you're new, welcome! +This repo contains the full sourcecode for Void. We are currently in [open beta](https://voideditor.com/) for Discord members (see the `announcements` channel), with a waitlist for our official release. If you're new, welcome! - 👋 [Discord](https://discord.gg/RSNjgaugJs) +- 🔨 [Contribute](https://github.com/voideditor/void/blob/main/CONTRIBUTING.md) + - 🚙 [Roadmap](https://github.com/orgs/voideditor/projects/2) - 📝 [Changelog](https://voideditor.com/changelog) -- 🔨 [Contribute](https://github.com/voideditor/void/blob/main/CONTRIBUTING.md) - ## Contributing +1. Feel free to attend a weekly meeting in our Discord channel if you'd like to contribute! -1. To get started developing Void, see [`CONTRIBUTING.md`](https://github.com/voideditor/void/blob/main/CONTRIBUTING.md). - -2. Feel free to attend a weekly meeting in our Discord channel! +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. @@ -37,4 +36,4 @@ This repo contains the full sourcecode for Void. We are currently in [open beta] Void is a fork of the [vscode](https://github.com/microsoft/vscode) repository. For some useful links on VSCode, see [`VOID_USEFUL_LINKS.md`](https://github.com/voideditor/void/blob/main/VOID_USEFUL_LINKS.md). ## Support -Feel free to reach out in our Discord or contact us via email: support@voideditor.com. +Feel free to reach out in our Discord or contact us via email: hello@voideditor.com. diff --git a/build/win32/code.iss b/build/win32/code.iss index 8728ca24..0bd3d09c 100644 --- a/build/win32/code.iss +++ b/build/win32/code.iss @@ -23,7 +23,7 @@ AppMutex={code:GetAppMutex} SetupMutex={#AppMutex}setup ; this is a Void icon comment. Old: WizardImageFile="{#RepoDir}\resources\win32\inno-big-100.bmp,{#RepoDir}\resources\win32\inno-big-125.bmp,{#RepoDir}\resources\win32\inno-big-150.bmp,{#RepoDir}\resources\win32\inno-big-175.bmp,{#RepoDir}\resources\win32\inno-big-200.bmp,{#RepoDir}\resources\win32\inno-big-225.bmp,{#RepoDir}\resources\win32\inno-big-250.bmp" ; this is a Void icon comment. Old: WizardSmallImageFile="{#RepoDir}\resources\win32\inno-small-100.bmp,{#RepoDir}\resources\win32\inno-small-125.bmp,{#RepoDir}\resources\win32\inno-small-150.bmp,{#RepoDir}\resources\win32\inno-small-175.bmp,{#RepoDir}\resources\win32\inno-small-200.bmp,{#RepoDir}\resources\win32\inno-small-225.bmp,{#RepoDir}\resources\win32\inno-small-250.bmp" -WizardImageFile="{#RepoDir}\resources\win32\inno-void.bmp" +; WizardImageFile="{#RepoDir}\resources\win32\inno-void.bmp" WizardSmallImageFile="{#RepoDir}\resources\win32\inno-void.bmp" SetupIconFile={#RepoDir}\resources\win32\code.ico UninstallDisplayIcon={app}\{#ExeBasename}.exe diff --git a/resources/win32/code.ico b/resources/win32/code.ico index 2659eb19..26774fa2 100644 Binary files a/resources/win32/code.ico and b/resources/win32/code.ico differ diff --git a/resources/win32/inno-void.bmp b/resources/win32/inno-void.bmp index fb3fad34..3c986c74 100644 Binary files a/resources/win32/inno-void.bmp and b/resources/win32/inno-void.bmp differ diff --git a/src/vs/platform/void/common/llmMessageService.ts b/src/vs/platform/void/common/llmMessageService.ts index caaeb0c8..40855c8d 100644 --- a/src/vs/platform/void/common/llmMessageService.ts +++ b/src/vs/platform/void/common/llmMessageService.ts @@ -98,6 +98,7 @@ export class LLMMessageService extends Disposable implements ILLMMessageService } 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 }) diff --git a/src/vs/platform/void/common/llmMessageTypes.ts b/src/vs/platform/void/common/llmMessageTypes.ts index f14e82a6..77b31c07 100644 --- a/src/vs/platform/void/common/llmMessageTypes.ts +++ b/src/vs/platform/void/common/llmMessageTypes.ts @@ -30,6 +30,12 @@ export type LLMMessage = { content: string; } +export type _InternalLLMMessage = { + role: 'user' | 'assistant'; + content: string; +} + + export type ServiceSendLLMFeatureParams = { useProviderFor: 'Ctrl+K'; range: IRange; @@ -80,7 +86,7 @@ export type EventLLMMessageOnFinalMessageParams = Parameters[0] export type EventLLMMessageOnErrorParams = Parameters[0] & { requestId: string } export type _InternalSendLLMMessageFnType = (params: { - messages: LLMMessage[]; + messages: _InternalLLMMessage[]; onText: OnText; onFinalMessage: OnFinalMessage; onError: OnError; diff --git a/src/vs/platform/void/common/voidSettingsService.ts b/src/vs/platform/void/common/voidSettingsService.ts index ffaa5e72..7200d8bf 100644 --- a/src/vs/platform/void/common/voidSettingsService.ts +++ b/src/vs/platform/void/common/voidSettingsService.ts @@ -115,8 +115,13 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService { this.waitForInitState = new Promise((res, rej) => resolver = res) // read and update the actual state immediately - this._readState().then(s => { - this.state = s + this._readState().then(readS => { + + // THIS IS A HACK BECAUSE WE ADDED DEEPSEEK + const deepseekAdd = { deepseek: defaultSettingsOfProvider['deepseek'] } + readS = { ...readS, settingsOfProvider: { ...deepseekAdd, ...readS.settingsOfProvider, } } + + this.state = readS resolver() this._onDidChangeState.fire('all') }) diff --git a/src/vs/platform/void/common/voidSettingsTypes.ts b/src/vs/platform/void/common/voidSettingsTypes.ts index 222ce250..659ad446 100644 --- a/src/vs/platform/void/common/voidSettingsTypes.ts +++ b/src/vs/platform/void/common/voidSettingsTypes.ts @@ -77,6 +77,11 @@ export const defaultOpenAIModels = modelInfoOfDefaultNames([ // 'gpt-3.5-turbo-1106', ]) +// https://platform.openai.com/docs/models/gp +export const defaultDeepseekModels = modelInfoOfDefaultNames([ + 'deepseek-chat', + 'deepseek-reasoner', +]) // https://console.groq.com/docs/models @@ -141,6 +146,9 @@ export const defaultProviderSettings = { openAI: { apiKey: '', }, + deepseek: { + apiKey: '', + }, ollama: { endpoint: 'http://127.0.0.1:11434', }, @@ -212,6 +220,11 @@ export const displayInfoOfProviderName = (providerName: ProviderName): DisplayIn title: 'OpenAI', } } + else if (providerName === 'deepseek') { + return { + title: 'DeepSeek', + } + } else if (providerName === 'openRouter') { return { title: 'OpenRouter', @@ -258,21 +271,23 @@ 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 === 'openRouter' ? 'sk-or-key...' : // sk-or-v1-key - providerName === 'gemini' ? 'key...' : - providerName === 'groq' ? 'gsk_key...' : - providerName === 'mistral' ? 'api-key...' : - providerName === 'openAICompatible' ? 'sk-key...' : - '(never)', + providerName === 'deepseek' ? 'sk-...' : + providerName === 'openRouter' ? 'sk-or-key...' : // sk-or-v1-key + providerName === 'gemini' ? 'key...' : + providerName === 'groq' ? 'gsk_key...' : + providerName === 'mistral' ? 'api-key...' : + providerName === 'openAICompatible' ? 'sk-key...' : + '', subTextMd: providerName === 'anthropic' ? 'Get your [API Key here](https://console.anthropic.com/settings/keys).' : providerName === 'openAI' ? 'Get your [API Key here](https://platform.openai.com/api-keys).' : - providerName === 'openRouter' ? 'Get your [API Key here](https://openrouter.ai/settings/keys).' : - providerName === 'gemini' ? 'Get your [API Key here](https://aistudio.google.com/apikey).' : - providerName === 'groq' ? 'Get your [API Key here](https://console.groq.com/keys).' : - providerName === 'mistral' ? 'Get your [API Key here](https://console.mistral.ai/api-keys/).' : - providerName === 'openAICompatible' ? undefined : - undefined, + providerName === 'deepseek' ? 'Get your [API Key here](https://platform.deepseek.com/api_keys).' : + providerName === 'openRouter' ? 'Get your [API Key here](https://openrouter.ai/settings/keys).' : + providerName === 'gemini' ? 'Get your [API Key here](https://aistudio.google.com/apikey).' : + providerName === 'groq' ? 'Get your [API Key here](https://console.groq.com/keys).' : + providerName === 'mistral' ? 'Get your [API Key here](https://console.mistral.ai/api-keys/).' : + providerName === 'openAICompatible' ? undefined : + '', } } else if (settingName === 'endpoint') { @@ -323,6 +338,9 @@ export const voidInitModelOptions = { openAI: { models: defaultOpenAIModels, }, + deepseek: { + models: defaultDeepseekModels, + }, ollama: { models: [], }, @@ -358,6 +376,12 @@ export const defaultSettingsOfProvider: SettingsOfProvider = { ...defaultProviderSettings.openAI, ...voidInitModelOptions.openAI, }, + deepseek: { + ...defaultCustomSettings, + ...defaultProviderSettings.deepseek, + ...voidInitModelOptions.deepseek, + _enabled: undefined, + }, gemini: { ...defaultCustomSettings, ...defaultProviderSettings.gemini, diff --git a/src/vs/platform/void/common/voidUpdateService.ts b/src/vs/platform/void/common/voidUpdateService.ts index 0304073f..fd3467dd 100644 --- a/src/vs/platform/void/common/voidUpdateService.ts +++ b/src/vs/platform/void/common/voidUpdateService.ts @@ -33,7 +33,6 @@ export class VoidUpdateService implements IVoidUpdateService { } - // anything transmitted over a channel must be async even if it looks like it doesn't have to be check: IVoidUpdateService['check'] = async () => { const res = await this.voidUpdateService.check() diff --git a/src/vs/platform/void/electron-main/llmMessage/anthropic.ts b/src/vs/platform/void/electron-main/llmMessage/anthropic.ts index 04dcaa21..59d4c04a 100644 --- a/src/vs/platform/void/electron-main/llmMessage/anthropic.ts +++ b/src/vs/platform/void/electron-main/llmMessage/anthropic.ts @@ -7,11 +7,6 @@ import Anthropic from '@anthropic-ai/sdk'; import { _InternalSendLLMMessageFnType } from '../../common/llmMessageTypes.js'; import { anthropicMaxPossibleTokens } from '../../common/voidSettingsTypes.js'; -// Anthropic -type LLMMessageAnthropic = { - role: 'user' | 'assistant'; - content: string; -} export const sendAnthropicMsg: _InternalSendLLMMessageFnType = ({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter }) => { const thisConfig = settingsOfProvider.anthropic @@ -24,20 +19,9 @@ export const sendAnthropicMsg: _InternalSendLLMMessageFnType = ({ messages, onTe const anthropic = new Anthropic({ apiKey: thisConfig.apiKey, dangerouslyAllowBrowser: true }); - // find system messages and concatenate them - const systemMessage = messages - .filter(msg => msg.role === 'system') - .map(msg => msg.content) - .join('\n'); - - // remove system messages for Anthropic - const anthropicMessages = messages.filter(msg => msg.role !== 'system') as LLMMessageAnthropic[] - - - const stream = anthropic.messages.stream({ - system: systemMessage, - messages: anthropicMessages, + // system: systemMessage, + messages: messages, model: modelName, max_tokens: maxTokens, }); diff --git a/src/vs/platform/void/electron-main/llmMessage/gemini.ts b/src/vs/platform/void/electron-main/llmMessage/gemini.ts index 557d28c7..936a68f0 100644 --- a/src/vs/platform/void/electron-main/llmMessage/gemini.ts +++ b/src/vs/platform/void/electron-main/llmMessage/gemini.ts @@ -3,7 +3,7 @@ * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. *--------------------------------------------------------------------------------------*/ -import { Content, GoogleGenerativeAI, GoogleGenerativeAIFetchError } from '@google/generative-ai'; +import { Content, GoogleGenerativeAI } from '@google/generative-ai'; import { _InternalSendLLMMessageFnType } from '../../common/llmMessageTypes.js'; // Gemini @@ -16,22 +16,17 @@ export const sendGeminiMsg: _InternalSendLLMMessageFnType = async ({ messages, o const genAI = new GoogleGenerativeAI(thisConfig.apiKey); const model = genAI.getGenerativeModel({ model: modelName }); - // remove system messages that get sent to Gemini - // str of all system messages - const systemMessage = messages - .filter(msg => msg.role === 'system') - .map(msg => msg.content) - .join('\n'); - // Convert messages to Gemini format const geminiMessages: Content[] = messages - .filter(msg => msg.role !== 'system') .map((msg, i) => ({ parts: [{ text: msg.content }], role: msg.role === 'assistant' ? 'model' : 'user' })) - model.generateContentStream({ contents: geminiMessages, systemInstruction: systemMessage, }) + model.generateContentStream({ + // systemInstruction: systemMessage, + contents: geminiMessages, + }) .then(async response => { _setAborter(() => response.stream.return(fullText)) @@ -43,11 +38,6 @@ export const sendGeminiMsg: _InternalSendLLMMessageFnType = async ({ messages, o onFinalMessage({ fullText }); }) .catch((error) => { - if (error instanceof GoogleGenerativeAIFetchError && error.status === 400) { - onError({ message: 'Invalid API key.', fullError: null }); - } - else { - onError({ message: error + '', fullError: error }); - } + onError({ message: error + '', fullError: error }) }) } diff --git a/src/vs/platform/void/electron-main/llmMessage/openai.ts b/src/vs/platform/void/electron-main/llmMessage/openai.ts index 391764cb..5160fa02 100644 --- a/src/vs/platform/void/electron-main/llmMessage/openai.ts +++ b/src/vs/platform/void/electron-main/llmMessage/openai.ts @@ -69,6 +69,14 @@ export const sendOpenAIMsg: _InternalSendLLMMessageFnType = ({ messages, onText, }); options = { model: modelName, messages: messages, stream: true, /*max_completion_tokens: parseMaxTokensStr(thisConfig.maxTokens)*/ } } + else if (providerName === 'deepseek') { + const thisConfig = settingsOfProvider.deepseek + openai = new OpenAI({ + baseURL: 'https://api.deepseek.com/v1', apiKey: thisConfig.apiKey, dangerouslyAllowBrowser: true, + }); + options = { model: modelName, messages: messages, stream: true, /*max_completion_tokens: parseMaxTokensStr(thisConfig.maxTokens)*/ } + + } else if (providerName === 'openAICompatible') { const thisConfig = settingsOfProvider.openAICompatible openai = new OpenAI({ baseURL: thisConfig.endpoint, apiKey: thisConfig.apiKey, dangerouslyAllowBrowser: true }) @@ -79,7 +87,6 @@ export const sendOpenAIMsg: _InternalSendLLMMessageFnType = ({ messages, onText, throw new Error(`providerName was invalid: ${providerName}`) } - openai.models.list() openai.chat.completions .create(options) .then(async response => { @@ -98,7 +105,7 @@ export const sendOpenAIMsg: _InternalSendLLMMessageFnType = ({ messages, onText, onError({ message: 'Invalid API key.', fullError: error }); } else { - onError({ message: error, fullError: error }); + 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 2696020b..bb3884b3 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 } from '../../common/llmMessageTypes.js'; +import { LLMMMessageParams, OnText, OnFinalMessage, OnError, LLMMessage, _InternalLLMMessage } from '../../common/llmMessageTypes.js'; import { IMetricsService } from '../../common/metricsService.js'; import { sendAnthropicMsg } from './anthropic.js'; @@ -13,8 +13,43 @@ import { sendGeminiMsg } from './gemini.js'; import { sendGroqMsg } from './groq.js'; import { sendMistralMsg } from './mistral.js'; + +const cleanMessages = (messages: LLMMessage[]): _InternalLLMMessage[] => { + // trim message content (Anthropic and other providers give an error if there is trailing whitespace) + messages = messages.map(m => ({ ...m, content: m.content.trim() })) + + // find system messages and concatenate them + const systemMessage = messages + .filter(msg => msg.role === 'system') + .map(msg => msg.content) + .join('\n') || undefined; + + // remove all system messages + const noSystemMessages = messages + .filter(msg => msg.role !== 'system') as _InternalLLMMessage[] + + // add system mesasges to first message (should be a user message) + if (systemMessage && (noSystemMessages.length !== 0)) { + const newFirstMessage = { + role: noSystemMessages[0].role, + content: ('' + + '\n' + + systemMessage + + '\n' + + '\n' + + noSystemMessages[0].content + ) + } + noSystemMessages.splice(0, 1) // delete first message + noSystemMessages.unshift(newFirstMessage) // add new first message + } + + return noSystemMessages +} + + export const sendLLMMessage = ({ - messages, + messages: messages_, onText: onText_, onFinalMessage: onFinalMessage_, onError: onError_, @@ -27,9 +62,7 @@ export const sendLLMMessage = ({ metricsService: IMetricsService ) => { - - // trim message content (Anthropic and other providers give an error if there is trailing whitespace) - messages = messages.map(m => ({ ...m, content: m.content.trim() })) + const messages = cleanMessages(messages_) // only captures number of messages and message "shape", no actual code, instructions, prompts, etc const captureChatEvent = (eventId: string, extras?: object) => { @@ -38,6 +71,9 @@ export const sendLLMMessage = ({ 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 })), + ...extras, }) } @@ -84,6 +120,7 @@ export const sendLLMMessage = ({ break; case 'openAI': case 'openRouter': + case 'deepseek': case 'openAICompatible': sendOpenAIMsg({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName }); break; diff --git a/src/vs/platform/void/electron-main/metricsMainService.ts b/src/vs/platform/void/electron-main/metricsMainService.ts index fdfb1d16..cd8abf2b 100644 --- a/src/vs/platform/void/electron-main/metricsMainService.ts +++ b/src/vs/platform/void/electron-main/metricsMainService.ts @@ -9,84 +9,123 @@ import { generateUuid } from '../../../base/common/uuid.js'; import { IEnvironmentMainService } from '../../environment/electron-main/environmentMainService.js'; import { IProductService } from '../../product/common/productService.js'; -import { IStorageMainService } from '../../storage/electron-main/storageMainService.js'; +import { StorageScope, StorageTarget } from '../../storage/common/storage.js'; +import { IApplicationStorageMainService } from '../../storage/electron-main/storageMainService.js'; import { IMetricsService } from '../common/metricsService.js'; import { PostHog } from 'posthog-node' const os = isWindows ? 'windows' : isMacintosh ? 'mac' : isLinux ? 'linux' : null +const _getOSInfo = () => { + try { + const { platform, arch } = process // see platform.ts + return { platform, arch } + } + catch (e) { + return { osInfo: { platform: '??', arch: '??' } } + } +} +const osInfo = _getOSInfo() -const VOID_MACHINE_STORAGE_KEY = 'void.machineId' +// we'd like to use devDeviceId on telemetryService, but that gets sanitized by the time it gets here as 'someValue.devDeviceId' export class MetricsMainService extends Disposable implements IMetricsService { _serviceBrand: undefined; private readonly client: PostHog - private readonly _initProperties: object + private _initProperties: object = {} - // TODO we should eventually identify people based on email - private get machineId() { - const currVal = this._storageService.applicationStorage.get(VOID_MACHINE_STORAGE_KEY) + // helper - looks like this is stored in a .vscdb file in ~/Library/Application Support/Void + private _memoStorage(key: string, target: StorageTarget, setValIfNotExist?: string) { + const currVal = this._appStorage.get(key, StorageScope.APPLICATION) if (currVal !== undefined) return currVal - const newVal = generateUuid() - this._storageService.applicationStorage.set(VOID_MACHINE_STORAGE_KEY, newVal) + const newVal = setValIfNotExist ?? generateUuid() + this._appStorage.store(key, newVal, StorageScope.APPLICATION, target) return newVal } + // this is old, eventually we can just delete this since all the keys will have been transferred over + // returns 'NULL' or the old key + private get oldId() { + // check new storage key first + const newKey = 'void.app.oldMachineId' + const newOldId = this._appStorage.get(newKey, StorageScope.APPLICATION) + if (newOldId) return newOldId + + // put old key into new key if didn't already + const oldValue = this._appStorage.get('void.machineId', StorageScope.APPLICATION) ?? 'NULL' // the old way of getting the key + this._appStorage.store(newKey, oldValue, StorageScope.APPLICATION, StorageTarget.MACHINE) + return oldValue + + // in a few weeks we can replace above with this + // private get oldId() { + // return this._memoStorage('void.app.oldMachineId', StorageTarget.MACHINE, 'NULL') + // } + } + + + // the main id + private get distinctId() { + const oldId = this.oldId + const setValIfNotExist = oldId === 'NULL' ? undefined : oldId + return this._memoStorage('void.app.machineId', StorageTarget.MACHINE, setValIfNotExist) + } + + // just to see if there are ever multiple machineIDs per userID (instead of this, we should just track by the user's email) + private get userId() { + return this._memoStorage('void.app.userMachineId', StorageTarget.USER) + } + constructor( @IProductService private readonly _productService: IProductService, - @IStorageMainService private readonly _storageService: IStorageMainService, @IEnvironmentMainService private readonly _envMainService: IEnvironmentMainService, + @IApplicationStorageMainService private readonly _appStorage: IApplicationStorageMainService, ) { super() this.client = new PostHog('phc_UanIdujHiLp55BkUTjB1AuBXcasVkdqRwgnwRlWESH2', { host: 'https://us.i.posthog.com', }) - // we'd like to use devDeviceId on telemetryService, but that gets sanitized by the time it gets here as 'someValue.devDeviceId' + this.initialize() // async + } + + async initialize() { + // very important to await whenReady! + await this._appStorage.whenReady const { commit, version, quality } = this._productService const isDevMode = !this._envMainService.isBuilt // found in abstractUpdateService.ts - // custom properties we identify this._initProperties = { commit, - version, + vscodeVersion: version, os, quality, - distinctId: this.machineId, + distinctId: this.distinctId, + distinctIdUser: this.userId, + oldId: this.oldId, isDevMode, - ...this._getOSInfo(), + ...osInfo, } const identifyMessage = { - distinctId: this.machineId, + distinctId: this.distinctId, properties: this._initProperties, } this.client.identify(identifyMessage) console.log('Void posthog metrics info:', JSON.stringify(identifyMessage, null, 2)) - } - _getOSInfo() { - try { - const { platform, arch } = process // see platform.ts - return { platform, arch } - } - catch (e) { - return { osInfo: { platform: '??', arch: '??' } } - } - } capture: IMetricsService['capture'] = (event, params) => { - const capture = { distinctId: this.machineId, event, properties: params } as const + const capture = { distinctId: this.distinctId, event, properties: params } as const // console.log('full capture:', capture) this.client.capture(capture) } diff --git a/src/vs/workbench/browser/parts/editor/editorGroupWatermark.ts b/src/vs/workbench/browser/parts/editor/editorGroupWatermark.ts index 787632ae..42f4a7a7 100644 --- a/src/vs/workbench/browser/parts/editor/editorGroupWatermark.ts +++ b/src/vs/workbench/browser/parts/editor/editorGroupWatermark.ts @@ -146,25 +146,6 @@ export class EditorGroupWatermark extends Disposable { private render(): void { - // const enabled = this.configurationService.getValue('workbench.tips.enabled'); - - // if (enabled === this.enabled) { - // return; - // } - - // this.enabled = enabled; - - - // if (!enabled) { - // return; - // } - - // const hasFolder = this.workbenchState !== WorkbenchState.EMPTY; - // const selected = (hasFolder ? folderEntries : noFolderEntries) - // .filter(entry => !('when' in entry) || this.contextKeyService.contextMatchesRules(entry.when)) - // .filter(entry => !('mac' in entry) || entry.mac === (isMacintosh && !isWeb)) - // .filter(entry => !!CommandsRegistry.getCommand(entry.id)) - // .filter(entry => !!this.keybindingService.lookupKeybinding(entry.id)); this.clear(); const voidIconBox = append(this.shortcuts, $('.watermark-box')); @@ -176,6 +157,10 @@ export class EditorGroupWatermark extends Disposable { const update = async () => { + // put async at top so don't need to wait (this prevents a jitter on load) + const recentlyOpened = await this.workspacesService.getRecentlyOpened() + .catch(() => ({ files: [], workspaces: [] })).then(w => w.workspaces); + clearNode(voidIconBox); clearNode(recentsBox); @@ -206,10 +191,6 @@ export class EditorGroupWatermark extends Disposable { // Recents - const recentlyOpened = await this.workspacesService.getRecentlyOpened() - .catch(() => ({ files: [], workspaces: [] })).then(w => w.workspaces); - - if (recentlyOpened.length !== 0) { voidIconBox.append( diff --git a/src/vs/workbench/contrib/void/browser/autocompleteService.ts b/src/vs/workbench/contrib/void/browser/autocompleteService.ts index 9237c7ca..a7516be2 100644 --- a/src/vs/workbench/contrib/void/browser/autocompleteService.ts +++ b/src/vs/workbench/contrib/void/browser/autocompleteService.ts @@ -1,760 +1,826 @@ -/*-------------------------------------------------------------------------------------- - * 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'; - -// The extension this was called from is here - https://github.com/voideditor/void/blob/autocomplete/extensions/void/src/extension/extension.ts - - -/* -A summary of autotab: - -Postprocessing --one common problem for all models is outputting unbalanced parentheses -we solve this by trimming all extra closing parentheses from the generated string -in future, should make sure parentheses are always balanced - --another problem is completing the middle of a string, eg. "const [x, CURSOR] = useState()" -we complete up to first matchup character -but should instead complete the whole line / block (difficult because of parenthesis accuracy) - --too much info is bad. usually we want to show the user 1 line, and have a preloaded response afterwards -this should happen automatically with caching system -should break preloaded responses into \n\n chunks - -Preprocessing -- we don't generate if cursor is at end / beginning of a line (no spaces) -- we generate 1 line if there is text to the right of cursor -- we generate 1 line if variable declaration -- (in many cases want to show 1 line but generate multiple) - -State -- cache based on prefix (and do some trimming first) -- when press tab on one line, should have an immediate followup response -to do this, show autocompletes before they're fully finished -- [todo] remove each autotab when accepted -!- [todo] provide type information - -Details --generated results are trimmed up to 1 leading/trailing space --prefixes are cached up to 1 trailing newline -- -*/ - -class LRUCache { - public items: Map; - private keyOrder: K[]; - private maxSize: number; - private disposeCallback?: (value: V, key?: K) => void; - - constructor(maxSize: number, disposeCallback?: (value: V, key?: K) => void) { - if (maxSize <= 0) throw new Error('Cache size must be greater than 0'); - - this.items = new Map(); - this.keyOrder = []; - this.maxSize = maxSize; - this.disposeCallback = disposeCallback; - } - - set(key: K, value: V): void { - // If key exists, remove it from the order list - if (this.items.has(key)) { - this.keyOrder = this.keyOrder.filter(k => k !== key); - } - // If cache is full, remove least recently used item - else if (this.items.size >= this.maxSize) { - const key = this.keyOrder[0]; - const value = this.items.get(key); - - // Call dispose callback if it exists - if (this.disposeCallback && value !== undefined) { - this.disposeCallback(value, key); - } - - this.items.delete(key); - this.keyOrder.shift(); - } - - // Add new item - this.items.set(key, value); - this.keyOrder.push(key); - } - - delete(key: K): boolean { - const value = this.items.get(key); - - if (value !== undefined) { - // Call dispose callback if it exists - if (this.disposeCallback) { - this.disposeCallback(value, key); - } - - this.items.delete(key); - this.keyOrder = this.keyOrder.filter(k => k !== key); - return true; - } - - return false; - } - - clear(): void { - // Call dispose callback for all items if it exists - if (this.disposeCallback) { - for (const [key, value] of this.items.entries()) { - this.disposeCallback(value, key); - } - } - - this.items.clear(); - this.keyOrder = []; - } - - get size(): number { - return this.items.size; - } - - has(key: K): boolean { - return this.items.has(key); - } -} - -type AutocompletionStatus = 'pending' | 'finished' | 'error'; -type Autocompletion = { - id: number, - prefix: string, - suffix: string, - startTime: number, - endTime: number | undefined, - status: AutocompletionStatus, - llmPromise: Promise | undefined, - insertText: string, - requestId: string | null, -} - -const DEBOUNCE_TIME = 500 -const TIMEOUT_TIME = 60000 -const MAX_CACHE_SIZE = 20 -const MAX_PENDING_REQUESTS = 2 - -// postprocesses the result -const postprocessResult = (result: string) => { - - // trim all whitespace except for a single leading/trailing space - // return result.trim() - - const hasLeadingSpace = result.startsWith(' '); - const hasTrailingSpace = result.endsWith(' '); - return (hasLeadingSpace ? ' ' : '') - + result.trim() - + (hasTrailingSpace ? ' ' : ''); - -} - - -// trims the end of the prefix to improve cache hit rate -const removeLeftTabsAndTrimEnd = (s: string): string => { - const trimmedString = s.trimEnd(); - const trailingEnd = s.slice(trimmedString.length); - - // keep only a single trailing newline - if (trailingEnd.includes('\n')) { - s = trimmedString + '\n'; - } - - s = s.replace(/^\s+/gm, ''); // remove left tabs - - return s; -} - - - -function getStringUpToUnbalancedParenthesis(s: string, prefix: string): string { - - const pairs: Record = { ')': '(', '}': '{', ']': '[' }; - - // process all bracets in prefix - let stack: string[] = [] - const firstOpenIdx = prefix.search(/[[({]/); - if (firstOpenIdx !== -1) { - const brackets = prefix.slice(firstOpenIdx).split('').filter(c => '()[]{}'.includes(c)); - - for (const bracket of brackets) { - if (bracket === '(' || bracket === '{' || bracket === '[') { - stack.push(bracket); - } else { - if (stack.length > 0 && stack[stack.length - 1] === pairs[bracket]) { - stack.pop(); - } else { - stack.push(bracket); - } - } - } - } - - // iterate through each character - for (let i = 0; i < s.length; i++) { - const char = s[i]; - - if (char === '(' || char === '{' || char === '[') { stack.push(char); } - else if (char === ')' || char === '}' || char === ']') { - if (stack.length === 0 || stack.pop() !== pairs[char]) { return s.substring(0, i); } - } - } - return s; -} - - -const parenthesisChars = `{}()[]<>\`'"` - -// returns the text in the autocompletion to display, assuming the prefix is already matched -const toInlineCompletions = ({ matchInfo, prefix, suffix, autocompletion, position, debug }: { matchInfo: matchInfo, prefix: string, suffix: string, autocompletion: Autocompletion, position: Position, debug?: boolean }): { insertText: string, range: Range }[] => { - - - const suffixLines = suffix.split('\n') - const prefixLines = prefix.split('\n') - const suffixToTheRightOfCursor = suffixLines[0] - const prefixToTheLeftOfCursor = prefixLines[prefixLines.length - 1] - const generatedMiddle = autocompletion.insertText - - let startIdx = matchInfo.startIdx - let endIdx = generatedMiddle.length // exclusive bounds - - // const naiveReturnValue = generatedMiddle.slice(startIdx) - // console.log('naiveReturnValue: ', JSON.stringify(naiveReturnValue)) - // return [{ insertText: naiveReturnValue, }] - - // do postprocessing for better ux - // this is a bit hacky but may change a lot - - // if there is space at the start of the completion and user has added it, remove it - const charToLeftOfCursor = prefixToTheLeftOfCursor.slice(-1)[0] || '' - const userHasAddedASpace = charToLeftOfCursor === ' ' || charToLeftOfCursor === '\t' - const rawFirstNonspaceIdx = generatedMiddle.slice(startIdx).search(/[^\t ]/) - if (rawFirstNonspaceIdx > -1 && userHasAddedASpace) { - const firstNonspaceIdx = rawFirstNonspaceIdx + startIdx; - // console.log('p0', startIdx, rawFirstNonspaceIdx) - startIdx = Math.max(startIdx, firstNonspaceIdx) - } - - // if user is on a blank line and the generation starts with newline(s), remove them - const numStartingNewlines = generatedMiddle.slice(startIdx).match(/^\n+/)?.[0].length || 0; - if ( - !prefixToTheLeftOfCursor.trim() - && !suffixToTheRightOfCursor.trim() - && numStartingNewlines > 0 - ) { - // console.log('p1', numStartingNewlines) - startIdx += numStartingNewlines - } - - // if the generated text matches with the suffix on the current line, stop - if (suffixToTheRightOfCursor.trim()) { // completing in the middle of a line - // complete until there is a match - const rawMatchIndex = generatedMiddle.slice(startIdx).lastIndexOf(suffixToTheRightOfCursor.trim()[0]) - if (rawMatchIndex > -1) { - // console.log('p2', rawMatchIndex, startIdx, suffixToTheRightOfCursor.trim()[0], 'AAA', generatedMiddle.slice(startIdx)) - const matchIdx = rawMatchIndex + startIdx; - const matchChar = generatedMiddle[matchIdx] - if (parenthesisChars.includes(matchChar)) { - endIdx = Math.min(endIdx, matchIdx) - } - } - } - - const restOfLineToGenerate = generatedMiddle.slice(startIdx).split('\n')[0] ?? '' - // condition to complete as a single line completion - if ( - prefixToTheLeftOfCursor.trim() - && !suffixToTheRightOfCursor.trim() - && restOfLineToGenerate.trim() - ) { - - const rawNewlineIdx = generatedMiddle.slice(startIdx).indexOf('\n') - if (rawNewlineIdx > -1) { - // console.log('p3', startIdx, rawNewlineIdx) - const newlineIdx = rawNewlineIdx + startIdx; - endIdx = Math.min(endIdx, newlineIdx) - } - } - - // // if a generated line matches with a suffix line, stop - // if (suffixLines.length > 1) { - // console.log('4') - // const lines = [] - // for (const generatedLine of generatedLines) { - // if (suffixLines.slice(0, 10).some(suffixLine => - // generatedLine.trim() !== '' && suffixLine.trim() !== '' - // && generatedLine.trim().startsWith(suffixLine.trim()) - // )) break; - // lines.push(generatedLine) - // } - // endIdx = lines.join('\n').length // this is hacky, remove or refactor in future - // } - - // console.log('pFinal', startIdx, endIdx) - let completionStr = generatedMiddle.slice(startIdx, endIdx) - - // filter out unbalanced parentheses - completionStr = getStringUpToUnbalancedParenthesis(completionStr, prefix) - // console.log('originalCompletionStr: ', JSON.stringify(generatedMiddle.slice(startIdx))) - // console.log('finalCompletionStr: ', JSON.stringify(completionStr)) - - let rangeToReplace: Range = new Range(position.lineNumber, position.column, position.lineNumber, position.column) - - return [{ - insertText: completionStr, - range: rangeToReplace, - }] - -} - - - - - -// returns whether this autocompletion is in the cache -// const doesPrefixMatchAutocompletion = ({ prefix, autocompletion }: { prefix: string, autocompletion: Autocompletion }): boolean => { - -// const originalPrefix = autocompletion.prefix -// const generatedMiddle = autocompletion.result -// const originalPrefixTrimmed = trimPrefix(originalPrefix) -// const currentPrefixTrimmed = trimPrefix(prefix) - -// if (currentPrefixTrimmed.length < originalPrefixTrimmed.length) { -// return false +// /*-------------------------------------------------------------------------------------- +// * 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'; + +// // The extension this was called from is here - https://github.com/voideditor/void/blob/autocomplete/extensions/void/src/extension/extension.ts + + +// /* +// A summary of autotab: + +// Postprocessing +// -one common problem for all models is outputting unbalanced parentheses +// we solve this by trimming all extra closing parentheses from the generated string +// in future, should make sure parentheses are always balanced + +// -another problem is completing the middle of a string, eg. "const [x, CURSOR] = useState()" +// we complete up to first matchup character +// but should instead complete the whole line / block (difficult because of parenthesis accuracy) + +// -too much info is bad. usually we want to show the user 1 line, and have a preloaded response afterwards +// this should happen automatically with caching system +// should break preloaded responses into \n\n chunks + +// Preprocessing +// - we don't generate if cursor is at end / beginning of a line (no spaces) +// - we generate 1 line if there is text to the right of cursor +// - we generate 1 line if variable declaration +// - (in many cases want to show 1 line but generate multiple) + +// State +// - cache based on prefix (and do some trimming first) +// - when press tab on one line, should have an immediate followup response +// to do this, show autocompletes before they're fully finished +// - [todo] remove each autotab when accepted +// !- [todo] provide type information + +// Details +// -generated results are trimmed up to 1 leading/trailing space +// -prefixes are cached up to 1 trailing newline +// - +// */ + +// class LRUCache { +// public items: Map; +// private keyOrder: K[]; +// private maxSize: number; +// private disposeCallback?: (value: V, key?: K) => void; + +// constructor(maxSize: number, disposeCallback?: (value: V, key?: K) => void) { +// if (maxSize <= 0) throw new Error('Cache size must be greater than 0'); + +// this.items = new Map(); +// this.keyOrder = []; +// this.maxSize = maxSize; +// this.disposeCallback = disposeCallback; // } -// const isMatch = (originalPrefixTrimmed + generatedMiddle).startsWith(currentPrefixTrimmed) -// return isMatch +// 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 getPrefixAndSuffix = (model: ITextModel, position: Position) => { - const fullText = model.getValue(); +// // 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 cursorOffset = model.getOffsetAt(position) - const prefix = fullText.substring(0, cursorOffset) - const suffix = fullText.substring(cursorOffset) +// // keep only a single trailing newline +// if (trailingEnd.includes('\n')) { +// s = trimmedString + '\n'; +// } - return { prefix, suffix } +// s = s.replace(/^\s+/gm, ''); // remove left tabs -} +// return s; +// } -const getIndex = (str: string, line: number, char: number) => { - return str.split('\n').slice(0, line).join('\n').length + (line > 0 ? 1 : 0) + char; -} -const getLastLine = (s: string): string => { - const matches = s.match(/[^\n]*$/) - return matches ? matches[0] : '' -} -type matchInfo = { - lineStart: number, - character: number, - startIdx: number, -} -// returns the startIdx of the match if there is a match, or undefined if there is no match -// all results are wrt `autocompletion.result` -const getPrefixAutocompletionMatch = ({ prefix, autocompletion }: { prefix: string, autocompletion: Autocompletion }): matchInfo | undefined => { - const trimmedCurrentPrefix = removeLeftTabsAndTrimEnd(prefix) - const trimmedCompletionPrefix = removeLeftTabsAndTrimEnd(autocompletion.prefix) - const trimmedCompletionMiddle = removeLeftTabsAndTrimEnd(autocompletion.insertText) +// 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() +// ) { - // 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 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) - if (trimmedCurrentPrefix.length < trimmedCompletionPrefix.length) { // user must write text beyond the original prefix at generation time - console.log('@undefined1') - return undefined - } +// // filter out unbalanced parentheses +// completionStr = getStringUpToUnbalancedParenthesis(completionStr, prefix) +// // console.log('originalCompletionStr: ', JSON.stringify(generatedMiddle.slice(startIdx))) +// // console.log('finalCompletionStr: ', JSON.stringify(completionStr)) + + +// return completionStr - if ( // check that completion starts with the prefix - !(trimmedCompletionPrefix + trimmedCompletionMiddle) - .startsWith(trimmedCurrentPrefix) - ) { - console.log('@undefined2') - return undefined - } +// } + +// // 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 + +// // } - // reverse map to find position wrt `autocompletion.result` - const lineStart = - trimmedCurrentPrefix.split('\n').length - - trimmedCompletionPrefix.split('\n').length; - if (lineStart < 0) { - console.log('@undefined3') +// type PrefixAndSuffixInfo = { prefix: string, suffix: string, prefixLines: string[], suffixLines: string[], prefixToTheLeftOfCursor: string, suffixToTheRightOfCursor: string } +// const getPrefixAndSuffixInfo = (model: ITextModel, position: Position): PrefixAndSuffixInfo => { - 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 +// const fullText = model.getValue(); - // console.log('currentPrefixLine', currentPrefixLine) - // console.log('completionPrefixLine', completionPrefixLine) - // console.log('completionMiddleLine', completionMiddleLine) +// const cursorOffset = model.getOffsetAt(position) +// const prefix = fullText.substring(0, cursorOffset) +// const suffix = fullText.substring(cursorOffset) - const charMatchIdx = fullCompletionLine.indexOf(currentPrefixLine) - if (charMatchIdx < 0) { - console.log('@undefined4', charMatchIdx) +// const prefixLines = prefix.split('\n') +// const suffixLines = suffix.split('\n') - console.error('Warning: Found character with negative index. This should never happen.') - return undefined - } +// const prefixToTheLeftOfCursor = prefixLines.slice(-1)[0] ?? '' +// const suffixToTheRightOfCursor = suffixLines[0] ?? '' - const character = (charMatchIdx + - currentPrefixLine.length - - completionPrefixLine.length - ) +// return { prefix, suffix, prefixLines, suffixLines, prefixToTheLeftOfCursor, suffixToTheRightOfCursor } - const startIdx = getIndex(autocompletion.insertText, lineStart, character) +// } - return { - lineStart, - character, - startIdx, - } +// const getIndex = (str: string, line: number, char: number) => { +// return str.split('\n').slice(0, line).join('\n').length + (line > 0 ? 1 : 0) + char; +// } +// const getLastLine = (s: string): string => { +// const matches = s.match(/[^\n]*$/) +// return matches ? matches[0] : '' +// } +// type 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) +// // console.log('@result: ', JSON.stringify(autocompletion.insertText)) +// // console.log('@trimmedCurrentPrefix: ', JSON.stringify(trimmedCurrentPrefix)) +// // console.log('@trimmedCompletionPrefix: ', JSON.stringify(trimmedCompletionPrefix)) +// // console.log('@trimmedCompletionMiddle: ', JSON.stringify(trimmedCompletionMiddle)) +// if (trimmedCurrentPrefix.length < trimmedCompletionPrefix.length) { // user must write text beyond the original prefix at generation time +// console.log('@undefined1') +// return undefined +// } +// if ( // check that completion starts with the prefix +// !(trimmedCompletionPrefix + trimmedCompletionMiddle) +// .startsWith(trimmedCurrentPrefix) +// ) { +// console.log('@undefined2') +// return undefined +// } -const getCompletionOptions = ({ prefix, suffix }: { prefix: string, suffix: string }) => { +// // reverse map to find position wrt `autocompletion.result` +// const lineStart = +// trimmedCurrentPrefix.split('\n').length - +// trimmedCompletionPrefix.split('\n').length; - const prefixLines = prefix.split('\n') - const suffixLines = suffix.split('\n') +// if (lineStart < 0) { +// console.log('@undefined3') - const prefixToLeftOfCursor = prefixLines.slice(-1)[0] ?? '' - const suffixToRightOfCursor = suffixLines[0] ?? '' +// 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 - // default parameters - let shouldGenerate = true - let stopTokens: string[] = ['\n\n', '\r\n\r\n'] +// // console.log('currentPrefixLine', currentPrefixLine) +// // console.log('completionPrefixLine', completionPrefixLine) +// // console.log('completionMiddleLine', completionMiddleLine) - // specific cases - if (suffixToRightOfCursor.trim() !== '') { // typing between something - stopTokens = ['\n', '\r\n'] - } +// const charMatchIdx = fullCompletionLine.indexOf(currentPrefixLine) +// if (charMatchIdx < 0) { +// console.log('@undefined4', charMatchIdx) - // if (prefixToLeftOfCursor.trim() === '' && suffixToRightOfCursor.trim() === '') { // at an empty line - // stopTokens = ['\n\n', '\r\n\r\n'] - // } +// console.error('Warning: Found character with negative index. This should never happen.') +// return undefined +// } - if (prefixToLeftOfCursor === '') { // at beginning or end of line - shouldGenerate = false - } +// const character = (charMatchIdx + +// currentPrefixLine.length +// - completionPrefixLine.length +// ) - return { shouldGenerate, stopTokens } +// const startIdx = getIndex(autocompletion.insertText, lineStart, character) -} +// return { +// startLine: lineStart, +// startCharacter: character, +// startIdx, +// } +// } -export interface IAutocompleteService { - readonly _serviceBrand: undefined; -} -export const IAutocompleteService = createDecorator('AutocompleteService'); -export class AutocompleteService extends Disposable implements IAutocompleteService { - _serviceBrand: undefined; +// const getCompletionOptions = (prefixAndSuffix: PrefixAndSuffixInfo) => { + +// const { prefix, suffix, prefixToTheLeftOfCursor, suffixToTheRightOfCursor, suffixLines } = prefixAndSuffix - private _autocompletionId: number = 0; - private _autocompletionsOfDocument: { [docUriStr: string]: LRUCache } = {} +// // single line prediction unless the current line is blank +// let predictionType: Autocompletion['type'] +// let llmPrefix = prefix +// let llmSuffix = suffix - private _lastCompletionTime = 0 - private _lastPrefix: string = '' +// 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' +// } - // 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 { +// // default parameters +// let shouldGenerate = true +// let stopTokens: string[] = ['\n\n', '\r\n\r\n'] // default to multi-line prediction - const disabled = true - const testMode = false - - if (disabled) return []; - - const docUriStr = model.uri.toString(); - - const { prefix, suffix } = getPrefixAndSuffix(model, position) - // initialize cache and other variables - // note that whenever an autocompletion is rejected, it is removed from cache - if (!this._autocompletionsOfDocument[docUriStr]) { - this._autocompletionsOfDocument[docUriStr] = new LRUCache( - MAX_CACHE_SIZE, - (autocompletion: Autocompletion) => { - if (autocompletion.requestId) - this._llmMessageService.abort(autocompletion.requestId) - } - ) - } - this._lastPrefix = prefix +// // 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 +// } - // print all pending autocompletions - // let _numPending = 0 - // this._autocompletionsOfDocument[docUriStr].items.forEach((a: Autocompletion) => { if (a.status === 'pending') _numPending += 1 }) - // console.log('@numPending: ' + _numPending) - - // get autocompletion from cache - let cachedAutocompletion: Autocompletion | undefined = undefined - let matchInfo: matchInfo | undefined = undefined - for (const autocompletion of this._autocompletionsOfDocument[docUriStr].items.values()) { - // if the user's change matches up with the generated text - matchInfo = getPrefixAutocompletionMatch({ prefix, autocompletion }) - if (matchInfo !== undefined) { - cachedAutocompletion = autocompletion - break; - } - } +// // Don't generate if at the very beginning of a line +// if (prefixToTheLeftOfCursor === '') { +// shouldGenerate = false +// } - // if there is a cached autocompletion, return it - if (cachedAutocompletion && matchInfo) { - - // console.log('id: ' + cachedAutocompletion.id) - - if (cachedAutocompletion.status === 'finished') { - // console.log('A1') - - const inlineCompletions = toInlineCompletions({ matchInfo, autocompletion: cachedAutocompletion, prefix, suffix, position, debug: true }) - return inlineCompletions - - } else if (cachedAutocompletion.status === 'pending') { - // console.log('A2') - - try { - await cachedAutocompletion.llmPromise; - const inlineCompletions = toInlineCompletions({ matchInfo, autocompletion: cachedAutocompletion, prefix, suffix, position }) - return inlineCompletions - - } catch (e) { - this._autocompletionsOfDocument[docUriStr].delete(cachedAutocompletion.id) - console.error('Error creating autocompletion (1): ' + e) - } - - } else if (cachedAutocompletion.status === 'error') { - // console.log('A3') - } - - return [] - } - - // else if no more typing happens, then go forwards with the request - // wait DEBOUNCE_TIME for the user to stop typing - const thisTime = Date.now() - this._lastCompletionTime = thisTime - const didTypingHappenDuringDebounce = await new Promise((resolve, reject) => - setTimeout(() => { - if (this._lastCompletionTime === thisTime) { - resolve(false) - } else { - resolve(true) - } - }, DEBOUNCE_TIME) - ) - - // if more typing happened, then do not go forwards with the request - if (didTypingHappenDuringDebounce) { - return [] - } - - - // if there are too many pending requests, cancel the oldest one - let numPending = 0 - let oldestPending: Autocompletion | undefined = undefined - for (const autocompletion of this._autocompletionsOfDocument[docUriStr].items.values()) { - if (autocompletion.status === 'pending') { - numPending += 1 - if (oldestPending === undefined) { - oldestPending = autocompletion - } - if (numPending >= MAX_PENDING_REQUESTS) { - // cancel the oldest pending request and remove it from cache - this._autocompletionsOfDocument[docUriStr].delete(oldestPending.id) - break - } - } - } - - const { shouldGenerate, stopTokens: _ } = getCompletionOptions({ prefix, suffix }) // TODO mat - - if (!shouldGenerate) return [] - - if (testMode && this._autocompletionId !== 0) { // TODO remove this - return [] - } - - // console.log('B') - - // create a new autocompletion and add it to cache - const newAutocompletion: Autocompletion = { - id: this._autocompletionId++, - prefix: prefix, - suffix: suffix, - startTime: Date.now(), - endTime: undefined, - status: 'pending', - llmPromise: undefined, - insertText: '', - requestId: null, - } - - // set parameters of `newAutocompletion` appropriately - newAutocompletion.llmPromise = new Promise((resolve, reject) => { - - const requestId = this._llmMessageService.sendLLMMessage({ - 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 (!getPrefixAutocompletionMatch({ prefix: this._lastPrefix, autocompletion: newAutocompletion })) { - reject('LLM response did not match user\'s text.') - } - }, - onFinalMessage: ({ fullText }) => { - - // newAutocompletion.prefix = prefix - // newAutocompletion.suffix = suffix - // newAutocompletion.startTime = Date.now() - newAutocompletion.endTime = Date.now() - // newAutocompletion.abortRef = { current: () => { } } - newAutocompletion.status = 'finished' - // newAutocompletion.promise = undefined - 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 matchInfo: matchInfo = { startIdx: 0, lineStart: 0, character: 0 } - const inlineCompletions = toInlineCompletions({ matchInfo, autocompletion: newAutocompletion, prefix, suffix, position }) - return inlineCompletions - - } catch (e) { - this._autocompletionsOfDocument[docUriStr].delete(newAutocompletion.id) - console.error('Error creating autocompletion (2): ' + e) - return [] - } - - } - - constructor( - @ILanguageFeaturesService private _langFeatureService: ILanguageFeaturesService, - @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(); - - const { prefix, } = getPrefixAndSuffix(model, position) - - if (!this._autocompletionsOfDocument[docUriStr]) return; - - // go through cached items and remove matching ones - // autocompletion.prefix + autocompletion.insertedText ~== insertedText - completions.items.forEach(item => { - this._autocompletionsOfDocument[docUriStr].items.forEach((autocompletion: Autocompletion) => { - if (removeLeftTabsAndTrimEnd(prefix) - === removeLeftTabsAndTrimEnd(autocompletion.prefix + autocompletion.insertText) - ) { - this._autocompletionsOfDocument[docUriStr].delete(autocompletion.id); - } - }); - }); - - }, - }) - - - } - - -} +// return { shouldGenerate, predictionType, stopTokens, llmPrefix, llmSuffix } +// } -registerSingleton(IAutocompleteService, AutocompleteService, InstantiationType.Eager); + + + +// 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); diff --git a/src/vs/workbench/contrib/void/browser/chatThreadService.ts b/src/vs/workbench/contrib/void/browser/chatThreadService.ts index 7fede377..8c9ad0e5 100644 --- a/src/vs/workbench/contrib/void/browser/chatThreadService.ts +++ b/src/vs/workbench/contrib/void/browser/chatThreadService.ts @@ -13,29 +13,25 @@ import { Emitter, Event } from '../../../../base/common/event.js'; import { IRange } from '../../../../editor/common/core/range.js'; import { ILLMMessageService } from '../../../../platform/void/common/llmMessageService.js'; import { IModelService } from '../../../../editor/common/services/model.js'; -import { VSReadFile } from './helpers/readFile.js'; -import { chat_prompt, chat_systemMessage } from './prompt/prompts.js'; +import { chat_userMessage, chat_systemMessage } from './prompt/prompts.js'; +// one of the square items that indicates a selection in a chat bubble (NOT a file, a Selection of text) export type CodeSelection = { + type: 'Selection'; fileURI: URI; - selectionStr: string | null; - content: string; // TODO remove this (replace `selectionStr` with `content`) + selectionStr: string; range: IRange; } -// if selectionStr is null, it means to use the entire file at send time -export type CodeStagingSelection = { - type: 'Selection', - fileURI: URI, - selectionStr: string, - range: IRange -} | { - type: 'File', - fileURI: URI, - selectionStr: null, - range: null +export type FileSelection = { + type: 'File'; + fileURI: URI; + selectionStr: null; + range: null; } +export type StagingSelectionItem = CodeSelection | FileSelection + // WARNING: changing this format is a big deal!!!!!! need to migrate old format to new format on users' computers so people don't get errors. export type ChatMessage = @@ -43,7 +39,7 @@ export type ChatMessage = role: 'user'; content: string | null; // content sent to the llm - allowed to be '', will be replaced with (empty) displayContent: string | null; // content displayed to user - allowed to be '', will be ignored - selections: CodeSelection[] | null; // the user's selection + selections: StagingSelectionItem[] | null; // the user's selection } | { role: 'assistant'; @@ -69,7 +65,7 @@ export type ChatThreads = { export type ThreadsState = { allThreads: ChatThreads; currentThreadId: string; // intended for internal use only - currentStagingSelections: CodeStagingSelection[] | null; + currentStagingSelections: StagingSelectionItem[] | null; } export type ThreadStreamState = { @@ -91,6 +87,9 @@ const newThreadObject = () => { } satisfies ChatThreads[string] } +const THREAD_VERSION_KEY = 'void.chatThreadVersion' +const THREAD_VERSION = 'v1' + const THREAD_STORAGE_KEY = 'void.chatThreadStorage' export interface IChatThreadService { @@ -106,7 +105,7 @@ export interface IChatThreadService { openNewThread(): void; switchToThread(threadId: string): void; - setStaging(stagingSelection: CodeStagingSelection[] | null): void; + setStaging(stagingSelection: StagingSelectionItem[] | null): void; addUserMessageAndStreamResponse(userMessage: string): Promise; cancelStreaming(threadId: string): void; @@ -143,11 +142,15 @@ class ChatThreadService extends Disposable implements IChatThreadService { // always be in a thread this.openNewThread() + + // for now just write the version, anticipating bigger changes in the future where we'll want to access this + this._storageService.store(THREAD_VERSION_KEY, THREAD_VERSION, StorageScope.APPLICATION, StorageTarget.USER) } private _readAllThreads(): ChatThreads { // PUT ANY VERSION CHANGE FORMAT CONVERSION CODE HERE + // CAN ADD "v0" TAG IN STORAGE AND CONVERT const threads = this._storageService.get(THREAD_STORAGE_KEY, StorageScope.APPLICATION) return threads ? JSON.parse(threads) : {} } @@ -188,15 +191,11 @@ class ChatThreadService extends Disposable implements IChatThreadService { const threadId = this.getCurrentThread().id const currSelns = this.state.currentStagingSelections ?? [] - const selections = !currSelns ? null : await Promise.all( - currSelns.map(async (sel) => ({ ...sel, content: await VSReadFile(this._modelService, sel.fileURI) })) - ).then( - (files) => files.filter(file => file.content !== null) as CodeSelection[] - ) // add user's message to chat history const instructions = userMessage - const userHistoryElt: ChatMessage = { role: 'user', content: chat_prompt(instructions, selections), displayContent: instructions, selections: selections } + const content = await chat_userMessage(instructions, currSelns, this._modelService) + const userHistoryElt: ChatMessage = { role: 'user', content: content, displayContent: instructions, selections: currSelns } this._addMessageToThread(threadId, userHistoryElt) @@ -292,7 +291,7 @@ class ChatThreadService extends Disposable implements IChatThreadService { } - setStaging(stagingSelection: CodeStagingSelection[] | null): void { + setStaging(stagingSelection: StagingSelectionItem[] | null): void { this._setState({ currentStagingSelections: stagingSelection }, true) // this is a hack for now } diff --git a/src/vs/workbench/contrib/void/browser/helperServices/consistentItemService.ts b/src/vs/workbench/contrib/void/browser/helperServices/consistentItemService.ts index 6de0b53b..3165e57f 100644 --- a/src/vs/workbench/contrib/void/browser/helperServices/consistentItemService.ts +++ b/src/vs/workbench/contrib/void/browser/helperServices/consistentItemService.ts @@ -91,10 +91,7 @@ export class ConsistentItemService extends Disposable { this._register(this._editorService.onCodeEditorAdd(editor => { initializeEditor(editor) })) // when an editor is deleted, remove its items - this._register(this._editorService.onCodeEditorRemove(editor => { - removeItemsFromEditor(editor) - })) - + this._register(this._editorService.onCodeEditorRemove(editor => { removeItemsFromEditor(editor) })) } @@ -127,8 +124,6 @@ export class ConsistentItemService extends Disposable { const editorId = editor.getId() this.itemIdsOfEditorId[editorId]?.delete(itemId) - if (this.itemIdsOfEditorId[editorId]?.size === 0) - delete this.itemIdsOfEditorId[editorId] this.disposeFnOfItemId[itemId]?.() delete this.disposeFnOfItemId[itemId] @@ -175,8 +170,6 @@ export class ConsistentItemService extends Disposable { // clear this.consistentItemIdsOfURI[uri.fsPath]?.delete(consistentItemId) - if (this.consistentItemIdsOfURI[uri.fsPath]?.size === 0) - delete this.consistentItemIdsOfURI[uri.fsPath] delete this.infoOfConsistentItemId[consistentItemId] diff --git a/src/vs/workbench/contrib/void/browser/helpers/extractCodeFromResult.ts b/src/vs/workbench/contrib/void/browser/helpers/extractCodeFromResult.ts index d7e109ae..768f4cfc 100644 --- a/src/vs/workbench/contrib/void/browser/helpers/extractCodeFromResult.ts +++ b/src/vs/workbench/contrib/void/browser/helpers/extractCodeFromResult.ts @@ -76,47 +76,55 @@ class SurroundingsRemover { removeCodeBlock = () => { + // Match either: + // 1. ```language\n\n```\n? + // 2. ```\n```\n? + const pm = this const foundCodeBlock = pm.removePrefix('```') if (!foundCodeBlock) return false pm.removeFromStartUntil('\n', true) // language - const foundCodeBlockEnd = pm.removeSuffix('```') + const j = pm.j + let foundCodeBlockEnd = pm.removeSuffix('```') + + if (pm.j === j) foundCodeBlockEnd = pm.removeSuffix('```\n') // if no change, try again with \n after ``` + if (!foundCodeBlockEnd) return false - pm.removeSuffix('\n') + pm.removeSuffix('\n') // remove the newline before ``` return true } - actualRecentlyAdded = (recentlyAddedTextLen: number) => { + deltaInfo = (recentlyAddedTextLen: number) => { // aaaaaatextaaaaaa{recentlyAdded} - // i ^ j + // ^ i j len // | // recentyAddedIdx - const recentlyAddedIdx = this.j - recentlyAddedTextLen + 1 - return this.originalS.substring(Math.max(this.i, recentlyAddedIdx), this.j + 1) + const recentlyAddedIdx = this.originalS.length - recentlyAddedTextLen + const actualDelta = this.originalS.substring(Math.max(this.i, recentlyAddedIdx), this.j + 1) + const ignoredSuffix = this.originalS.substring(Math.max(this.j + 1, recentlyAddedIdx), Infinity) + return [actualDelta, ignoredSuffix] as const } + } -export const extractCodeFromRegular = ({ text, recentlyAddedTextLen }: { text: string, recentlyAddedTextLen: number }): [string, string] => { - // Match either: - // 1. ```language\n``` - // 2. `````` +export const extractCodeFromRegular = ({ text, recentlyAddedTextLen }: { text: string, recentlyAddedTextLen: number }): [string, string, string] => { const pm = new SurroundingsRemover(text) pm.removeCodeBlock() const s = pm.value() - const actual = pm.actualRecentlyAdded(recentlyAddedTextLen) + const [delta, ignoredSuffix] = pm.deltaInfo(recentlyAddedTextLen) - return [s, actual] + return [s, delta, ignoredSuffix] } @@ -124,7 +132,7 @@ export const extractCodeFromRegular = ({ text, recentlyAddedTextLen }: { text: s // Ollama has its own FIM, we should not use this if we use that -export const extractCodeFromFIM = ({ text, recentlyAddedTextLen, midTag, }: { text: string, recentlyAddedTextLen: number, midTag: string }): [string, string] => { +export const extractCodeFromFIM = ({ text, recentlyAddedTextLen, midTag, }: { text: string, recentlyAddedTextLen: number, midTag: string }): [string, string, string] => { /* ------------- summary of the regex ------------- [optional ` | `` | ```] @@ -146,9 +154,9 @@ export const extractCodeFromFIM = ({ text, recentlyAddedTextLen, midTag, }: { te pm.removeSuffix(``) } const s = pm.value() - const actual = pm.actualRecentlyAdded(recentlyAddedTextLen) + const [delta, ignoredSuffix] = pm.deltaInfo(recentlyAddedTextLen) - return [s, actual] + return [s, delta, ignoredSuffix] // // const regex = /[\s\S]*?(?:`{1,3}\s*([a-zA-Z_]+[\w]*)?[\s\S]*?)?([\s\S]*?)(?:<\/MID>|`{1,3}|$)/; diff --git a/src/vs/workbench/contrib/void/browser/inlineDiffsService.ts b/src/vs/workbench/contrib/void/browser/inlineDiffsService.ts index 76bcbd8c..fdbf84bb 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_prompt, ctrlKStream_systemMessage, ctrlLStream_prompt, ctrlLStream_systemMessage, defaultFimTags } from './prompt/prompts.js'; +import { ctrlKStream_prefixAndSuffix, 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' @@ -104,10 +104,9 @@ const getLeadingWhitespacePx = (editor: ICodeEditor, startLine: number): number export type StartApplyingOpts = { featureName: 'Ctrl+K'; diffareaid: number; // id of the CtrlK area (contains text selection) - userMessage: string; // user message } | { featureName: 'Ctrl+L'; - userMessage: string; + applyStr: string; } | { featureName: 'Autocomplete'; range: IRange; @@ -260,6 +259,7 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService { if (!(model.uri.fsPath in this.diffAreasOfURI)) { this.diffAreasOfURI[model.uri.fsPath] = new Set(); } + else return // do not add listeners to the same model twice - important, or will see duplicates // when the user types, realign diff areas and re-render them this._register( @@ -280,7 +280,7 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService { .filter(diffArea => !!diffArea && diffArea.type === 'DiffZone') const isStreaming = diffZones.find(diffZone => !!diffZone._streamState.isStreaming) if (diffZones.length !== 0 && !isStreaming && !removeAcceptRejectAllUI) { - removeAcceptRejectAllUI = this._addAcceptRejectUI(uri) ?? null + removeAcceptRejectAllUI = this._addAcceptRejectAllUI(uri) ?? null } else { removeAcceptRejectAllUI?.() removeAcceptRejectAllUI = null @@ -394,7 +394,7 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService { } } - private _addAcceptRejectUI(uri: URI) { + private _addAcceptRejectAllUI(uri: URI) { // find all diffzones that aren't streaming const diffZones: DiffZone[] = [] @@ -1214,7 +1214,6 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService { let startLine: number let endLine: number let uri: URI - let userMessage: string if (featureName === 'Ctrl+L') { @@ -1231,20 +1230,16 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService { startLine = 1 endLine = numLines - userMessage = opts.userMessage } else if (featureName === 'Ctrl+K') { const { diffareaid } = opts const ctrlKZone = this.diffAreaOfId[diffareaid] if (ctrlKZone.type !== 'CtrlKZone') return - const { startLine: startLine_, endLine: endLine_, _URI, _mountInfo } = ctrlKZone + const { startLine: startLine_, endLine: endLine_, _URI } = ctrlKZone uri = _URI startLine = startLine_ endLine = endLine_ - - if (!_mountInfo?.textAreaRef.current) return - userMessage = _mountInfo.textAreaRef.current?.value } else { throw new Error(`Void: diff.type not recognized on: ${featureName}`) @@ -1295,24 +1290,25 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService { let messages: LLMMessage[] if (featureName === 'Ctrl+L') { - const userContent = ctrlLStream_prompt({ originalCode, userMessage, uri }) + const userContent = fastApply_userMessage({ originalCode, applyStr: opts.applyStr, uri }) messages = [ - { role: 'system', content: ctrlLStream_systemMessage, }, + { role: 'system', content: fastApply_systemMessage, }, { role: 'user', content: userContent, } ] } else if (featureName === 'Ctrl+K') { - const { prefix, suffix } = ctrlKStream_prefixAndSuffix({ fullFileStr: currentFileStr, startLine, endLine }) - // console.log('PREFIX:\n', prefix) - // console.log('SUFFIX:\n', suffix) - // console.log('USER CONTENT:\n', userContent) + const { diffareaid } = opts + const ctrlKZone = this.diffAreaOfId[diffareaid] + if (ctrlKZone.type !== 'CtrlKZone') return + const { _mountInfo } = ctrlKZone + const instructions = _mountInfo?.textAreaRef.current?.value ?? '' - // __TODO__ use Ollama's FIM api - // if (isOllamaFIM) {...} else: + // __TODO__ use Ollama's FIM api, if (isOllamaFIM) {...} else: + const { prefix, suffix } = ctrlKStream_prefixAndSuffix({ fullFileStr: currentFileStr, startLine, endLine }) const language = filenameToVscodeLanguage(uri.fsPath) ?? '' - const userContent = ctrlKStream_prompt({ selection: originalCode, userMessage, prefix, suffix, isOllamaFIM: false, fimTags: modelFimTags, language }) + const userContent = ctrlKStream_userMessage({ selection: originalCode, instructions: instructions, prefix, suffix, isOllamaFIM: false, fimTags: modelFimTags, language }) messages = [ - { role: 'system', content: ctrlKStream_systemMessage, }, + { role: 'system', content: ctrlKStream_systemMessage({ fimTags: modelFimTags }), }, { role: 'user', content: userContent, } ] } @@ -1354,15 +1350,25 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService { } const latestStreamInfo = { line: diffZone.startLine, addedSplitYet: false, col: 1, originalCodeStartLine: 1 } + + // state used in onText: + let fullText = '' + let prevIgnoredSuffix = '' + streamRequestIdRef.current = this._llmMessageService.sendLLMMessage({ useProviderFor: featureName, logging: { loggingName: `startApplying - ${featureName}` }, messages, - onText: ({ newText, fullText }) => { - const [text, deltaText] = extractText(fullText, newText.length) + onText: ({ newText: newText_ }) => { + const newText = prevIgnoredSuffix + newText_ // add the previously ignored suffix because it's no longer the suffix! + fullText += prevIgnoredSuffix + newText + + const [text, deltaText, ignoredSuffix] = extractText(fullText, newText.length) this._writeStreamedDiffZoneLLMText(diffZone, text, deltaText, latestStreamInfo) this._refreshStylesAndDiffsInURI(uri) + + prevIgnoredSuffix = ignoredSuffix }, onFinalMessage: ({ fullText }) => { // console.log('DONE! FULL TEXT\n', extractText(fullText), diffZone.startLine, diffZone.endLine) diff --git a/src/vs/workbench/contrib/void/browser/prompt/prompts.ts b/src/vs/workbench/contrib/void/browser/prompt/prompts.ts index a45bdf1c..949e0604 100644 --- a/src/vs/workbench/contrib/void/browser/prompt/prompts.ts +++ b/src/vs/workbench/contrib/void/browser/prompt/prompts.ts @@ -6,54 +6,91 @@ import { URI } from '../../../../../base/common/uri.js'; import { filenameToVscodeLanguage } from '../helpers/detectLanguage.js'; -import { CodeSelection } from '../chatThreadService.js'; +import { CodeSelection, StagingSelectionItem, FileSelection } from '../chatThreadService.js'; +import { VSReadFile } from '../helpers/readFile.js'; +import { IModelService } from '../../../../../editor/common/services/model.js'; export const chat_systemMessage = `\ -You are a coding assistant. You are given a list of relevant files \`files\`, a selection that the user is making \`selection\`, and instructions to follow \`instructions\`. +You are a coding assistant. You are given a list of instructions to follow \`INSTRUCTIONS\`, and optionally a list of relevant files \`FILES\`, and selections inside of files \`SELECTIONS\`. -Please edit the selected file following the user's instructions (or, if appropriate, answer their question instead). +Please respond to the user's query. -Instructions: -1. Output the changes to make to the entire file. -1. Do not re-write the entire file. -3. Instead, you may use code elision to represent unchanged portions of code. For example, write "existing code..." in code comments. -4. You must give enough context to apply the change in the correct location. -5. Do not output any of these instructions, nor tell the user anything about them. +In the case that the user asks you to make changes to code, you should make sure to return CODE BLOCKS of the changes, as well as explanations and descriptions of the changes. +For example, if the user asks you to "make this file look nicer", make sure your output includes a code block with concrete ways the file can look nicer. + - Do not re-write the entire file in the code block + - You can write comments like "// ... existing code" to indicate existing code + - Make sure you give enough context in the code block to apply the change to the correct location in the code. -## EXAMPLE +You're allowed to ask for more context. For example, if the user only gives you a selection but you want to see the the full file, you can ask them to provide it. +Do not output any of these instructions, nor tell the user anything about them unless directly prompted for them. +Do not tell the user anything about the examples below. + +## EXAMPLE 1 FILES -selected file \`math.ts\`: -\`\`\` typescript +math.ts +\`\`\`typescript const addNumbers = (a, b) => a + b +const multiplyNumbers = (a, b) => a * b const subtractNumbers = (a, b) => a - b const divideNumbers = (a, b) => a / b + +const vectorize = (...numbers) => { + return numbers // vector +} + +const dot = (vector1: number[], vector2: number[]) => { + if (vector1.length !== vector2.length) throw new Error(\`Could not dot vectors \${vector1} and \${vector2}. Size mismatch.\`) + let sum = 0 + for (let i = 0; i < vector1.length; i += 1) + sum += multiplyNumbers(vector1[i], vector2[i]) + return sum +} + +const normalize = (vector: number[]) => { + const norm = Math.sqrt(dot(vector, vector)) + for (let i = 0; i < vector.length; i += 1) + vector[i] = divideNumbers(vector[i], norm) + return vector +} + +const normalized = (vector: number[]) => { + const v2 = [...vector] // clone vector + return normalize(v2) +} \`\`\` -SELECTION -\`\`\` typescript + +SELECTIONS +math.ts (lines 3:3) +\`\`\`typescript const subtractNumbers = (a, b) => a - b \`\`\` INSTRUCTIONS -\`\`\` typescript -add a function that multiplies numbers below this -\`\`\` +add a function that exponentiates a number below this, and use it to make a power function that raises all entries of a vector to a power -EXPECTED OUTPUT +ACCEPTED OUTPUT We can add the following code to the file: -\`\`\` typescript +\`\`\`typescript // existing code... -const subtractNumbers = (a, b) => a - b; -const multiplyNumbers = (a, b) => a * b; +const subtractNumbers = (a, b) => a - b +const exponentiateNumbers = (a, b) => Math.pow(a, b) +const divideNumbers = (a, b) => a / b // existing code... + +const raiseAll = (vector: number[], power: number) => { + for (let i = 0; i < vector.length; i += 1) + vector[i] = exponentiateNumbers(vector[i], power) + return vector +} \`\`\` -## EXAMPLE +## EXAMPLE 2 FILES -selected file \`fib.ts\`: -\`\`\` typescript +fib.ts +\`\`\`typescript const dfs = (root) => { if (!root) return; @@ -67,19 +104,18 @@ const fib = (n) => { } \`\`\` -SELECTION -\`\`\` typescript +SELECTIONS +fib.ts (lines 10:10) +\`\`\`typescript return fib(n - 1) + fib(n - 2) \`\`\` INSTRUCTIONS -\`\`\` typescript memoize results -\`\`\` -EXPECTED OUTPUT +ACCEPTED OUTPUT To implement memoization in your Fibonacci function, you can use a JavaScript object to store previously computed results. This will help avoid redundant calculations and improve performance. Here's how you can modify your function: -\`\`\` typescript +\`\`\`typescript // existing code... const fib = (n, memo = {}) => { if (n < 1) return 1; @@ -97,188 +133,91 @@ Store Result: After computing fib(n), the result is stored in memo for future re ` +type FileSelnLocal = FileSelection & { content: string } +const stringifyFileSelection = ({ fileURI, selectionStr, range, content }: FileSelnLocal) => { + return `\ +${fileURI.fsPath} +\`\`\`${filenameToVscodeLanguage(fileURI.fsPath) ?? ''} +${content} +\`\`\` +` +} +const stringifyCodeSelection = ({ fileURI, selectionStr, range }: CodeSelection) => { + return `\ +${fileURI.fsPath} (lines ${range.startLineNumber}:${range.endLineNumber}) +\`\`\`${filenameToVscodeLanguage(fileURI.fsPath) ?? ''} +${selectionStr} +\`\`\` +` +} -const stringifySelections = (selections: CodeSelection[]) => { - return selections.map(({ fileURI, content, selectionStr }) => - `\ -File: ${fileURI.fsPath} -\`\`\` ${filenameToVscodeLanguage(fileURI.fsPath) ?? ''} -${content // this was the enite file which is foolish - } -\`\`\`${selectionStr === null ? '' : ` -Selection: ${selectionStr}`} -`).join('\n') +const failToReadStr = 'Could not read content. This file may have been deleted. If you expected content here, you can tell the user about this as they might not know.' +const stringifyFileSelections = async (fileSelections: FileSelection[], modelService: IModelService) => { + if (fileSelections.length === 0) return null + const fileSlns: FileSelnLocal[] = await Promise.all(fileSelections.map(async (sel) => { + const content = await VSReadFile(modelService, sel.fileURI) ?? failToReadStr + return { ...sel, content } + })) + return fileSlns.map(sel => stringifyFileSelection(sel)).join('\n') +} +const stringifyCodeSelections = (codeSelections: CodeSelection[]) => { + return codeSelections.map(sel => stringifyCodeSelection(sel)).join('\n') } -export const chat_prompt = (instructions: string, selections: CodeSelection[] | null) => { - let str = ''; - if (selections && selections.length > 0) { - str += stringifySelections(selections); - str += `Please edit the selected code following these instructions:\n` - } - str += `${instructions}`; + +export const chat_userMessage = async (instructions: string, selections: StagingSelectionItem[] | null, modelService: IModelService) => { + const fileSelections = selections?.filter(s => s.type === 'File') as FileSelection[] + const codeSelections = selections?.filter(s => s.type === 'Selection') as CodeSelection[] + + const filesStr = await stringifyFileSelections(fileSelections, modelService) + const codeStr = stringifyCodeSelections(codeSelections) + + let str = '' + if (filesStr) str += `FILES\n${filesStr}\n` + if (codeStr) str += `SELECTIONS\n${codeStr}\n` + str += `INSTRUCTIONS\n${instructions}` return str; }; -export const ctrlLStream_systemMessage = ` -You are a coding assistant that applies a diff to a file. You are given the original file \`original_file\`, a diff \`diff\`, and a new file that you are applying the diff to \`new_file\`. - -Please finish writing the new file \`new_file\`, according to the diff \`diff\`. You must completely re-write the whole file, using the diff. +export const fastApply_systemMessage = `\ +You are a coding assistant that re-writes an entire file to make a change. You are given the original file \`ORIGINAL_FILE\` and a change \`CHANGE\`. Directions: -1. Continue exactly where the new file \`new_file\` left off. +1. Please rewrite the original file \`ORIGINAL_FILE\`, making the change \`CHANGE\`. You must completely re-write the whole file. 2. Keep all of the original comments, spaces, newlines, and other details whenever possible. -3. Note that \`+\` lines represent additions, \`-\` lines represent removals, and space lines \` \` represent no change. - -# Example 1: - -ORIGINAL_FILE -\`Sidebar.tsx\`: -\`\`\` typescript -import React from 'react'; -import styles from './Sidebar.module.css'; - -interface SidebarProps { - items: { label: string; href: string }[]; - onItemSelect?: (label: string) => void; - onExtraButtonClick?: () => void; -} - -const Sidebar: React.FC = ({ items, onItemSelect, onExtraButtonClick }) => { - return ( -
-
    - {items.map((item, index) => ( -
  • - -
  • - ))} -
- -
- ); -}; - -export default Sidebar; -\`\`\` - -DIFF -\`\`\` typescript -@@ ... @@ --
--
    -- {items.map((item, index) => ( --
  • -- --
  • -- ))} --
-- --
-+
-+
    -+ {items.map((item, index) => ( -+
  • -+
    onItemSelect?.(item.label)} -+ > -+ {item.label} -+
    -+
  • -+ ))} -+
-+
-+ Extra Action -+
-+
-\`\`\` - -NEW_FILE -\`\`\` typescript -import React from 'react'; -import styles from './Sidebar.module.css'; - -interface SidebarProps { - items: { label: string; href: string }[]; - onItemSelect?: (label: string) => void; - onExtraButtonClick?: () => void; -} - -const Sidebar: React.FC = ({ items, onItemSelect, onExtraButtonClick }) => { - return ( -\`\`\` - -COMPLETION -\`\`\` typescript -
-
    - {items.map((item, index) => ( -
  • -
    onItemSelect?.(item.label)} - > - {item.label} -
    -
  • - ))} -
-
- Extra Action -
-
- ); -}; - -export default Sidebar;\`\`\` +3. ONLY output the full new file. Do not add any other explanations or text. ` -export const ctrlLStream_prompt = ({ originalCode, userMessage, uri }: { originalCode: string, userMessage: string, uri: URI }) => { +export const fastApply_userMessage = ({ originalCode, applyStr, uri }: { originalCode: string, applyStr: string, uri: URI }) => { const language = filenameToVscodeLanguage(uri.fsPath) ?? '' return `\ -ORIGINAL_CODE -\`\`\` ${language} +ORIGINAL_FILE +\`\`\`${language} ${originalCode} \`\`\` -DIFF +CHANGE \`\`\` -${userMessage} +${applyStr} \`\`\` INSTRUCTIONS -Please finish writing the new file by applying the diff to the original file. Return ONLY the completion of the file, without any explanation. +Please finish writing the new file by applying the change to the original file. Return ONLY the completion of the file, without any explanation. ` } -export const ctrlKStream_systemMessage = `\ -` + export const ctrlKStream_prefixAndSuffix = ({ fullFileStr, startLine, endLine }: { fullFileStr: string, startLine: number, endLine: number }) => { @@ -336,16 +275,31 @@ export type FimTagsType = { midTag: string } export const defaultFimTags: FimTagsType = { - preTag: 'BEFORE', - sufTag: 'AFTER', + preTag: 'ABOVE', + sufTag: 'BELOW', midTag: 'SELECTION', } -export const ctrlKStream_prompt = ({ selection, prefix, suffix, userMessage, fimTags, isOllamaFIM, language }: - { - selection: string, prefix: string, suffix: string, userMessage: string, fimTags: FimTagsType, language: string, - isOllamaFIM: false, // we require this be false for clarity - }) => { +// this should probably be longer +export const ctrlKStream_systemMessage = ({ fimTags: { preTag, midTag, sufTag } }: { fimTags: FimTagsType }) => { + return `\ +You are a FIM (fill-in-the-middle) coding assistant. Your task is to fill in the middle SELECTION marked by <${midTag}> tags. + +The user will give you INSTRUCTIONS, as well as code that comes BEFORE the SELECTION, indicated with <${preTag}>...before, and code that comes AFTER the SELECTION, indicated with <${sufTag}>...after. +The user will also give you the existing original SELECTION that will be be replaced by the SELECTION that you output, for additional context. + +Instructions: +1. Your OUTPUT should be a SINGLE PIECE OF CODE of the form <${midTag}>...new_code. Do NOT output any text or explanations before or after this. +2. You may ONLY CHANGE the original SELECTION, and NOT the content in the <${preTag}>... or <${sufTag}>... tags. +3. Make sure all brackets in the new selection are balanced the same as in the original selection. +4. Be careful not to duplicate or remove variables, comments, or other syntax by mistake. +` +} + +export const ctrlKStream_userMessage = ({ selection, prefix, suffix, instructions, fimTags, isOllamaFIM, language }: { + selection: string, prefix: string, suffix: string, instructions: string, fimTags: FimTagsType, language: string, + isOllamaFIM: false, // we require this be false for clarity +}) => { const { preTag, sufTag, midTag } = fimTags // prompt the model artifically on how to do FIM @@ -353,300 +307,20 @@ export const ctrlKStream_prompt = ({ selection, prefix, suffix, userMessage, fim // const sufTag = 'AFTER' // const midTag = 'SELECTION' return `\ -The user is selecting this code as their SELECTION: -\`\`\` ${language} + +CURRENT SELECTION +\`\`\`${language} <${midTag}>${selection} \`\`\` -The user wants to apply the following INSTRUCTIONS to the SELECTION: -${userMessage} +INSTRUCTIONS +${instructions} -Please edit the SELECTION following the user's INSTRUCTIONS, and return the edited selection. - -Note that the SELECTION has code that comes before it. This code is indicated with <${preTag}>...before. -Note also that the SELECTION has code that comes after it. This code is indicated with <${sufTag}>...after. - -Instructions: -1. Your OUTPUT should be a SINGLE PIECE OF CODE of the form <${midTag}>...new_selection. Do NOT output any text or explanations before or after this. -2. You may ONLY CHANGE the original SELECTION, and NOT the content in the <${preTag}>... or <${sufTag}>... tags. -3. Make sure all brackets in the new selection are balanced the same as in the original selection. -4. Be careful not to duplicate or remove variables, comments, or other syntax by mistake. - -Given the code: <${preTag}>${prefix} <${sufTag}>${suffix} -Return only the completion block of code (of the form \`\`\` ${language}\n <${midTag}>...new_selection\`\`\`):` +Return only the completion block of code (of the form \`\`\`${language} +<${midTag}>...new code +\`\`\`).` }; - - -// export const searchDiffChunkInstructions = ` -// You are a coding assistant that applies a diff to a file. You are given a diff \`diff\`, a list of files \`files\` to apply the diff to, and a selection \`selection\` that you are currently considering in the file. - -// Determine whether you should modify ANY PART of the selection \`selection\` following the \`diff\`. Return \`true\` if you should modify any part of the selection, and \`false\` if you should not modify any part of it. - -// # Example 1: - -// FILES -// selected file \`Sidebar.tsx\`: -// \`\`\` -// import React from 'react'; -// import styles from './Sidebar.module.css'; - -// interface SidebarProps { -// items: { label: string; href: string }[]; -// onItemSelect?: (label: string) => void; -// onExtraButtonClick?: () => void; -// } - -// const Sidebar: React.FC = ({ items, onItemSelect, onExtraButtonClick }) => { -// return ( -//
-//
    -// {items.map((item, index) => ( -//
  • -// -//
  • -// ))} -//
-// -//
-// ); -// }; - -// export default Sidebar; -// \`\`\` - -// DIFF -// \`\`\` -// @@ ... @@ -// -
-// -
    -// - {items.map((item, index) => ( -// -
  • -// - -// -
  • -// - ))} -// -
-// - -// -
-// +
-// +
    -// + {items.map((item, index) => ( -// +
  • -// +
    onItemSelect?.(item.label)} -// + > -// + {item.label} -// +
    -// +
  • -// + ))} -// +
-// +
-// + Extra Action -// +
-// +
-// \`\`\` - -// SELECTION -// \`\`\` -// import React from 'react'; -// import styles from './Sidebar.module.css'; - -// interface SidebarProps { -// items: { label: string; href: string }[]; -// onItemSelect?: (label: string) => void; -// onExtraButtonClick?: () => void; -// } - -// const Sidebar: React.FC = ({ items, onItemSelect, onExtraButtonClick }) => { -// return ( -//
-//
    -// {items.map((item, index) => ( -// \`\`\` - -// RESULT -// The output should be \`true\` because the diff begins on the line with \`
    \` and this line is present in the selection. - -// OUTPUT -// \`true\` -// ` - - - -// export const generateDiffInstructions = ` -// You are a coding assistant. You are given a list of relevant files \`files\`, a selection that the user is making \`selection\`, and instructions to follow \`instructions\`. - -// Please edit the selected file following the user's instructions (or, if appropriate, answer their question instead). - -// All changes made to files must be outputted in unified diff format. -// Unified diff format instructions: -// 1. Each diff must begin with \`\`\`@@ ... @@\`\`\`. -// 2. Each line must start with a \`+\` or \`-\` or \` \` symbol. -// 3. Make diffs more than a few lines. -// 4. Make high-level diffs rather than many one-line diffs. - -// Here's an example of unified diff format: - -// \`\`\` -// @@ ... @@ -// -def factorial(n): -// - if n == 0: -// - return 1 -// - else: -// - return n * factorial(n-1) -// +def factorial(number): -// + if number == 0: -// + return 1 -// + else: -// + return number * factorial(number-1) -// \`\`\` - -// Please create high-level diffs where you group edits together if they are near each other, like in the above example. Another way to represent the above example is to make many small line edits. However, this is less preferred, because the edits are not high-level. The edits are close together and should be grouped: - -// \`\`\` -// @@ ... @@ # This is less preferred because edits are close together and should be grouped: -// -def factorial(n): -// +def factorial(number): -// - if n == 0: -// + if number == 0: -// return 1 -// else: -// - return n * factorial(n-1) -// + return number * factorial(number-1) -// \`\`\` - -// # Example 1: - -// FILES -// selected file \`test.ts\`: -// \`\`\` -// x = 1 - -// {{selection}} - -// z = 3 -// \`\`\` - -// SELECTION -// \`\`\`const y = 2\`\`\` - -// INSTRUCTIONS -// \`\`\`y = 3\`\`\` - -// EXPECTED RESULT - -// We should change the selection from \`\`\`y = 2\`\`\` to \`\`\`y = 3\`\`\`. -// \`\`\` -// @@ ... @@ -// -x = 1 -// - -// -y = 2 -// +x = 1 -// + -// +y = 3 -// \`\`\` - -// # Example 2: - -// FILES -// selected file \`Sidebar.tsx\`: -// \`\`\` -// import React from 'react'; -// import styles from './Sidebar.module.css'; - -// interface SidebarProps { -// items: { label: string; href: string }[]; -// onItemSelect?: (label: string) => void; -// onExtraButtonClick?: () => void; -// } - -// const Sidebar: React.FC = ({ items, onItemSelect, onExtraButtonClick }) => { -// return ( -//
    -//
      -// {items.map((item, index) => ( -//
    • -// {{selection}} -// className={styles.sidebarButton} -// onClick={() => onItemSelect?.(item.label)} -// > -// {item.label} -// -//
    • -// ))} -//
    -// -//
    -// ); -// }; - -// export default Sidebar; -// \`\`\` - -// SELECTION -// \`\`\` -// -
      -// - {items.map((item, index) => ( -// -
    • -// - -// -
    • -// - ))} -// -
    -// - -// -
    -// +
    -// +
      -// + {items.map((item, index) => ( -// +
    • -// +
      onItemSelect?.(item.label)} -// + > -// + {item.label} -// +
      -// +
    • -// + ))} -// +
    -// +
    -// + Extra Action -// +
    -// +
    -// \`\`\` -// `; diff --git a/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx b/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx index 79429e20..6d7fc46b 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx @@ -41,15 +41,15 @@ const CodeButtonsOnHover = ({ text }: { text: string }) => { .catch(() => { setCopyButtonState(CopyButtonState.Error) }) metricsService.capture('Copy Code', { length: text.length }) // capture the length only - }, [text, clipboardService]) + }, [metricsService, clipboardService, text]) const onApply = useCallback(() => { inlineDiffService.startApplying({ featureName: 'Ctrl+L', - userMessage: text, + applyStr: text, }) metricsService.capture('Apply Code', { length: text.length }) // capture the length only - }, [inlineDiffService]) + }, [metricsService, inlineDiffService, text]) const isSingleLine = !text.includes('\n') diff --git a/src/vs/workbench/contrib/void/browser/react/src/quick-edit-tsx/QuickEditChat.tsx b/src/vs/workbench/contrib/void/browser/react/src/quick-edit-tsx/QuickEditChat.tsx index 80988038..da2a0d70 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/quick-edit-tsx/QuickEditChat.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/quick-edit-tsx/QuickEditChat.tsx @@ -58,7 +58,6 @@ export const QuickEditChat = ({ const id = inlineDiffsService.startApplying({ featureName: 'Ctrl+K', diffareaid: diffareaid, - userMessage: instructions, }) setCurrentlyStreamingDiffZone(id ?? null) }, [currStreamingDiffZoneRef, setCurrentlyStreamingDiffZone, isDisabled, inlineDiffsService, diffareaid]) @@ -80,7 +79,7 @@ export const QuickEditChat = ({ const keybindingString = accessor.get('IKeybindingService').lookupKeybinding(VOID_CTRL_K_ACTION_ID)?.getLabel() - return
    + return
    diff --git a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/ErrorDisplay.tsx b/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/ErrorDisplay.tsx index 84fe410a..689d55c1 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/ErrorDisplay.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/ErrorDisplay.tsx @@ -9,7 +9,7 @@ import { errorDetails } from '../../../../../../../platform/void/common/llmMessa export const ErrorDisplay = ({ - message:message_, + message: message_, fullError, onDismiss, showDismiss, @@ -23,7 +23,7 @@ export const ErrorDisplay = ({ const details = errorDetails(fullError) - const message = message_ === 'TypeError: fetch failed' ? 'TypeError: fetch failed. This likely means you specified the wrong endpoint in Void Settings.' : message_ + const message = message_ === 'TypeError: fetch failed' ? 'TypeError: fetch failed. This likely means you specified the wrong endpoint in Void Settings.' : message_ + '' return ( 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 75e4579e..ed1611e8 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 @@ -3,32 +3,23 @@ * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. *--------------------------------------------------------------------------------------*/ -import React, { ButtonHTMLAttributes, FormEvent, FormHTMLAttributes, Fragment, useCallback, useEffect, useRef, useState } from 'react'; +import React, { ButtonHTMLAttributes, FormEvent, FormHTMLAttributes, Fragment, useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useAccessor, useSidebarState, useChatThreadsState, useChatThreadsStreamState, useUriState } from '../util/services.js'; -import { ChatMessage, CodeSelection, CodeStagingSelection } from '../../../chatThreadService.js'; +import { ChatMessage, StagingSelectionItem } from '../../../chatThreadService.js'; import { BlockCode } from '../markdown/BlockCode.js'; import { ChatMarkdownRender } from '../markdown/ChatMarkdownRender.js'; import { URI } from '../../../../../../../base/common/uri.js'; -import { EndOfLinePreference } from '../../../../../../../editor/common/model.js'; import { IDisposable } from '../../../../../../../base/common/lifecycle.js'; import { ErrorDisplay } from './ErrorDisplay.js'; -import { OnError, ServiceSendLLMMessageParams } from '../../../../../../../platform/void/common/llmMessageTypes.js'; -import { HistoryInputBox, InputBox } from '../../../../../../../base/browser/ui/inputbox/inputBox.js'; -import { TextAreaFns, VoidCodeEditorProps, VoidInputBox2 } from '../util/inputs.js'; +import { TextAreaFns, VoidInputBox2 } from '../util/inputs.js'; import { ModelDropdown, WarningBox } from '../void-settings-tsx/ModelDropdown.js'; -import { chat_systemMessage, chat_prompt } from '../../../prompt/prompts.js'; -import { ISidebarStateService } from '../../../sidebarStateService.js'; -import { ILLMMessageService } from '../../../../../../../platform/void/common/llmMessageService.js'; -import { IModelService } from '../../../../../../../editor/common/services/model.js'; import { SidebarThreadSelector } from './SidebarThreadSelector.js'; import { useScrollbarStyles } from '../util/useScrollbarStyles.js'; import { VOID_CTRL_L_ACTION_ID } from '../../../actionIDs.js'; -import { ArrowBigLeftDash, CopyX, Delete, FileX2, SquareX, X } from 'lucide-react'; import { filenameToVscodeLanguage } from '../../../helpers/detectLanguage.js'; -import { Pencil } from 'lucide-react' import { VOID_OPEN_SETTINGS_ACTION_ID } from '../../../voidSettingsPane.js'; @@ -261,8 +252,8 @@ const getBasename = (pathStr: string) => { export const SelectedFiles = ( { type, selections, setSelections, showProspectiveSelections }: - | { type: 'past', selections: CodeSelection[]; setSelections?: undefined, showProspectiveSelections?: undefined } - | { type: 'staging', selections: CodeStagingSelection[]; setSelections: ((newSelections: CodeStagingSelection[]) => void), showProspectiveSelections?: boolean } + | { type: 'past', selections: StagingSelectionItem[]; setSelections?: undefined, showProspectiveSelections?: undefined } + | { type: 'staging', selections: StagingSelectionItem[]; setSelections: ((newSelections: StagingSelectionItem[]) => void), showProspectiveSelections?: boolean } ) => { // index -> isOpened @@ -287,11 +278,11 @@ export const SelectedFiles = ( return withCurrent.slice(0, maxRecentUris) }) }, [currentUri]) - let prospectiveSelections: CodeStagingSelection[] = [] + let prospectiveSelections: StagingSelectionItem[] = [] if (type === 'staging' && showProspectiveSelections) { // handle prospective files // add a prospective file if type === 'staging' and if the user is in a file, and if the file is not selected yet prospectiveSelections = recentUris - .filter(uri => !selections.find(s => s.range === null && s.fileURI.fsPath === uri.fsPath)) + .filter(uri => !selections.find(s => s.type === 'File' && s.fileURI.fsPath === uri.fsPath)) .slice(0, maxProspectiveFiles) .map(uri => ({ type: 'File', @@ -325,7 +316,7 @@ export const SelectedFiles = ( > {/* selection summary */}
    { if (isThisSelectionProspective) { // add prospective selection to selections if (type !== 'staging') return; // (never) - setSelections([...selections, selection as CodeStagingSelection]) + setSelections([...selections, selection]) } else if (isThisSelectionAFile) { // open files commandService.executeCommand('vscode.open', selection.fileURI, { @@ -380,7 +371,7 @@ export const SelectedFiles = (
    {/* clear all selections button */} - {type !== 'staging' || selections.length === 0 || i !== selections.length - 1 + {/* {type !== 'staging' || selections.length === 0 || i !== selections.length - 1 ? null :
    - } + } */}
    {/* selection text */} {isThisSelectionOpened && @@ -413,7 +404,7 @@ export const SelectedFiles = ( }} > ) return - {selections.length > 0 && i === selections.length && -
    // divider between `selections` and `prospectiveSelections` - } + {/* divider between `selections` and `prospectiveSelections` */} + {/* {selections.length > 0 && i === selections.length &&
    } */} {selectionHTML}
    @@ -446,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 mt-4 self-end w-fit max-w-full` : role === 'assistant' ? `px-2 self-start w-full max-w-full` : '' } `} @@ -487,7 +477,7 @@ const ChatBubble = ({ chatMessage, isLoading }: { chatMessage: ChatMessage, isLo const [isEditMode, setIsEditMode] = useState(false) - if (!chatMessage.content) { // don't show if empty + if (!chatMessage.content && !isLoading) { // don't show if empty and not loading (if loading, want to show) return null } @@ -606,6 +596,13 @@ export const SidebarChat = () => { scrollContainerRef.current?.scrollTo({ top: 0, left: 0 }) }, [isHistoryOpen, currentThread.id]) + + const prevMessagesHTML = useMemo(() => { + return previousMessages.map((message, i) => + + ) + }, [previousMessages]) + return
    { scrollContainerRef={scrollContainerRef} className={` w-full h-auto - flex flex-col gap-1 + flex flex-col overflow-x-hidden overflow-y-auto `} style={{ maxHeight: sidebarDimensions.height - historyDimensions.height - formDimensions.height - 36 }} // the height of the previousMessages is determined by all other heights > {/* previous messages */} - {previousMessages.map((message, i) => - - )} + {prevMessagesHTML} {/* message stream */} @@ -675,7 +670,7 @@ export const SidebarChat = () => { {/* top row */} <> {/* selections */} - + {/* middle row */} diff --git a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarThreadSelector.tsx b/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarThreadSelector.tsx index cca66c58..72dc6d39 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarThreadSelector.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarThreadSelector.tsx @@ -104,6 +104,7 @@ export const SidebarThreadSelector = () => { flex items-center `} onClick={() => chatThreadsService.switchToThread(pastThread.id)} + onDoubleClick={() => sidebarStateService.setState({ isHistoryOpen: false })} title={new Date(pastThread.createdAt).toLocaleString()} >
    {`${firstMsg}`}
    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 c1421f66..b21df36f 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 @@ -687,6 +687,8 @@ export const VoidCodeEditor = ({ initValue, language, maxHeight, showScrollbars // maxColumn: 0, }, + hover: { enabled: false }, + selectionHighlight: false, // highlights whole words renderLineHighlight: 'none', diff --git a/src/vs/workbench/contrib/void/browser/sidebarActions.ts b/src/vs/workbench/contrib/void/browser/sidebarActions.ts index 5787fc4d..5e085e8e 100644 --- a/src/vs/workbench/contrib/void/browser/sidebarActions.ts +++ b/src/vs/workbench/contrib/void/browser/sidebarActions.ts @@ -11,7 +11,7 @@ import { ServicesAccessor } from '../../../../editor/browser/editorExtensions.js import { KeybindingWeight } from '../../../../platform/keybinding/common/keybindingsRegistry.js'; import { ContextKeyExpr } from '../../../../platform/contextkey/common/contextkey.js'; -import { CodeStagingSelection, IChatThreadService } from './chatThreadService.js'; +import { StagingSelectionItem, IChatThreadService } from './chatThreadService.js'; import { ICodeEditorService } from '../../../../editor/browser/services/codeEditorService.js'; import { IRange } from '../../../../editor/common/core/range.js'; @@ -112,7 +112,7 @@ registerAction2(class extends Action2 { const selectionStr = getContentInRange(model, selectionRange) - const selection: CodeStagingSelection = !selectionRange || !selectionStr || (selectionRange.startLineNumber > selectionRange.endLineNumber) ? { + const selection: StagingSelectionItem = !selectionRange || !selectionStr || (selectionRange.startLineNumber > selectionRange.endLineNumber) ? { type: 'File', fileURI: model.uri, selectionStr: null, diff --git a/src/vs/workbench/contrib/void/browser/voidUpdateActions.ts b/src/vs/workbench/contrib/void/browser/voidUpdateActions.ts index e58d26ad..04b263b4 100644 --- a/src/vs/workbench/contrib/void/browser/voidUpdateActions.ts +++ b/src/vs/workbench/contrib/void/browser/voidUpdateActions.ts @@ -53,10 +53,11 @@ registerAction2(class extends Action2 { const notifService = accessor.get(INotificationService) const metricsService = accessor.get(IMetricsService) + metricsService.capture('Void Update Manual: Checking...', {}) const res = await voidUpdateService.check() - if (!res) { notifyErrChecking(notifService); metricsService.capture('Void Update: Error', {}) } - else if (res.hasUpdate) { notifyYesUpdate(notifService, res.message); metricsService.capture('Void Update: Yes', {}) } - else if (!res.hasUpdate) { notifyNoUpdate(notifService); metricsService.capture('Void Update: No', {}) } + if (!res) { notifyErrChecking(notifService); metricsService.capture('Void Update Manual: Error', { res }) } + else if (res.hasUpdate) { notifyYesUpdate(notifService, res.message); metricsService.capture('Void Update Manual: Yes', { res }) } + else if (!res.hasUpdate) { notifyNoUpdate(notifService); metricsService.capture('Void Update Manual: No', { res }) } } }) @@ -65,23 +66,27 @@ class VoidUpdateWorkbenchContribution extends Disposable implements IWorkbenchCo static readonly ID = 'workbench.contrib.void.voidUpdate' constructor( @IVoidUpdateService private readonly voidUpdateService: IVoidUpdateService, - @INotificationService private readonly notifService: INotificationService, @IMetricsService private readonly metricsService: IMetricsService, + @INotificationService private readonly notifService: INotificationService, ) { super() - - // on mount - setTimeout(async () => { + const autoCheck = async () => { + this.metricsService.capture('Void Update Startup: Checking...', {}) const res = await this.voidUpdateService.check() + if (!res) { notifyErrChecking(this.notifService); this.metricsService.capture('Void Update Startup: Error', { res }) } + else if (res.hasUpdate) { notifyYesUpdate(this.notifService, res.message); this.metricsService.capture('Void Update Startup: Yes', { res }) } + else if (!res.hasUpdate) { this.metricsService.capture('Void Update Startup: No', { res }) } // display nothing if up to date + } - const notifService = this.notifService - const metricsService = this.metricsService + // check once 5 seconds after mount - if (!res) { notifyErrChecking(notifService); metricsService.capture('Void Update Startup: Error', {}) } - else if (res.hasUpdate) { notifyYesUpdate(this.notifService, res.message); metricsService.capture('Void Update Startup: Yes', {}) } - else if (!res.hasUpdate) { metricsService.capture('Void Update Startup: No', {}) } // display nothing if up to date + const initId = setTimeout(() => autoCheck(), 5 * 1000) + this._register({ dispose: () => clearTimeout(initId) }) + + // check every 3 hours + const intervalId = setInterval(() => autoCheck(), 3 * 60 * 60 * 1000) + this._register({ dispose: () => clearInterval(intervalId) }) - }, 5 * 1000) } } registerWorkbenchContribution2(VoidUpdateWorkbenchContribution.ID, VoidUpdateWorkbenchContribution, WorkbenchPhase.BlockRestore); diff --git a/src/vs/workbench/electron-sandbox/desktop.contribution.ts b/src/vs/workbench/electron-sandbox/desktop.contribution.ts index f6bae665..c14e3e1a 100644 --- a/src/vs/workbench/electron-sandbox/desktop.contribution.ts +++ b/src/vs/workbench/electron-sandbox/desktop.contribution.ts @@ -190,7 +190,7 @@ import { MAX_ZOOM_LEVEL, MIN_ZOOM_LEVEL } from '../../platform/window/electron-s }, 'window.zoomLevel': { 'type': 'number', - 'default': 0, + 'default': -1, 'minimum': MIN_ZOOM_LEVEL, 'maximum': MAX_ZOOM_LEVEL, 'markdownDescription': localize({ comment: ['{0} will be a setting name rendered as a link'], key: 'zoomLevel' }, "Adjust the default zoom level for all windows. Each increment above `0` (e.g. `1`) or below (e.g. `-1`) represents zooming `20%` larger or smaller. You can also enter decimals to adjust the zoom level with a finer granularity. See {0} for configuring if the 'Zoom In' and 'Zoom Out' commands apply the zoom level to all windows or only the active window.", '`#window.zoomPerWindow#`'), diff --git a/src/vs/workbench/services/themes/common/workbenchThemeService.ts b/src/vs/workbench/services/themes/common/workbenchThemeService.ts index ba080802..73e6fd39 100644 --- a/src/vs/workbench/services/themes/common/workbenchThemeService.ts +++ b/src/vs/workbench/services/themes/common/workbenchThemeService.ts @@ -44,45 +44,77 @@ export enum ThemeSettings { } export enum ThemeSettingDefaults { - COLOR_THEME_DARK = 'Default Dark Modern', + COLOR_THEME_DARK = 'Default Dark+', // Void changed this. this is the default theme COLOR_THEME_LIGHT = 'Default Light Modern', COLOR_THEME_HC_DARK = 'Default High Contrast', COLOR_THEME_HC_LIGHT = 'Default High Contrast Light', - COLOR_THEME_DARK_OLD = 'Default Dark+', + COLOR_THEME_DARK_OLD = 'Default Dark Modern', // Void changed this COLOR_THEME_LIGHT_OLD = 'Default Light+', FILE_ICON_THEME = 'vs-seti', PRODUCT_ICON_THEME = 'Default', } -export const COLOR_THEME_DARK_INITIAL_COLORS = { - 'activityBar.activeBorder': '#0078d4', - 'activityBar.background': '#181818', - 'activityBar.border': '#2b2b2b', - 'activityBar.foreground': '#d7d7d7', - 'activityBar.inactiveForeground': '#868686', - 'editorGroup.border': '#ffffff17', - 'editorGroupHeader.tabsBackground': '#181818', - 'editorGroupHeader.tabsBorder': '#2b2b2b', - 'statusBar.background': '#181818', - 'statusBar.border': '#2b2b2b', - 'statusBar.foreground': '#cccccc', - 'statusBar.noFolderBackground': '#1f1f1f', - 'tab.activeBackground': '#1f1f1f', - 'tab.activeBorder': '#1f1f1f', - 'tab.activeBorderTop': '#0078d4', +// export const COLOR_THEME_DARK_INITIAL_COLORS = { +// 'activityBar.activeBorder': '#0078d4', +// 'activityBar.background': '#181818', +// 'activityBar.border': '#2b2b2b', +// 'activityBar.foreground': '#d7d7d7', +// 'activityBar.inactiveForeground': '#868686', +// 'editorGroup.border': '#ffffff17', +// 'editorGroupHeader.tabsBackground': '#181818', +// 'editorGroupHeader.tabsBorder': '#2b2b2b', +// 'statusBar.background': '#181818', +// 'statusBar.border': '#2b2b2b', +// 'statusBar.foreground': '#cccccc', +// 'statusBar.noFolderBackground': '#1f1f1f', +// 'tab.activeBackground': '#1f1f1f', +// 'tab.activeBorder': '#1f1f1f', +// 'tab.activeBorderTop': '#0078d4', +// 'tab.activeForeground': '#ffffff', +// 'tab.border': '#2b2b2b', +// 'textLink.foreground': '#4daafc', +// 'titleBar.activeBackground': '#181818', +// 'titleBar.activeForeground': '#cccccc', +// 'titleBar.border': '#2b2b2b', +// 'titleBar.inactiveBackground': '#1f1f1f', +// 'titleBar.inactiveForeground': '#9d9d9d', +// 'welcomePage.tileBackground': '#2b2b2b' +// }; + + + +export const COLOR_THEME_DARK_INITIAL_COLORS = { // Void changed this to match dark+ + 'activityBar.activeBorder': '#ffffff', + 'activityBar.background': '#333333', + 'activityBar.border': '#454545', + 'activityBar.foreground': '#ffffff', + 'activityBar.inactiveForeground': '#ffffff66', + 'editorGroup.border': '#444444', + 'editorGroupHeader.tabsBackground': '#252526', + 'editorGroupHeader.tabsBorder': '#252526', + 'statusBar.background': '#007ACC', + 'statusBar.border': '#454545', + 'statusBar.foreground': '#ffffff', + 'statusBar.noFolderBackground': '#68217A', + 'tab.activeBackground': '#2D2D2D', + 'tab.activeBorder': '#ffffff', + 'tab.activeBorderTop': '#007ACC', 'tab.activeForeground': '#ffffff', - 'tab.border': '#2b2b2b', - 'textLink.foreground': '#4daafc', - 'titleBar.activeBackground': '#181818', - 'titleBar.activeForeground': '#cccccc', - 'titleBar.border': '#2b2b2b', - 'titleBar.inactiveBackground': '#1f1f1f', - 'titleBar.inactiveForeground': '#9d9d9d', - 'welcomePage.tileBackground': '#2b2b2b' + 'tab.border': '#252526', + 'textLink.foreground': '#3794ff', + 'titleBar.activeBackground': '#3C3C3C', + 'titleBar.activeForeground': '#CCCCCC', + 'titleBar.border': '#454545', + 'titleBar.inactiveBackground': '#2C2C2C', + 'titleBar.inactiveForeground': '#999999', + 'welcomePage.tileBackground': '#252526' }; + + + export const COLOR_THEME_LIGHT_INITIAL_COLORS = { 'activityBar.activeBorder': '#005FB8', 'activityBar.background': '#f8f8f8',