Compare commits

..

58 Commits

Author SHA1 Message Date
Andrew Pareles 4e392bee8e start adding icons for copying files and folders in the explorer 2025-01-06 22:25:41 -08:00
Andrew Pareles f96d320ce0 fix tricky reject bug 2025-01-06 02:02:48 -08:00
Andrew Pareles bb7e69b20b small fix for realigning 2025-01-06 01:43:54 -08:00
Andrew Pareles f19e579ce9 extract code works! ignore </MID> etc 2025-01-06 01:19:27 -08:00
Andrew Pareles dd133d2cd5 ctrlK roundRange 2025-01-05 21:27:16 -08:00
Andrew Pareles 2ba04a78a8 add X to ctrlK 2025-01-05 21:02:31 -08:00
Andrew Pareles fb948b331f ctrlK 2025-01-05 18:03:36 -08:00
Mathew Pareles 35091eb8f2 change wording 2025-01-05 17:11:10 -08:00
Mathew Pareles b9e61bf4e2 styles 2025-01-05 17:10:20 -08:00
Andrew Pareles 55ec3365a2 empty seln 2025-01-05 17:04:07 -08:00
Mathew Pareles d6811e872a fix color 2025-01-05 16:57:39 -08:00
Mathew Pareles c176b8020f styles 2025-01-05 16:35:43 -08:00
Andrew Pareles 6dcf684429 updateService changes 2025-01-03 17:11:18 -08:00
Mathew Pareles 26ddd6418c extract <MID> result 2025-01-03 16:54:04 -08:00
Mathew Pareles cffe558cf5 code editor bg 2025-01-03 01:35:36 -08:00
Mathew Pareles 9bc551f0ac code editor bg 2025-01-03 01:35:36 -08:00
Mathew Pareles 01d7878671 code editor bg 2025-01-03 01:31:47 -08:00
Mathew Pareles 2519f094cc input and dropdown styles 2025-01-03 00:00:03 -08:00
Andrew Pareles a72305819a copy 2025-01-02 19:15:08 -08:00
Andrew Pareles 32bfe9a111 fix line indexing when streaming 2025-01-02 18:58:44 -08:00
Andrew Pareles a9adcad560 misc 2025-01-02 17:57:38 -08:00
Mathew Pareles 80f7511eee default settings instructions 2025-01-02 17:34:31 -08:00
Andrew Pareles 7fe8f5c47d ctrlK line numbers are right 2025-01-02 17:26:28 -08:00
Mathew Pareles b31882565b fix watchreact 2025-01-02 17:11:32 -08:00
Andrew Pareles 6aaf425419 Settings update + ctrlK stream runs (but is broken) 2025-01-02 17:10:38 -08:00
Andrew Pareles 2087948789 misc necessary updates 2025-01-02 16:40:55 -08:00
Andrew Pareles df14b80d8b not sure where this came from 2025-01-02 16:28:33 -08:00
Andrew Pareles 86decf8ed9 add (c) headers 2025-01-02 16:27:06 -08:00
Andrew Pareles bd9b9a9058 minor updates 2025-01-02 16:20:19 -08:00
Mathew Pareles 83c1f3430a fix assistant chat 2025-01-02 15:51:00 -08:00
Andrew Pareles 02de8937ac update copy 2025-01-02 01:28:19 -08:00
Mathew Pareles 78f3819c33 move extractCodeFromResult 2025-01-01 23:19:16 -08:00
Mathew Pareles b9cea4906e fix chat bubble direction 2025-01-01 23:08:32 -08:00
Mathew Pareles 4a4677b5e2 center button; fix single line button 2025-01-01 22:35:48 -08:00
Andrew Pareles 5dab1a6a77 update product.json 2025-01-01 22:05:57 -08:00
Mathew Pareles 9b01d5ba7c minor changes 2025-01-01 21:28:44 -08:00
Mathew Pareles dcb708758d fix input resize 2025-01-01 21:28:44 -08:00
Andrew Pareles dd4548466a minor chagnes 2025-01-01 20:01:10 -08:00
Andrew Pareles cda489e3e0 small fixes 2025-01-01 05:04:51 -08:00
Andrew Pareles 13594ef06c rework copy 2025-01-01 04:57:10 -08:00
Andrew Pareles c2b8df06eb desc 2025-01-01 04:47:41 -08:00
Andrew Pareles 33809ec48b finish adding css 2025-01-01 04:44:43 -08:00
Andrew Pareles b4820abc9e css 2025-01-01 04:31:44 -08:00
Andrew Pareles 0e79dec0d7 VOID ICONS 2024-12-31 22:03:38 -08:00
Andrew Pareles 659f01413f misc + track down icons to change 2024-12-31 20:27:31 -08:00
Andrew Pareles 110534df30 memo model dropdown + add ability to remove ctrlK zone 2024-12-31 16:18:18 -08:00
Andrew Pareles d3b61867f0 ctrlK now switches tabs + UI + remove async 2024-12-31 15:41:24 -08:00
Andrew Pareles 954e611fda Merge branch 'new-services' into model-selection 2024-12-31 13:56:07 -08:00
Mathew Pareles 198adaaf18 selection ui 2024-12-30 04:57:43 -05:00
Mathew Pareles 2df7fa9ebc better model selection 2024-12-30 04:36:31 -05:00
Mathew Pareles 312942b004 ollama instructions 2024-12-30 03:39:17 -05:00
Mathew Pareles 0a444f6ff8 mark if model is detected locally 2024-12-30 01:45:02 -05:00
Mathew Pareles 0c10cb73f6 fix polling enable bug 2024-12-29 23:45:46 -05:00
Mathew Pareles 3a3bc24fef better polling state 2024-12-29 05:07:52 -05:00
Mathew Pareles 076e0add44 prepare large merge 2024-12-25 19:21:16 -08:00
Mathew Pareles 8a17c9246b language logic 2024-12-24 17:34:18 -08:00
mp 1afb39b155 minor changes 2024-12-24 02:22:45 -08:00
mp f08fd04af5 autodetect language 2024-12-23 16:21:37 -08:00
60 changed files with 1461 additions and 575 deletions
+5 -4
View File
@@ -43,19 +43,19 @@ First, run `npm install -g node-gyp`. Then:
### Building Void
To build Void, open `void/` inside VSCode. Then:
To build Void, open `void/` inside VSCode. Then open your terminal and run:
1. `npm install` to install all dependencies.
2. `npm run watchreact` to build Void's browser dependencies like React.
3. Build.
3. Build Void.
- Press <kbd>Cmd+Shift+B</kbd> (Mac).
- Press <kbd>Ctrl+Shift+B</kbd> (Windows/Linux).
- This step can take ~5 min. The build is done when you see two check marks.
4. Run.
4. Run Void.
- Run `./scripts/code.sh` (Mac/Linux).
- Run `./scripts/code.bat` (Windows).
- This command should open up the built IDE. You can always press <kbd>Ctrl+R</kbd> (<kbd>Cmd+R</kbd>) inside the new window to see changes without re-building, or press or <kbd>Ctrl+Shift+P</kbd> in the new window and run "Reload Window".
- If you would like to reset Void back to its default settings, you can run `./scripts/code.sh --user-data-dir ./.tmp/user-data --extensions-dir ./.tmp/extensions` (mac). This will save all data and extensions to the `.tmp` folder. You can delete this folder to reset your settings.
#### Building Void from Terminal
@@ -75,6 +75,7 @@ Alternatively, if you want to build Void from the terminal, instead of pressing
- Make sure you follow the prerequisite steps.
- Make sure you have the same NodeJS version as `.nvmrc`.
- Make sure your `npm run watchreact` is running if you change any React files, or else you'll need to re-build.
- If you see missing styles, go to `src2/styles.css` and re-save the file.
- If you get `"TypeError: Failed to fetch dynamically imported module: vscode-file://vscode-app/.../workbench.desktop.main.js", source: file:///.../bootstrap-window.js`, make sure all imports end with `.js`.
- If you have any questions, feel free to [submit an issue](https://github.com/voideditor/void/issues/new). For building questions, you can also refer to VSCode's full [How to Contribute](https://github.com/microsoft/vscode/wiki/How-to-Contribute) page.
+4 -4
View File
@@ -425,15 +425,15 @@ function packageTask(platform, arch, sourceFolderName, destinationFolderName, op
'resources/win32/vue.ico',
'resources/win32/xml.ico',
'resources/win32/yaml.ico',
'resources/win32/code_70x70.png',
'resources/win32/code_150x150.png'
'resources/win32/code_70x70.png', // <-- Void icon
'resources/win32/code_150x150.png' // <-- Void icon
], { base: '.' }));
} else if (platform === 'linux') {
all = es.merge(all, gulp.src('resources/linux/code.png', { base: '.' }));
all = es.merge(all, gulp.src('resources/linux/code.png', { base: '.' })); // <-- Void icon
} else if (platform === 'darwin') {
const shortcut = gulp.src('resources/darwin/bin/code.sh')
.pipe(replace('@@APPNAME@@', product.applicationName))
.pipe(rename('bin/code'));
.pipe(rename('bin/code')); // <-- Void icon
all = es.merge(all, shortcut);
}
+2 -2
View File
@@ -54,7 +54,7 @@ function darwinBundleDocumentType(extensions, icon, nameOrSuffix, utis) {
role: 'Editor',
ostypes: ['TEXT', 'utxt', 'TUTX', '****'],
extensions,
iconFile: 'resources/darwin/' + icon.toLowerCase() + '.icns',
iconFile: 'resources/darwin/' + icon.toLowerCase() + '.icns', // <-- Void icon code.icns
utis
};
}
@@ -179,7 +179,7 @@ exports.config = {
darwinForceDarkModeSupport: true,
darwinCredits: darwinCreditsTemplate ? Buffer.from(darwinCreditsTemplate({ commit: commit, date: new Date().toISOString() })) : undefined,
linuxExecutableName: product.applicationName,
winIcon: 'resources/win32/code.ico',
winIcon: 'resources/win32/code.ico', // <-- Void icon
token: process.env['GITHUB_TOKEN'],
repo: product.electronRepository || undefined,
validateChecksum: true,
+2 -2
View File
@@ -68,7 +68,7 @@ function darwinBundleDocumentType(extensions: string[], icon: string, nameOrSuff
role: 'Editor',
ostypes: ['TEXT', 'utxt', 'TUTX', '****'],
extensions,
iconFile: 'resources/darwin/' + icon.toLowerCase() + '.icns',
iconFile: 'resources/darwin/' + icon.toLowerCase() + '.icns', // <-- Void icon code.icns
utis
};
}
@@ -196,7 +196,7 @@ export const config = {
darwinForceDarkModeSupport: true,
darwinCredits: darwinCreditsTemplate ? Buffer.from(darwinCreditsTemplate({ commit: commit, date: new Date().toISOString() })) : undefined,
linuxExecutableName: product.applicationName,
winIcon: 'resources/win32/code.ico',
winIcon: 'resources/win32/code.ico', // <-- Void icon
token: process.env['GITHUB_TOKEN'],
repo: product.electronRepository || undefined,
validateChecksum: true,
+5 -2
View File
@@ -4,6 +4,7 @@
? ('; LicenseFile: "' + RepoDir + '\licenses\LICENSE-' + Language + '.rtf"') \
: '; LicenseFile: "' + RepoDir + '\' + RootLicenseFileName + '"'
[Setup]
AppId={#AppId}
AppName={#NameLong}
@@ -20,8 +21,10 @@ Compression=lzma
SolidCompression=yes
AppMutex={code:GetAppMutex}
SetupMutex={#AppMutex}setup
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"
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"
; 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"
WizardSmallImageFile="{#RepoDir}\resources\win32\inno-void.bmp"
SetupIconFile={#RepoDir}\resources\win32\code.ico
UninstallDisplayIcon={app}\{#ExeBasename}.exe
ChangesEnvironment=true
+4 -4
View File
@@ -1,12 +1,12 @@
{
"name": "code-oss-dev",
"version": "1.94.0",
"name": "void-dev",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "code-oss-dev",
"version": "1.94.0",
"name": "void-dev",
"version": "1.0.0",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
+4 -3
View File
@@ -1,9 +1,10 @@
{
"name": "code-oss-dev",
"name": "void-dev",
"productName": "Void",
"version": "1.94.0",
"distro": "ffcc24343ac46468a625666e5b9e673971dd1a1f",
"distro": "this is a commit number if we want to publish on npm",
"author": {
"name": "Microsoft Corporation"
"name": "Glass Devtools, Inc."
},
"license": "MIT",
"main": "./out/main",
+23 -24
View File
@@ -1,36 +1,35 @@
{
"nameShort": "Void",
"nameLong": "Void",
"applicationName": "code-oss",
"dataFolderName": ".vscode-oss",
"win32MutexName": "vscodeoss",
"applicationName": "void",
"dataFolderName": ".void-editor",
"win32MutexName": "voideditor",
"licenseName": "MIT",
"licenseUrl": "https://github.com/microsoft/vscode/blob/main/LICENSE.txt",
"serverLicenseUrl": "https://github.com/microsoft/vscode/blob/main/LICENSE.txt",
"licenseUrl": "https://github.com/voideditor/void/blob/main/LICENSE.txt",
"serverLicenseUrl": "https://github.com/voideditor/void/blob/main/LICENSE.txt",
"serverGreeting": [],
"serverLicense": [],
"serverLicensePrompt": "",
"serverApplicationName": "code-server-oss",
"serverDataFolderName": ".vscode-server-oss",
"tunnelApplicationName": "code-tunnel-oss",
"win32DirName": "Microsoft Code OSS",
"win32NameVersion": "Microsoft Code OSS",
"win32RegValueName": "CodeOSS",
"win32x64AppId": "{{D77B7E06-80BA-4137-BCF4-654B95CCEBC5}",
"win32arm64AppId": "{{D1ACE434-89C5-48D1-88D3-E2991DF85475}",
"win32x64UserAppId": "{{CC6B787D-37A0-49E8-AE24-8559A032BE0C}",
"win32arm64UserAppId": "{{3AEBF0C8-F733-4AD4-BADE-FDB816D53D7B}",
"win32AppUserModelId": "Microsoft.CodeOSS",
"win32ShellNameShort": "C&ode - OSS",
"win32TunnelServiceMutex": "vscodeoss-tunnelservice",
"win32TunnelMutex": "vscodeoss-tunnel",
"darwinBundleIdentifier": "com.visualstudio.code.oss",
"linuxIconName": "code-oss",
"serverApplicationName": "void-server",
"serverDataFolderName": ".void-server",
"tunnelApplicationName": "void-tunnel",
"win32DirName": "Void",
"win32NameVersion": "Void",
"win32RegValueName": "VoidEditor",
"win32x64AppId": "{{9D394D01-1728-45A7-B997-A6C82C5452C3}",
"win32arm64AppId": "{{0668DD58-2BDE-4101-8CDA-40252DF8875D}",
"win32x64UserAppId": "{{8BED5DC1-6C55-46E6-9FE6-18F7E6F7C7F1}",
"win32arm64UserAppId": "{{F6C87466-BC82-4A8F-B0FF-18CA366BA4D8}",
"win32AppUserModelId": "Void.Editor",
"win32ShellNameShort": "V&oid",
"win32TunnelServiceMutex": "void-tunnelservice",
"win32TunnelMutex": "void-tunnel",
"darwinBundleIdentifier": "com.voideditor.code",
"linuxIconName": "void-editor",
"licenseFileName": "LICENSE.txt",
"reportIssueUrl": "https://github.com/microsoft/vscode/issues/new",
"reportIssueUrl": "https://github.com/voideditor/void/issues/new",
"nodejsRepository": "https://nodejs.org",
"urlProtocol": "code-oss",
"webviewContentExternalBaseUrlTemplate": "https://{{uuid}}.vscode-cdn.net/insider/ef65ac1ba57f57f2a3961bfe94aa20481caca4c6/out/vs/workbench/contrib/webview/browser/pre/",
"urlProtocol": "void-editor",
"extensionsGallery": {
"serviceUrl": "https://open-vsx.org/vscode/gallery",
"itemUrl": "https://open-vsx.org/vscode/item"
Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 813 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 254 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 254 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 89 KiB

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 395 B

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 338 B

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 MiB

+1 -1
View File
@@ -159,7 +159,7 @@ export class InputBox extends Widget {
this.scrollableElement = new ScrollableElement(this.element, { vertical: ScrollbarVisibility.Auto });
if (this.options.flexibleWidth) {
this.input.setAttribute('wrap', 'off');
this.input.setAttribute('wrap', 'on');
this.mirror.style.whiteSpace = 'pre';
this.mirror.style.wordWrap = 'initial';
}
@@ -7,7 +7,10 @@ import { IRange } from '../../../common/core/range.js';
import { EditorOption } from '../../../common/config/editorOptions.js';
// THIS FILE IS OLD!!!
// THIS FILE IS OLD + UNUSED!!!
// SEE inlineDiffsService.ts INSTEAD.
export interface IInlineDiffService {
readonly _serviceBrand: undefined;
addDiff(editor: ICodeEditor, originalText: string, modifiedRange: IRange): void;
@@ -15,6 +15,7 @@ import { IRequestService } from '../../request/common/request.js';
import { AvailableForDownload, DisablementReason, IUpdateService, State, StateType, UpdateType } from '../common/update.js';
export function createUpdateURL(platform: string, quality: string, productService: IProductService): string {
// return `https://voideditor.dev/api/update/${platform}/stable`;
return `${productService.updateUrl}/api/update/${platform}/${quality}/${productService.commit}`;
}
@@ -70,9 +71,11 @@ export abstract class AbstractUpdateService implements IUpdateService {
*/
protected async initialize(): Promise<void> {
if (!this.environmentMainService.isBuilt) {
console.log('is NOT built, canceling update service')
this.setState(State.Disabled(DisablementReason.NotBuilt));
return; // updates are never enabled when running out of sources
}
console.log('is built, continuing with update service')
if (this.environmentMainService.disableUpdates) {
this.setState(State.Disabled(DisablementReason.DisabledByEnvironment));
@@ -86,16 +89,19 @@ export abstract class AbstractUpdateService implements IUpdateService {
return;
}
const updateMode = this.configurationService.getValue<'none' | 'manual' | 'start' | 'default'>('update.mode');
const quality = this.getProductQuality(updateMode);
// Void - for now, always update
const updateMode = this.configurationService.getValue<'none' | 'manual' | 'start' | 'default'>('update.mode');
const quality = this.getProductQuality(updateMode);
if (!quality) {
this.setState(State.Disabled(DisablementReason.ManuallyDisabled));
this.logService.info('update#ctor - updates are disabled by user preference');
return;
}
this.url = this.buildUpdateFeedUrl(quality);
// const quality = 'stable'
this.url = this.doBuildUpdateFeedUrl(quality);
if (!this.url) {
this.setState(State.Disabled(DisablementReason.InvalidConfiguration));
this.logService.info('update#ctor - updates are disabled as the update URL is badly formed');
@@ -131,13 +137,10 @@ export abstract class AbstractUpdateService implements IUpdateService {
return updateMode === 'none' ? undefined : this.productService.quality;
}
private scheduleCheckForUpdates(delay = 60 * 60 * 1000): Promise<void> {
return timeout(delay)
.then(() => this.checkForUpdates(false))
.then(() => {
// Check again after 1 hour
return this.scheduleCheckForUpdates(60 * 60 * 1000);
});
private async scheduleCheckForUpdates(delay = 60 * 60 * 1000): Promise<void> {
await timeout(delay);
await this.checkForUpdates(false);
return await this.scheduleCheckForUpdates(60 * 60 * 1000);
}
async checkForUpdates(explicit: boolean): Promise<void> {
@@ -160,6 +163,7 @@ export abstract class AbstractUpdateService implements IUpdateService {
await this.doDownloadUpdate(this.state);
}
// override implemented by windows and linux
protected async doDownloadUpdate(state: AvailableForDownload): Promise<void> {
// noop
}
@@ -174,6 +178,7 @@ export abstract class AbstractUpdateService implements IUpdateService {
await this.doApplyUpdate();
}
// windows overrides this
protected async doApplyUpdate(): Promise<void> {
// noop
}
@@ -236,6 +241,6 @@ export abstract class AbstractUpdateService implements IUpdateService {
// noop
}
protected abstract buildUpdateFeedUrl(quality: string): string | undefined;
protected abstract doBuildUpdateFeedUrl(quality: string): string | undefined;
protected abstract doCheckForUpdates(context: any): void;
}
@@ -73,7 +73,7 @@ export class DarwinUpdateService extends AbstractUpdateService implements IRelau
this.setState(State.Idle(UpdateType.Archive, message));
}
protected buildUpdateFeedUrl(quality: string): string | undefined {
protected doBuildUpdateFeedUrl(quality: string): string | undefined {
let assetID: string;
if (!this.productService.darwinUniversalAssetId) {
assetID = process.arch === 'x64' ? 'darwin' : 'darwin-arm64';
@@ -30,7 +30,7 @@ export class LinuxUpdateService extends AbstractUpdateService {
super(lifecycleMainService, configurationService, environmentMainService, requestService, logService, productService);
}
protected buildUpdateFeedUrl(quality: string): string {
protected doBuildUpdateFeedUrl(quality: string): string {
return createUpdateURL(`linux-${process.arch}`, quality, this.productService);
}
@@ -99,7 +99,7 @@ export class Win32UpdateService extends AbstractUpdateService implements IRelaun
await super.initialize();
}
protected buildUpdateFeedUrl(quality: string): string | undefined {
protected doBuildUpdateFeedUrl(quality: string): string | undefined {
let platform = `win32-${process.arch}`;
if (getUpdateType() === UpdateType.Archive) {
@@ -1,4 +1,7 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Glass Devtools, Inc. All rights reserved.
* Void Editor additions licensed under the AGPL 3.0 License.
*--------------------------------------------------------------------------------------------*/
// ---------- common ----------
@@ -22,7 +22,10 @@ type RefreshableState = ({
state: 'refreshing',
timeoutId: NodeJS.Timeout | null, // the timeoutId of the most recent call to refreshModels
} | {
state: 'success',
state: 'finished',
timeoutId: null,
} | {
state: 'finished_invisible',
timeoutId: null,
})
@@ -82,7 +85,7 @@ export class RefreshModelService extends Disposable implements IRefreshModelServ
for (const providerName of refreshableProviderNames) {
const { _enabled: enabled } = this.voidSettingsService.state.settingsOfProvider[providerName]
this.refreshModels(providerName, !enabled)
this.refreshModels(providerName, !enabled, { isPolling: true, isInternal: true })
// every time providerName.enabled changes, refresh models too, like a useEffect
let relevantVals = () => refreshBasedOn[providerName].map(settingName => this.voidSettingsService.state.settingsOfProvider[providerName][settingName])
@@ -98,7 +101,7 @@ export class RefreshModelService extends Disposable implements IRefreshModelServ
// if it was just enabled, or there was a change and it wasn't to the enabled state, refresh
if ((enabled && !prevEnabled) || (!enabled && !prevEnabled)) {
// if user just clicked enable, refresh
this.refreshModels(providerName, !enabled)
this.refreshModels(providerName, !enabled, { isPolling: false, isInternal: true })
}
else {
// else if user just clicked disable, don't refresh
@@ -131,11 +134,16 @@ export class RefreshModelService extends Disposable implements IRefreshModelServ
// start listening for models (and don't stop until success)
async refreshModels(providerName: RefreshableProviderName, enableProviderOnSuccess?: boolean) {
async refreshModels(providerName: RefreshableProviderName, enableProviderOnSuccess?: boolean, options?: { isPolling?: boolean, isInternal?: boolean }) {
const { isPolling, isInternal } = options ?? {}
console.log(`refreshModels, isInternal ${isInternal} isPolling ${isPolling}`)
this._clearProviderTimeout(providerName)
// start loading models
this._setRefreshState(providerName, 'refreshing')
if (!isInternal) this._setRefreshState(providerName, 'refreshing')
const fn = providerName === 'ollama' ? this.llmMessageService.ollamaList
: providerName === 'openAICompatible' ? this.llmMessageService.openAICompatibleList
@@ -153,15 +161,22 @@ export class RefreshModelService extends Disposable implements IRefreshModelServ
this.voidSettingsService.setSettingOfProvider(providerName, '_enabled', true)
}
this._setRefreshState(providerName, 'success')
if (!isInternal) this._setRefreshState(providerName, 'finished')
},
onError: ({ error }) => {
// poll
console.log('retrying list models:', providerName, error)
const timeoutId = setTimeout(() => this.refreshModels(providerName, enableProviderOnSuccess), REFRESH_INTERVAL)
this._setTimeoutId(providerName, timeoutId)
}
})
if (isInternal) this._setRefreshState(providerName, 'finished_invisible')
// check if we should poll
// if it was originally called as `isPolling` and if the `autoRefreshModels` flag is enabled
if (isPolling && this.voidSettingsService.state.featureFlagSettings.autoRefreshModels) {
const timeoutId = setTimeout(() => this.refreshModels(providerName, enableProviderOnSuccess, options), REFRESH_INTERVAL)
this._setTimeoutId(providerName, timeoutId)
}
}
_clearAllTimeouts() {
@@ -222,7 +222,7 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService {
setDefaultModels(providerName: ProviderName, newDefaultModelNames: string[]) {
const { models } = this.state.settingsOfProvider[providerName]
const newDefaultModels = modelInfoOfDefaultNames(newDefaultModelNames)
const newDefaultModels = modelInfoOfDefaultNames(newDefaultModelNames, { isAutodetected: true, existingModels: models })
const newModels = [
...newDefaultModels,
...models.filter(m => !m.isDefault), // keep any non-default models
@@ -11,12 +11,32 @@ export type VoidModelInfo = {
modelName: string,
isDefault: boolean, // whether or not it's a default for its provider
isHidden: boolean, // whether or not the user is hiding it
isAutodetected?: boolean, // whether the model was autodetected by polling
}
type ModelInfoOfDefaultNamesOptions = { isAutodetected: true, existingModels: VoidModelInfo[] } // | { isOtherOption: true, ...otherOptions }
export const modelInfoOfDefaultNames = (modelNames: string[], options?: ModelInfoOfDefaultNamesOptions): VoidModelInfo[] => {
export const modelInfoOfDefaultNames = (modelNames: string[]): VoidModelInfo[] => {
const { isAutodetected, existingModels } = options ?? {}
const isDefault = true
const isHidden = modelNames.length >= 10 // hide all models if there are a ton of them, and make user enable them individually
return modelNames.map((modelName, i) => ({ modelName, isDefault: true, isHidden }))
if (!existingModels) {
return modelNames.map((modelName, i) => ({ modelName, isDefault, isAutodetected, isHidden, }))
} else {
// keep existing `isHidden` property
const existingModelsMap: Record<string, VoidModelInfo> = {}
for (const em of existingModels) {
existingModelsMap[em.modelName] = em
}
return modelNames.map((modelName, i) => ({ modelName, isDefault, isAutodetected, isHidden: !!existingModelsMap[modelName]?.isHidden, }))
}
}
// https://docs.anthropic.com/en/docs/about-claude/models
@@ -121,11 +141,11 @@ export const defaultProviderSettings = {
}
} as const
export type ProviderName = keyof typeof defaultProviderSettings
export const providerNames = Object.keys(defaultProviderSettings) as ProviderName[]
export const localProviderNames = ['ollama', 'openAICompatible'] satisfies ProviderName[] // all local names
export const nonlocalProviderNames = providerNames.filter((name) => !(localProviderNames as string[]).includes(name)) // all non-local names
type CustomSettingName = UnionOfKeys<typeof defaultProviderSettings[ProviderName]>
type CustomProviderSettings<providerName extends ProviderName> = {
@@ -157,6 +177,7 @@ export type SettingName = keyof SettingsForProvider<ProviderName>
type DisplayInfoForProviderName = {
title: string,
desc?: string,
}
export const displayInfoOfProviderName = (providerName: ProviderName): DisplayInfoForProviderName => {
@@ -222,7 +243,7 @@ export const displayInfoOfSettingName = (providerName: ProviderName, settingName
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 === 'openAICompatible' ? undefined :
providerName === 'openAICompatible' ? 'Add any OpenAI-Compatible endpoint.' :
undefined,
}
}
@@ -236,7 +257,7 @@ export const displayInfoOfSettingName = (providerName: ProviderName, settingName
: providerName === 'openAICompatible' ? 'https://my-website.com/v1'
: '(never)',
subTextMd: providerName === 'ollama' ? 'Read about Ollama [Endpoints here](https://github.com/ollama/ollama/blob/main/docs/faq.md#how-can-i-expose-ollama-on-my-network).' :
subTextMd: providerName === 'ollama' ? 'If you would like to change this endpoint, please read more about [Endpoints here](https://github.com/ollama/ollama/blob/main/docs/faq.md#how-can-i-expose-ollama-on-my-network).' :
undefined,
}
}
@@ -359,7 +380,7 @@ export const featureNames = ['Ctrl+L', 'Ctrl+K', 'Autocomplete'] as const
// the models of these can be refreshed (in theory all can, but not all should)
export const refreshableProviderNames = ['ollama', 'openAICompatible'] satisfies ProviderName[]
export const refreshableProviderNames = localProviderNames
export type RefreshableProviderName = typeof refreshableProviderNames[number]
@@ -385,7 +406,7 @@ type FeatureFlagDisplayInfo = {
export const displayInfoOfFeatureFlag = (featureFlag: FeatureFlagName): FeatureFlagDisplayInfo => {
if (featureFlag === 'autoRefreshModels') {
return {
description: `Automatically scan for and enable local models.`, // ${`refreshableProviderNames.map(providerName => titleOfProviderName(providerName)).join(', ')`}
description: `Automatically detect local providers and models (${refreshableProviderNames.map(providerName => displayInfoOfProviderName(providerName).title).join(', ')}).`,
}
}
throw new Error(`featureFlagInfo: Unknown feature flag: "${featureFlag}"`)
@@ -170,6 +170,9 @@ export class EditorGroupWatermark extends Disposable {
this.clear();
const box = append(this.shortcuts, $('.watermark-box'));
const boxBelow = append(this.shortcuts, $(''))
boxBelow.style.display = 'flex'
boxBelow.style.flex = 'row'
boxBelow.style.justifyContent = 'center'
const update = async () => {
@@ -184,9 +187,13 @@ export class EditorGroupWatermark extends Disposable {
// Void - if the workbench is empty, show open
if (this.contextService.getWorkbenchState() === WorkbenchState.EMPTY) {
// Open Folder
// Open a folder
const button = h('button')
button.root.textContent = 'Open Folder'
button.root.classList.add('void-watermark-button')
button.root.style.display = 'block'
button.root.style.marginLeft = 'auto'
button.root.style.marginRight = 'auto'
button.root.textContent = 'Open a folder'
button.root.onclick = () => {
this.commandService.executeCommand(isMacintosh && isNative ? OpenFileFolderAction.ID : OpenFolderAction.ID)
// if (this.contextKeyService.contextMatchesRules(ContextKeyExpr.and(WorkbenchStateContext.isEqualTo('workspace')))) {
@@ -197,58 +204,66 @@ export class EditorGroupWatermark extends Disposable {
}
box.appendChild(button.root);
// Recents
const recentlyOpened = await this.workspacesService.getRecentlyOpened()
.catch(() => ({ files: [], workspaces: [] })).then(w => w.workspaces);
if (recentlyOpened.length !== 0) {
box.append(
...recentlyOpened.map(w => {
const span = $('div')
span.textContent = 'Recent'
span.style.fontWeight = '500'
box.append(span)
let fullPath: string;
let windowOpenable: IWindowOpenable;
if (isRecentFolder(w)) {
windowOpenable = { folderUri: w.folderUri };
fullPath = w.label || this.labelService.getWorkspaceLabel(w.folderUri, { verbose: Verbosity.LONG });
}
else {
return null
// fullPath = w.label || this.labelService.getWorkspaceLabel(w.workspace, { verbose: Verbosity.LONG });
// windowOpenable = { workspaceUri: w.workspace.configPath };
}
box.append(
...recentlyOpened.map(w => {
let fullPath: string;
let windowOpenable: IWindowOpenable;
if (isRecentFolder(w)) {
windowOpenable = { folderUri: w.folderUri };
fullPath = w.label || this.labelService.getWorkspaceLabel(w.folderUri, { verbose: Verbosity.LONG });
}
else {
return null
// fullPath = w.label || this.labelService.getWorkspaceLabel(w.workspace, { verbose: Verbosity.LONG });
// windowOpenable = { workspaceUri: w.workspace.configPath };
}
const { name, parentPath } = splitRecentLabel(fullPath);
const { name, parentPath } = splitRecentLabel(fullPath);
const li = $('li');
const link = $('span');
link.classList.add('void-link')
const li = $('li');
const link = $('button.button-link');
link.innerText = name;
link.title = fullPath;
link.setAttribute('aria-label', localize('welcomePage.openFolderWithPath', "Open folder {0} with path {1}", name, parentPath));
link.addEventListener('click', e => {
this.hostService.openWindow([windowOpenable], {
forceNewWindow: e.ctrlKey || e.metaKey,
remoteAuthority: w.remoteAuthority || null // local window if remoteAuthority is not set or can not be deducted from the openable
link.innerText = name;
link.title = fullPath;
link.setAttribute('aria-label', localize('welcomePage.openFolderWithPath', "Open folder {0} with path {1}", name, parentPath));
link.addEventListener('click', e => {
this.hostService.openWindow([windowOpenable], {
forceNewWindow: e.ctrlKey || e.metaKey,
remoteAuthority: w.remoteAuthority || null // local window if remoteAuthority is not set or can not be deducted from the openable
});
e.preventDefault();
e.stopPropagation();
});
e.preventDefault();
e.stopPropagation();
});
li.appendChild(link);
li.appendChild(link);
const span = $('span');
span.classList.add('path');
span.classList.add('detail');
span.innerText = parentPath;
span.title = fullPath;
li.appendChild(span);
const span = $('span');
span.style.paddingLeft = '4px';
span.classList.add('path');
span.classList.add('detail');
span.innerText = parentPath;
span.title = fullPath;
li.appendChild(span);
return li
}).filter(v => !!v)
)
return li
}).filter(v => !!v)
)
}
@@ -280,6 +295,11 @@ export class EditorGroupWatermark extends Disposable {
const keys3 = this.keybindingService.lookupKeybinding('workbench.action.openGlobalKeybindings');
const button3 = append(boxBelow, $('button'));
button3.textContent = 'Void Settings'
button3.style.display = 'block'
button3.style.marginLeft = 'auto'
button3.style.marginRight = 'auto'
button3.classList.add('void-settings-watermark-button')
const label3 = new KeybindingLabel(button3, OS, { renderUnboundKeybindings: true, ...defaultKeybindingLabelStyles });
if (keys3)
label3.set(keys3);
@@ -289,6 +289,7 @@ export interface IFileTemplateData {
readonly templateDisposables: DisposableStore;
readonly elementDisposables: DisposableStore;
readonly label: IResourceLabel;
readonly voidLabels: IResourceLabel;
readonly container: HTMLElement;
readonly contribs: IExplorerFileContribution[];
currentContext?: ExplorerItem;
@@ -347,15 +348,24 @@ export class FilesRenderer implements ICompressibleTreeRenderer<ExplorerItem, Fu
renderTemplate(container: HTMLElement): IFileTemplateData {
const templateDisposables = new DisposableStore();
// Create void buttons container
const voidButtonsContainer = DOM.append(container, DOM.$('div'));
voidButtonsContainer.style.position = 'absolute'
voidButtonsContainer.style.top = '0'
voidButtonsContainer.style.right = '0'
// const voidButtons = DOM.append(voidButtonsContainer, DOM.$('span'));
// voidButtons.textContent = 'voidbuttons'
// voidButtons.addEventListener('click', () => {
// console.log('ON CLICK', templateData.currentContext?.children)
// })
const voidLabels = this.labels.create(voidButtonsContainer, { supportHighlights: false, supportIcons: false, });
voidLabels.element.textContent = 'hi333'
const label = templateDisposables.add(this.labels.create(container, { supportHighlights: true }));
templateDisposables.add(label.onDidRender(() => {
try {
if (templateData.currentContext) {
this.updateWidth(templateData.currentContext);
}
} catch (e) {
// noop since the element might no longer be in the tree, no update of width necessary
}
try { if (templateData.currentContext) this.updateWidth(templateData.currentContext); }
catch (e) { /* noop since the element might no longer be in the tree, no update of width necessary*/ }
}));
const contribs = explorerFileContribRegistry.create(this.instantiationService, container, templateDisposables);
@@ -365,10 +375,12 @@ export class FilesRenderer implements ICompressibleTreeRenderer<ExplorerItem, Fu
contr.setResource(templateData.currentContext?.resource);
}));
const templateData: IFileTemplateData = { templateDisposables, elementDisposables: templateDisposables.add(new DisposableStore()), label, container, contribs };
const templateData: IFileTemplateData = { templateDisposables, elementDisposables: templateDisposables.add(new DisposableStore()), label, voidLabels, container, contribs };
return templateData;
}
// Void cares about this function, this is where elements in the tree are rendered
renderElement(node: ITreeNode<ExplorerItem, FuzzyScore>, index: number, templateData: IFileTemplateData): void {
const stat = node.element;
templateData.currentContext = stat;
@@ -382,8 +394,7 @@ export class FilesRenderer implements ICompressibleTreeRenderer<ExplorerItem, Fu
templateData.label.element.style.display = 'flex';
this.renderStat(stat, stat.name, undefined, node.filterData, templateData);
}
// Input Box
// Input Box (Void - shown only if currently editing - this is the box that appears when user edits the name of the file)
else {
templateData.label.element.style.display = 'none';
templateData.contribs.forEach(c => c.setResource(undefined));
@@ -477,6 +488,13 @@ export class FilesRenderer implements ICompressibleTreeRenderer<ExplorerItem, Fu
separator: this.labelService.getSeparator(stat.resource.scheme, stat.resource.authority),
domId
});
templateData.voidLabels.setResource({ resource: undefined, name: 'hi', }, {
hideIcon: true,
extraClasses: realignNestedChildren ? [...extraClasses, 'align-nest-icon-with-parent-icon'] : extraClasses,
forceLabel: true,
});
}
private renderInputBox(container: HTMLElement, stat: ExplorerItem, editableData: IEditableData): IDisposable {
@@ -95,7 +95,12 @@ suite('Files - ExplorerView', () => {
label: <any>{
container: label,
onDidRender: emitter.event
}
},
voidLabels: <any>{
container: label,
onDidRender: emitter.event
},
}, 1, false);
ds.add(navigationController);
@@ -17,6 +17,7 @@ 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
@@ -165,20 +166,6 @@ const postprocessResult = (result: string) => {
}
const extractCodeFromResult = (result: string) => {
// Match either:
// 1. ```language\n<code>```
// 2. ```<code>```
const match = result.match(/```(?:\w+\n)?([\s\S]*?)```|```([\s\S]*?)```/);
if (!match) {
return result;
}
// Return whichever group matched (non-empty)
return match[1] ?? match[2] ?? result;
}
// trims the end of the prefix to improve cache hit rate
const removeLeftTabsAndTrimEnd = (s: string): string => {
@@ -665,7 +652,7 @@ export class AutocompleteService extends Disposable implements IAutocompleteServ
// newAutocompletion.abortRef = { current: () => { } }
newAutocompletion.status = 'finished'
// newAutocompletion.promise = undefined
newAutocompletion.insertText = postprocessResult(extractCodeFromResult(fullText))
newAutocompletion.insertText = postprocessResult(extractCodeFromRegular(fullText))
resolve(newAutocompletion.insertText)
@@ -768,3 +755,5 @@ export class AutocompleteService extends Disposable implements IAutocompleteServ
registerSingleton(IAutocompleteService, AutocompleteService, InstantiationType.Eager);
@@ -1,3 +1,8 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Glass Devtools, Inc. All rights reserved.
* Void Editor additions licensed under the AGPL 3.0 License.
*--------------------------------------------------------------------------------------------*/
import { Disposable } from '../../../../../base/common/lifecycle.js';
import { URI } from '../../../../../base/common/uri.js';
import { generateUuid } from '../../../../../base/common/uuid.js';
@@ -9,7 +14,6 @@ import { createDecorator } from '../../../../../platform/instantiation/common/in
// lets you add a "consistent" item to a Model (aka URI), instead of just to a single editor
type AddItemInputs = { uri: URI; fn: (editor: ICodeEditor) => (() => void); }
export interface IConsistentItemService {
@@ -56,6 +60,7 @@ export class ConsistentItemService extends Disposable {
}
// when editor switches tabs (models)
const addTabSwitchListeners = (editor: ICodeEditor) => {
this._register(
editor.onDidChangeModel(e => {
@@ -65,6 +70,7 @@ export class ConsistentItemService extends Disposable {
)
}
// when editor is disposed
const addDisposeListener = (editor: ICodeEditor) => {
this._register(editor.onDidDispose(() => {
// anything on the editor has been disposed already
@@ -176,3 +182,243 @@ export class ConsistentItemService extends Disposable {
registerSingleton(IConsistentItemService, ConsistentItemService, InstantiationType.Eager);
// mostly generated by o1 (almost the same as above, but just for 1 editor)
export interface IConsistentEditorItemService {
readonly _serviceBrand: undefined;
addToEditor(editor: ICodeEditor, fn: () => () => void): string;
removeFromEditor(itemId: string): void;
}
export const IConsistentEditorItemService = createDecorator<IConsistentEditorItemService>('ConsistentEditorItemService');
export class ConsistentEditorItemService extends Disposable {
readonly _serviceBrand: undefined;
/**
* For each editorId, we track the set of itemIds that have been "added" to that editor.
* This does *not* necessarily mean they're currently mounted (the user may have switched models).
*/
private readonly itemIdsByEditorId: Record<string, Set<string>> = {};
/**
* For each itemId, we store relevant info (the fn to call on the editor, the editorId, the uri, and the current dispose function).
*/
private readonly itemInfoById: Record<
string,
{
editorId: string;
uriFsPath: string;
fn: (editor: ICodeEditor) => () => void;
disposeFn?: () => void;
}
> = {};
constructor(
@ICodeEditorService private readonly _editorService: ICodeEditorService,
) {
super();
//
// Wire up listeners to watch for new editors, removed editors, etc.
//
// Initialize any already-existing editors
for (const editor of this._editorService.listCodeEditors()) {
this._initializeEditor(editor);
}
// When an editor is added, track it
this._register(
this._editorService.onCodeEditorAdd((editor) => {
this._initializeEditor(editor);
})
);
// When an editor is removed, remove all items associated with that editor
this._register(
this._editorService.onCodeEditorRemove((editor) => {
this._removeAllItemsFromEditor(editor);
})
);
}
/**
* Sets up listeners on the provided editor so that:
* - If the editor changes models, we remove items and re-mount only if the new model matches.
* - If the editor is disposed, we do the needed cleanup.
*/
private _initializeEditor(editor: ICodeEditor) {
const editorId = editor.getId();
//
// Listen for model changes
//
this._register(
editor.onDidChangeModel((e) => {
this._removeAllItemsFromEditor(editor);
if (!e.newModelUrl) {
return;
}
// Re-mount any items that belong to this editor and match the new URI
const itemsForEditor = this.itemIdsByEditorId[editorId];
if (itemsForEditor) {
for (const itemId of itemsForEditor) {
const itemInfo = this.itemInfoById[itemId];
if (itemInfo && itemInfo.uriFsPath === e.newModelUrl.fsPath) {
this._mountItemOnEditor(editor, itemId);
}
}
}
})
);
//
// When the editor is disposed, remove all items from it
//
this._register(
editor.onDidDispose(() => {
this._removeAllItemsFromEditor(editor);
})
);
//
// If the editor already has a model (e.g. on initial load), try mounting items
//
const uri = editor.getModel()?.uri;
if (!uri) {
return;
}
const itemsForEditor = this.itemIdsByEditorId[editorId];
if (itemsForEditor) {
for (const itemId of itemsForEditor) {
const itemInfo = this.itemInfoById[itemId];
if (itemInfo && itemInfo.uriFsPath === uri.fsPath) {
this._mountItemOnEditor(editor, itemId);
}
}
}
}
/**
* Actually calls the item-creation function `fn(editor)` and saves the resulting disposeFn
* so we can later clean it up.
*/
private _mountItemOnEditor(editor: ICodeEditor, itemId: string) {
const info = this.itemInfoById[itemId];
if (!info) {
return;
}
const { fn } = info;
const disposeFn = fn(editor);
info.disposeFn = disposeFn;
}
/**
* Removes a single item from an editor (calling its `disposeFn` if present).
*/
private _removeItemFromEditor(editor: ICodeEditor, itemId: string) {
const info = this.itemInfoById[itemId];
if (info?.disposeFn) {
info.disposeFn();
info.disposeFn = undefined;
}
}
/**
* Removes *all* items from the given editor. Typically called when the editor changes model or is disposed.
*/
private _removeAllItemsFromEditor(editor: ICodeEditor) {
const editorId = editor.getId();
const itemsForEditor = this.itemIdsByEditorId[editorId];
if (!itemsForEditor) {
return;
}
for (const itemId of itemsForEditor) {
this._removeItemFromEditor(editor, itemId);
}
}
/**
* Public API: Adds an item to an *individual* editor (determined by editor ID),
* but only when that editor is showing the same model (uri.fsPath).
*/
addToEditor(editor: ICodeEditor, fn: () => () => void): string {
const uri = editor.getModel()?.uri
if (!uri) {
throw new Error('No URI on the provided editor or in AddItemInputs.');
}
const editorId = editor.getId();
// Create an ID for this item
const itemId = generateUuid();
// Record the info
this.itemInfoById[itemId] = {
editorId,
uriFsPath: uri.fsPath,
fn,
};
// Add to the editor's known items
if (!this.itemIdsByEditorId[editorId]) {
this.itemIdsByEditorId[editorId] = new Set();
}
this.itemIdsByEditorId[editorId].add(itemId);
// If the editor's current URI matches, mount it now
if (editor.getModel()?.uri.fsPath === uri.fsPath) {
this._mountItemOnEditor(editor, itemId);
}
return itemId;
}
/**
* Public API: Removes an item from the *specific* editor. We look up which editor
* had this item and remove it from that editor.
*/
removeFromEditor(itemId: string): void {
const info = this.itemInfoById[itemId];
if (!info) {
// Nothing to remove
return;
}
const { editorId } = info;
// Find the editor in question
const editor = this._editorService.listCodeEditors().find(
(ed) => ed.getId() === editorId
);
if (editor) {
// Dispose on that editor
this._removeItemFromEditor(editor, itemId);
}
// Clean up references
this.itemIdsByEditorId[editorId]?.delete(itemId);
delete this.itemInfoById[itemId];
}
}
registerSingleton(IConsistentEditorItemService, ConsistentEditorItemService, InstantiationType.Eager);
@@ -0,0 +1,74 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Glass Devtools, Inc. All rights reserved.
* Void Editor additions licensed under the AGPL 3.0 License.
*--------------------------------------------------------------------------------------------*/
// modelWasTrainedOnFIM should be false here
export const extractCodeFromFIM = ({ text, midTag, modelWasTrainedOnFIM }: { text: string, midTag: string, modelWasTrainedOnFIM: false }) => {
/* desired matches
`
``
```
<
<P
<PR
<PRE
<PRE>
<PRE> a
<PRE> a </PRE>
<PRE> a </PRE><
<PRE> a </PRE><M
<PRE> a </PRE><MI
<PRE> a </PRE><MID
<PRE> a </PRE><MID>
<PRE> a <PRE/> ->
*/
/* ------------- summary of the regex -------------
[optional ` | `` | ```]
(match optional_language_name)
[optional strings here]
[required <MID> tag]
(match the stuff between mid tags)
[optional <MID/> tag]
[optional ` | `` | ```]
*/
// const regex = /[\s\S]*?(?:`{1,3}\s*([a-zA-Z_]+[\w]*)?[\s\S]*?)?<MID>([\s\S]*?)(?:<\/MID>|`{1,3}|$)/;
const regex = new RegExp(
`[\\s\\S]*?(?:\`{1,3}\\s*([a-zA-Z_]+[\\w]*)?[\\s\\S]*?)?<${midTag}>([\\s\\S]*?)(?:</${midTag}>|\`{1,3}|$)`,
''
);
const match = text.match(regex);
if (match) {
const [_, languageName, codeBetweenMidTags] = match;
return [languageName, codeBetweenMidTags] as const
} else {
return [undefined, extractCodeFromRegular(text)] as const
}
}
export const extractCodeFromRegular = (result: string) => {
// Match either:
// 1. ```language\n<code>```
// 2. ```<code>```
// 4 <PRE> A
// 3. <PRE> A </PRE><MID> B </MID> -> B
const match = result.match(/```(?:\w+\n)?([\s\S]*?)```|```([\s\S]*?)```/);
if (!match) {
return result;
}
// Return whichever group matched (non-empty)
return match[1] ?? match[2] ?? result;
}
@@ -26,16 +26,17 @@ import { ILanguageService } from '../../../../editor/common/languages/language.j
import * as dom from '../../../../base/browser/dom.js';
import { Widget } from '../../../../base/browser/ui/widget.js';
import { URI } from '../../../../base/common/uri.js';
import { IConsistentItemService } from './helperServices/consistentItemService.js';
import { ctrlKStream_prefixAndSuffix, ctrlKStream_prompt, ctrlKStream_systemMessage, ctrlLStream_prompt, ctrlLStream_systemMessage } from './prompt/prompts.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 { ILLMMessageService } from '../../../../platform/void/common/llmMessageService.js';
import { IPosition } from '../../../../editor/common/core/position.js';
import { mountCtrlK } from '../browser/react/out/ctrl-k-tsx/index.js'
import { mountCtrlK } from '../browser/react/out/quick-edit-tsx/index.js'
import { QuickEditPropsType } from './quickEditActions.js';
import { InputBox } from '../../../../base/browser/ui/inputbox/inputBox.js';
import { LLMMessage } from '../../../../platform/void/common/llmMessageTypes.js';
import { IModelContentChangedEvent } from '../../../../editor/common/textModelEvents.js';
import { extractCodeFromFIM, extractCodeFromRegular } from './helpers/extractCodeFromResult.js';
const configOfBG = (color: Color) => {
return { dark: color, light: color, hcDark: color, hcLight: color, }
@@ -117,7 +118,7 @@ type CtrlKZone = {
editorId: string; // the editor the input lives on
_mountInfo: null | {
inputBox: InputBox; // the input box that lives in the zone
inputBoxRef: { current: InputBox | null }; // the input box that lives in the zone
dispose: () => void;
refresh: () => void;
}
@@ -136,7 +137,7 @@ type DiffZone = {
} | {
isStreaming: false;
streamRequestIdRef?: undefined;
line: null;
line?: undefined;
};
editorId?: undefined;
} & CommonZoneProps
@@ -171,6 +172,7 @@ export interface IInlineDiffsService {
startApplying(opts: StartApplyingOpts): number | undefined;
interruptStreaming(diffareaid: number): void;
addCtrlKZone(opts: AddCtrlKOpts): number | undefined;
removeCtrlKZone(opts: { diffareaid: number }): void;
}
export const IInlineDiffsService = createDecorator<IInlineDiffsService>('inlineDiffAreasService');
@@ -195,6 +197,7 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService {
@ILLMMessageService private readonly _llmMessageService: ILLMMessageService,
@IConsistentItemService private readonly _consistentItemService: IConsistentItemService,
@IInstantiationService private readonly _instantiationService: IInstantiationService,
@IConsistentEditorItemService private readonly _consistentEditorItemService: IConsistentEditorItemService,
) {
super();
@@ -208,9 +211,8 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService {
this._register(
model.onDidChangeContent(e => {
// it's as if we just called _write, now all we need to do is realign and refresh
const uri = model.uri
if (this.weAreWriting) return
const uri = model.uri
this._onUserChangeContent(uri, e)
})
)
@@ -242,6 +244,7 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService {
private _onInternalChangeContent(uri: URI, { shouldRealign }: { shouldRealign: false | { newText: string, oldRange: IRange } }) {
if (shouldRealign) {
const { newText, oldRange } = shouldRealign
console.log('realiging', newText, oldRange)
this._realignAllDiffAreasLines(uri, newText, oldRange)
}
this._refreshStylesAndDiffsInURI(uri)
@@ -319,76 +322,78 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService {
mostRecentTextOfCtrlKZoneId: Record<string, string | undefined> = {}
private _addCtrlKZoneInput = async (ctrlKZone: CtrlKZone) => {
private _addCtrlKZoneInput = (ctrlKZone: CtrlKZone) => {
const { editorId } = ctrlKZone
const editor = this._editorService.listCodeEditors().find(e => e.getId() === editorId)
if (!editor) {
console.error('editor not found')
return null
}
if (!editor) { return null }
const domNode = document.createElement('div');
domNode.style.zIndex = '1'
const viewZone: IViewZone = {
afterLineNumber: ctrlKZone.startLine - 1,
domNode: domNode,
heightInPx: 0,
suppressMouseDown: false,
};
// mount zone
let zoneId: string | null = null
editor.changeViewZones(accessor => {
zoneId = accessor.addZone(viewZone)
})
let viewZone_: IViewZone | null = null
const inputBoxRef: { current: InputBox | null } = { current: null }
const itemId = this._consistentEditorItemService.addToEditor(editor, () => {
const domNode = document.createElement('div');
domNode.style.zIndex = '1'
const viewZone: IViewZone = {
afterLineNumber: ctrlKZone.startLine - 1,
domNode: domNode,
heightInPx: 52,
suppressMouseDown: false,
};
viewZone_ = viewZone
let res_: (inputBox: InputBox) => void
const inputBoxPromise: Promise<InputBox> = new Promise((res, rej) => { res_ = res })
// mount zone
editor.changeViewZones(accessor => {
zoneId = accessor.addZone(viewZone)
})
// mount react
this._instantiationService.invokeFunction(accessor => {
const props: QuickEditPropsType = {
diffareaid: ctrlKZone.diffareaid,
onGetInputBox(inputBox) {
res_(inputBox)
// not sure why this requries a timeout
setTimeout(() => inputBox.focus(), 0)
},
onChangeHeight(height) {
if (height === undefined) return
viewZone.heightInPx = height
// re-render with this new height
editor.changeViewZones(accessor => {
if (zoneId) {
accessor.layoutZone(zoneId)
// mount react
this._instantiationService.invokeFunction(accessor => {
mountCtrlK(domNode, accessor, {
diffareaid: ctrlKZone.diffareaid,
onGetInputBox: (inputBox) => {
inputBoxRef.current = inputBox
// if it's mounting for the first time, focus it
if (!(ctrlKZone.diffareaid in this.mostRecentTextOfCtrlKZoneId)) { // detect first mount this way (a hack)
this.mostRecentTextOfCtrlKZoneId[ctrlKZone.diffareaid] = undefined
setTimeout(() => inputBox.focus(), 0)
}
})
},
onUserUpdateText: (text) => { this.mostRecentTextOfCtrlKZoneId[ctrlKZone.diffareaid] = text; },
initText: this.mostRecentTextOfCtrlKZoneId[ctrlKZone.diffareaid] ?? null,
}
mountCtrlK(domNode, accessor, props)
})
},
onChangeHeight(height) {
if (height === undefined) return
viewZone.heightInPx = height
// re-render with this new height
editor.changeViewZones(accessor => {
if (zoneId) {
accessor.layoutZone(zoneId)
}
})
},
onUserUpdateText: (text) => { this.mostRecentTextOfCtrlKZoneId[ctrlKZone.diffareaid] = text; },
initText: this.mostRecentTextOfCtrlKZoneId[ctrlKZone.diffareaid] ?? null,
} satisfies QuickEditPropsType)
const inputBox = await inputBoxPromise
})
return () => editor.changeViewZones(accessor => {
if (zoneId)
accessor.removeZone(zoneId)
})
})
return {
inputBox,
inputBoxRef,
refresh: () => editor.changeViewZones(accessor => {
if (zoneId && viewZone) {
viewZone.afterLineNumber = ctrlKZone.startLine - 1
if (zoneId && viewZone_) {
viewZone_.afterLineNumber = ctrlKZone.startLine - 1
accessor.layoutZone(zoneId)
}
}),
dispose: () => {
editor.changeViewZones(accessor => {
if (zoneId)
accessor.removeZone(zoneId)
})
this._consistentEditorItemService.removeFromEditor(itemId)
},
}
} satisfies CtrlKZone['_mountInfo']
}
@@ -398,12 +403,12 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService {
const diffArea = this.diffAreaOfId[diffareaid]
if (diffArea.type !== 'CtrlKZone') continue
if (!diffArea._mountInfo) {
diffArea._mountInfo = await this._addCtrlKZoneInput(diffArea)
diffArea._mountInfo = this._addCtrlKZoneInput(diffArea)
console.log('MOUNTED', diffArea.diffareaid)
}
else {
diffArea._mountInfo.refresh()
}
}
}
@@ -574,10 +579,7 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService {
type: 'DiffZone',
_diffOfId: {},
_URI: uri,
_streamState: {
isStreaming: false,
line: null,
} as const,
_streamState: { isStreaming: false },
_removeStylesFns: new Set(),
}
}
@@ -735,17 +737,14 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService {
for (const diffareaid of this.diffAreasOfURI[model.uri.fsPath] || []) {
const diffArea = this.diffAreaOfId[diffareaid]
console.log('DA', diffArea.startLine, diffArea.endLine)
console.log('CHANGE', startLine, endLine)
// if the diffArea is entirely above the range, it is not affected
if (diffArea.endLine < startLine) {
// console.log('DA FULLY ABOVE (doing nothing)')
// console.log('CHANGE FULLY BELOW DA (doing nothing)')
continue
}
// if a diffArea is entirely below the range, shift the diffArea up/down by the delta amount of newlines
else if (endLine < diffArea.startLine) {
// console.log('DA FULLY BELOW')
// console.log('CHANGE FULLY ABOVE DA')
const changedRangeHeight = endLine - startLine + 1
const deltaNewlines = newTextHeight - changedRangeHeight
diffArea.startLine += deltaNewlines
@@ -766,16 +765,16 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService {
}
// if the change contains only the diffArea's top
else if (startLine < diffArea.startLine && diffArea.startLine <= endLine) {
// console.log('TOP ONLY')
// console.log('CHANGE CONTAINS TOP OF DA ONLY')
const numOverlappingLines = endLine - diffArea.startLine + 1
const numRemainingLinesInDA = diffArea.endLine - diffArea.startLine + 1 - numOverlappingLines
const newHeight = numRemainingLinesInDA + (newTextHeight - 1)
const newHeight = (numRemainingLinesInDA - 1) + (newTextHeight - 1) + 1
diffArea.startLine = startLine
diffArea.endLine = startLine + newHeight
}
// if the change contains only the diffArea's bottom
else if (startLine <= diffArea.endLine && diffArea.endLine < endLine) {
// console.log('BOTTOM ONLY')
// console.log('CHANGE CONTAINS BOTTOM OF DA ONLY')
const numOverlappingLines = diffArea.endLine - startLine + 1
diffArea.endLine += newTextHeight - numOverlappingLines
}
@@ -818,47 +817,42 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService {
// if streaming, use diffs to figure out where to write new code
// these are two different coordinate systems - new and old line number
let newFileEndLine: number // get new[0...newStoppingPoint] with line=newStoppingPoint highlighted
let originalCodeStartLine: number // get original[oldStartingPoint...]
let newCodeEndLine: number // get file[diffArea.startLine...newFileEndLine] with line=newFileEndLine highlighted
let originalCodeStartLine: number // get original[oldStartingPoint...] (line in the original code, so starts at 1)
const lastDiff = computedDiffs.pop()
if (!lastDiff) {
// console.log('!lastDiff')
// if the writing is identical so far, display no changes
newFileEndLine = diffZone.startLine
originalCodeStartLine = 1
newCodeEndLine = 1
}
else {
if (lastDiff.type === 'insertion') {
newFileEndLine = lastDiff.endLine
originalCodeStartLine = lastDiff.originalStartLine
}
else if (lastDiff.type === 'deletion') {
newFileEndLine = lastDiff.startLine
originalCodeStartLine = lastDiff.originalStartLine
}
else if (lastDiff.type === 'edit') {
newFileEndLine = lastDiff.endLine
originalCodeStartLine = lastDiff.originalStartLine
}
else {
originalCodeStartLine = lastDiff.originalStartLine
if (lastDiff.type === 'insertion' || lastDiff.type === 'edit')
newCodeEndLine = lastDiff.endLine
else if (lastDiff.type === 'deletion')
newCodeEndLine = lastDiff.startLine
else
throw new Error(`Void: diff.type not recognized on: ${lastDiff}`)
}
}
diffZone._streamState.line = newFileEndLine
// lines are 1-indexed
const newFileTop = llmText.split('\n').slice(diffZone.startLine, (newFileEndLine - 1)).join('\n')
const oldFileBottom = diffZone.originalCode.split('\n').slice((originalCodeStartLine - 1), Infinity).join('\n')
const newCodeTop = llmText.split('\n').slice(0, (newCodeEndLine - 1) + 1).join('\n')
const oldFileBottom = diffZone.originalCode.split('\n').slice((originalCodeStartLine - 1) + 1, Infinity).join('\n')
const newCode = `${newFileTop}\n${oldFileBottom}`
// oriignalCode[1 + line...Infinity]. Must make sure 1 + line < originalCode.length. This is another way to check:
const newCode = (newCodeTop && oldFileBottom) ? `${newCodeTop}\n${oldFileBottom}` : (oldFileBottom || newCodeTop)
this._writeText(uri, newCode,
{ startLineNumber: diffZone.startLine, startColumn: 1, endLineNumber: diffZone.endLine, endColumn: Number.MAX_SAFE_INTEGER, }, // 1-indexed
{ shouldRealignDiffAreas: true }
)
// add diffZone.startLine to convert to right coordinate system (line in file, not in diffarea)
diffZone._streamState.line = (diffZone.startLine - 1) + newCodeEndLine
return computedDiffs
@@ -866,6 +860,54 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService {
// // if streaming, use diffs to figure out where to write new code
// // these are two different coordinate systems - new and old line number
// let newFileEndLine: number // get new[0...newStoppingPoint] with line=newStoppingPoint highlighted
// let originalCodeStartLine: number // get original[oldStartingPoint...]
// const lastDiff = computedDiffs.pop()
// if (!lastDiff) {
// // if the writing is identical so far, display no changes
// newFileEndLine = diffZone.startLine
// originalCodeStartLine = 1
// }
// else {
// if (lastDiff.type === 'insertion') {
// newFileEndLine = lastDiff.endLine
// originalCodeStartLine = lastDiff.originalStartLine
// }
// else if (lastDiff.type === 'deletion') {
// newFileEndLine = lastDiff.startLine
// originalCodeStartLine = lastDiff.originalStartLine
// }
// else if (lastDiff.type === 'edit') {
// newFileEndLine = lastDiff.endLine
// originalCodeStartLine = lastDiff.originalStartLine
// }
// else {
// throw new Error(`Void: diff.type not recognized on: ${lastDiff}`)
// }
// }
// diffZone._streamState.line = newFileEndLine
// // lines are 1-indexed
// const newFileTop = llmText.split('\n').slice(diffZone.startLine, (newFileEndLine - 1)).join('\n')
// const oldFileBottom = diffZone.originalCode.split('\n').slice((originalCodeStartLine - 1), Infinity).join('\n')
// const newCode = `${newFileTop}\n${oldFileBottom}`
// this._writeText(uri, newCode,
// { startLineNumber: diffZone.startLine, startColumn: 1, endLineNumber: diffZone.endLine, endColumn: Number.MAX_SAFE_INTEGER, }, // 1-indexed
// { shouldRealignDiffAreas: true }
// )
// return computedDiffs
@@ -882,7 +924,7 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService {
if (diffArea.type !== 'CtrlKZone') continue
const noOverlap = diffArea.startLine > endLine || diffArea.endLine < startLine
if (!noOverlap) {
diffArea._mountInfo?.inputBox.focus()
setTimeout(() => diffArea._mountInfo?.inputBoxRef.current?.focus(), 0)
return
}
}
@@ -899,13 +941,24 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService {
_mountInfo: null,
}
const ctrlKZone = this._addDiffArea(adding)
this._refreshStylesAndDiffsInURI(uri)
onFinishEdit()
return ctrlKZone.diffareaid
}
public removeCtrlKZone({ diffareaid }: { diffareaid: number }) {
const ctrlKZone = this.diffAreaOfId[diffareaid]
if (!ctrlKZone) return
if (ctrlKZone.type !== 'CtrlKZone') return
const uri = ctrlKZone._URI
const { onFinishEdit } = this._addToHistory(uri)
this._deleteCtrlKZone(ctrlKZone)
this._refreshStylesAndDiffsInURI(uri)
onFinishEdit()
}
public startApplying(opts: StartApplyingOpts) {
@@ -967,14 +1020,13 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService {
startLine = startLine_
endLine = endLine_
if (!_mountInfo) return
userMessage = _mountInfo.inputBox.value
if (!_mountInfo?.inputBoxRef.current) return
userMessage = _mountInfo.inputBoxRef.current?.value
}
else {
throw new Error(`Void: diff.type not recognized on: ${featureName}`)
}
const currentFileStr = this._readURI(uri)
if (currentFileStr === null) return
const originalCode = currentFileStr.split('\n').slice((startLine - 1), (endLine - 1) + 1).join('\n')
@@ -994,6 +1046,10 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService {
// this._deleteDiffArea(ctrlKZone)
// }
// TODO ctrl+K case should be replaced with an actual check for model.isFIM
const modelWasTrainedOnFIM = featureName === 'Ctrl+K' ? false : false
const modelFimTags = defaultFimTags
const adding: Omit<DiffZone, 'diffareaid'> = {
type: 'DiffZone',
originalCode,
@@ -1022,7 +1078,7 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService {
}
else if (featureName === 'Ctrl+K') {
const { prefix, suffix } = ctrlKStream_prefixAndSuffix({ fullFileStr: currentFileStr, startLine, endLine })
const userContent = ctrlKStream_prompt({ selection: originalCode, userMessage, prefix, suffix })
const userContent = ctrlKStream_prompt({ selection: originalCode, userMessage, prefix, suffix, modelWasTrainedOnFIM, fimTags: modelFimTags })
console.log('PREFIX:\n', prefix)
console.log('SUFFIX:\n', suffix)
console.log('USER CONTENT:\n', userContent)
@@ -1039,7 +1095,7 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService {
const latestOriginalFileStart: IPosition = { lineNumber: 1, column: 1 }
const onDone = () => {
diffZone._streamState = { isStreaming: false, line: null }
diffZone._streamState = { isStreaming: false, }
if (featureName === 'Ctrl+K') {
const ctrlKZone = this.diffAreaOfId[opts.diffareaid] as CtrlKZone
@@ -1050,17 +1106,33 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService {
onFinishEdit()
}
// refresh now in case onText takes a while to get 1st message
this._refreshStylesAndDiffsInURI(uri)
const extractText = (fullText: string) => {
if (featureName === 'Ctrl+K') {
const [_, textSoFar] = extractCodeFromFIM({ text: fullText, midTag: modelFimTags.midTag, modelWasTrainedOnFIM })
return textSoFar
}
else if (featureName === 'Ctrl+L') {
return extractCodeFromRegular(fullText)
}
throw 1
}
streamRequestIdRef.current = this._llmMessageService.sendLLMMessage({
featureName,
logging: { loggingName: 'streamChunk' },
logging: { loggingName: `startApplying - ${featureName}` },
messages,
onText: ({ newText, fullText }) => {
this._writeDiffZoneLLMText(diffZone, fullText, latestCurrentFileEnd, latestOriginalFileStart)
this._writeDiffZoneLLMText(diffZone, extractText(fullText), latestCurrentFileEnd, latestOriginalFileStart)
this._refreshStylesAndDiffsInURI(uri)
},
onFinalMessage: ({ fullText }) => {
// console.log('DONE! FULL TEXT\n', extractText(fullText), diffZone.startLine, diffZone.endLine)
// at the end, re-write whole thing to make sure no sync errors
this._writeText(uri, fullText,
this._writeText(uri, extractText(fullText),
{ startLineNumber: diffZone.startLine, startColumn: 1, endLineNumber: diffZone.endLine, endColumn: Number.MAX_SAFE_INTEGER }, // 1-indexed
{ shouldRealignDiffAreas: false }
)
@@ -1092,16 +1164,12 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService {
this._llmMessageService.abort(streamRequestId)
diffZone._streamState = {
isStreaming: false,
streamRequestIdRef: undefined,
line: null
}
diffZone._streamState = { isStreaming: false, }
}
// call this outside undo/redo (it calls undo)
// call this outside undo/redo (it calls undo). this is only for aborting a diffzone stream
interruptStreaming(diffareaid: number) {
const diffArea = this.diffAreaOfId[diffareaid]
@@ -1230,8 +1298,18 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService {
// B| <-- endLine (we want to delete this whole line)
// C
else if (diff.type === 'insertion') {
writeText = ''
toRange = { startLineNumber: diff.startLine, startColumn: 1, endLineNumber: diff.endLine + 1, endColumn: 1 } // 1-indexed
// console.log('REJECTING:', diff)
// handle the case where the insertion was a newline at end of diffarea (applying to the next line doesnt work because it doesnt exist, vscode just doesnt delete the correct # of newlines)
if (diff.endLine === diffArea.endLine) {
// delete the line before instead of after
writeText = ''
toRange = { startLineNumber: diff.startLine - 1, startColumn: Number.MAX_SAFE_INTEGER, endLineNumber: diff.endLine, endColumn: 1 } // 1-indexed
}
else {
writeText = ''
toRange = { startLineNumber: diff.startLine, startColumn: 1, endLineNumber: diff.endLine + 1, endColumn: 1 } // 1-indexed
}
}
// if it was an edit, just edit the range
// (this image applies to writeText and toRange, not newOriginalCode)
@@ -22,3 +22,52 @@
.void-redBG {
background-color: var(--vscode-void-redBG);
}
.void-watermark-button {
margin: 8px 0;
padding: 8px 20px;
background-color: #3b82f6;
color: white;
border: none;
border-radius: 4px;
outline: none !important;
box-shadow: none !important;
cursor: pointer;
transition: background-color 0.2s ease;
}
.void-watermark-button:hover {
background-color: #2563eb;
}
.void-watermark-button:active {
background-color: #2563eb;
}
.void-settings-watermark-button {
margin: 8px 0;
padding: 8px 20px;
background-color: var(--vscode-input-background);
color: var(--vscode-input-foreground);
border: none;
border-radius: 4px;
outline: none !important;
box-shadow: none !important;
cursor: pointer;
transition: all 0.2s ease;
}
.void-settings-watermark-button:hover {
filter: brightness(1.1);
}
.void-settings-watermark-button:active {
filter: brightness(1.1);
}
.void-link {
color: #3b82f6;
cursor: pointer;
}
@@ -16,6 +16,7 @@ Instructions:
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.
## EXAMPLE
@@ -323,48 +324,63 @@ export const ctrlKStream_prefixAndSuffix = ({ fullFileStr, startLine, endLine }:
}
export const ctrlKStream_prompt = ({ selection, prefix, suffix, userMessage }: { selection: string, prefix: string, suffix: string, userMessage: string, }) => {
const onlySpeaksFIM = false
if (onlySpeaksFIM) {
const preTag = 'PRE'
const sufTag = 'SUF'
const midTag = 'MID'
export type FimTagsType = {
preTag: string,
sufTag: string,
midTag: string
}
export const defaultFimTags: FimTagsType = {
preTag: 'BEFORE',
sufTag: 'AFTER',
midTag: 'SELECTION',
}
export const ctrlKStream_prompt = ({ selection, prefix, suffix, userMessage, modelWasTrainedOnFIM, fimTags }: { selection: string, prefix: string, suffix: string, userMessage: string, modelWasTrainedOnFIM: boolean, fimTags: FimTagsType }) => {
const { preTag, sufTag, midTag } = fimTags
if (modelWasTrainedOnFIM) {
// const preTag = 'PRE'
// const sufTag = 'SUF'
// const midTag = 'MID'
return `\
<${preTag}>
/* Original Selection:
${selection}*/
/* Instructions: ${userMessage}*/
/* Instructions:
${userMessage}*/
${prefix}</${preTag}>
<${sufTag}>${suffix}</${sufTag}>
<${midTag}>`
}
// prompt the model on how to do FIM
// prompt the model artifically on how to do FIM
else {
const preTag = 'PRE'
const sufTag = 'SUF'
const midTag = 'MID'
// const preTag = 'BEFORE'
// const sufTag = 'AFTER'
// const midTag = 'SELECTION'
return `\
Here is the user's original selection:
The user is selecting this code as their SELECTION:
\`\`\`
<${midTag}>${selection}</${midTag}>
\`\`\`
The user wants to apply the following instructions to the selection:
The user wants to apply the following INSTRUCTIONS to the SELECTION:
${userMessage}
Please rewrite the selection following the user's instructions.
Please edit the SELECTION following the user's INSTRUCTIONS, and return the new SELECTION.
Instructions to follow:
1. Follow the user's instructions
2. You may ONLY CHANGE the selection, and nothing else in the file
3. Make sure all brackets in the new selection are balanced the same was as in the original selection
3. Be careful not to duplicate or remove variables, comments, or other syntax by mistake
Note that the SELECTION has code that comes before it. This code is indicated with <${preTag}>...before<${preTag}/>.
Note also that the SELECTION has code that comes after it. This code is indicated with <${sufTag}>...after<${sufTag}/>.
Instructions:
1. Your OUTPUT should be a SINGLE PIECE OF CODE of the form <${midTag}>...new_selection<${midTag}/>. Do not give any explanation before or after this. ONLY output this format, nothing more.
2. You may ONLY CHANGE the original SELECTION, and NOT the content in the <${preTag}>...<${preTag}/> or <${sufTag}>...<${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.
Complete the following:
<${preTag}>${prefix}</${preTag}>
<${sufTag}>${suffix}</${sufTag}>
<${midTag}>`
<${sufTag}>${suffix}</${sufTag}>`
}
};
@@ -1,3 +1,8 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Glass Devtools, Inc. All rights reserved.
* Void Editor additions licensed under the AGPL 3.0 License.
*--------------------------------------------------------------------------------------------*/
import { KeyCode, KeyMod } from '../../../../base/common/keyCodes.js';
import { Action2, registerAction2 } from '../../../../platform/actions/common/actions.js';
import { ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js';
@@ -6,6 +11,7 @@ import { IMetricsService } from '../../../../platform/void/common/metricsService
import { ICodeEditorService } from '../../../../editor/browser/services/codeEditorService.js';
import { IInlineDiffsService } from './inlineDiffsService.js';
import { InputBox } from '../../../../base/browser/ui/inputbox/inputBox.js';
import { roundRangeToLines } from './sidebarActions.js';
export type QuickEditPropsType = {
@@ -49,7 +55,7 @@ registerAction2(class extends Action2 {
if (!editor) return;
const model = editor.getModel()
if (!model) return;
const selection = editor.getSelection()
const selection = roundRangeToLines(editor.getSelection(), { emptySelectionBehavior: 'line' })
if (!selection) return;
@@ -1,3 +1,8 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Glass Devtools, Inc. All rights reserved.
* Void Editor additions licensed under the AGPL 3.0 License.
*--------------------------------------------------------------------------------------------*/
import { Emitter, Event } from '../../../../base/common/event.js';
import { Disposable } from '../../../../base/common/lifecycle.js';
import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js';
@@ -4,13 +4,42 @@
*--------------------------------------------------------------------------------------------*/
import { spawn, execSync } from 'child_process';
// Added lines below
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const __void_name = 'void'
// hack to refresh styles automatically
function saveStylesFile() {
setTimeout(() => {
try {
// Find "void" in __dirname and use that as our base:
const voidIdx = __dirname.indexOf(__void_name);
const baseDir = __dirname.substring(0, voidIdx + __void_name.length);
const target = path.join(
baseDir,
'src/vs/workbench/contrib/void/browser/react/src2/styles.css'
);
// Or re-write with the same content:
const content = fs.readFileSync(target, 'utf8');
fs.writeFileSync(target, content, 'utf8');
console.log('[scope-tailwind] Force-saved styles.css');
} catch (err) {
console.error('[scope-tailwind] Error saving styles.css:', err);
}
}, 5000);
}
const args = process.argv.slice(2);
const isWatch = args.includes('--watch') || args.includes('-w');
if (isWatch) {
// Watch mode
// Create a watcher for scope-tailwind using nodemon
const scopeTailwindWatcher = spawn('npx', [
'nodemon',
'--watch', 'src',
@@ -19,15 +48,17 @@ if (isWatch) {
'npx scope-tailwind ./src -o src2/ -s void-scope -c styles.css -p "void-"'
]);
// Create a watcher for tsup in watch mode
const tsupWatcher = spawn('npx', [
'tsup',
'--watch'
]);
// Handle scope-tailwind watcher output
scopeTailwindWatcher.stdout.on('data', (data) => {
console.log(`[scope-tailwind] ${data}`);
// If the output mentions "styles.css", trigger the save:
if (data.toString().includes('styles.css')) {
saveStylesFile();
}
});
scopeTailwindWatcher.stderr.on('data', (data) => {
@@ -3,78 +3,76 @@
* Void Editor additions licensed under the AGPL 3.0 License.
*--------------------------------------------------------------------------------------------*/
import React, { ReactNode } from "react"
import { ReactNode } from "react"
import { VoidCodeEditor } from '../util/inputs.js';
const extensionMap: { [key: string]: string } = {
// Web
'html': 'html',
'htm': 'html',
'css': 'css',
'scss': 'scss',
'less': 'less',
'js': 'javascript',
'jsx': 'javascript',
'ts': 'typescript',
'tsx': 'typescript',
'json': 'json',
'jsonc': 'json',
// Programming Languages
'py': 'python',
'java': 'java',
'cpp': 'cpp',
'cc': 'cpp',
'h': 'cpp',
'hpp': 'cpp',
'cs': 'csharp',
'go': 'go',
'rs': 'rust',
'rb': 'ruby',
'php': 'php',
'sh': 'shell',
'bash': 'shell',
'zsh': 'shell',
// Markup/Config
'md': 'markdown',
'markdown': 'markdown',
'xml': 'xml',
'svg': 'xml',
'yaml': 'yaml',
'yml': 'yaml',
'ini': 'ini',
'toml': 'ini',
// Other
'sql': 'sql',
'graphql': 'graphql',
'gql': 'graphql',
'dockerfile': 'dockerfile',
'docker': 'dockerfile'
};
export function getLanguageFromFileName(fileName: string): string {
if (!fileName) return 'plaintext';
const ext = fileName.toLowerCase().split('.').pop();
if (!ext) return 'plaintext';
const extensionMap: { [key: string]: string } = {
// Web
'html': 'html',
'htm': 'html',
'css': 'css',
'scss': 'scss',
'less': 'less',
'js': 'javascript',
'jsx': 'javascript',
'ts': 'typescript',
'tsx': 'typescript',
'json': 'json',
'jsonc': 'json',
// Programming Languages
'py': 'python',
'java': 'java',
'cpp': 'cpp',
'cc': 'cpp',
'h': 'cpp',
'hpp': 'cpp',
'cs': 'csharp',
'go': 'go',
'rs': 'rust',
'rb': 'ruby',
'php': 'php',
'sh': 'shell',
'bash': 'shell',
'zsh': 'shell',
// Markup/Config
'md': 'markdown',
'markdown': 'markdown',
'xml': 'xml',
'svg': 'xml',
'yaml': 'yaml',
'yml': 'yaml',
'ini': 'ini',
'toml': 'ini',
// Other
'sql': 'sql',
'graphql': 'graphql',
'gql': 'graphql',
'dockerfile': 'dockerfile',
'docker': 'dockerfile'
};
return extensionMap[ext] || 'plaintext';
}
export const BlockCode = ({ text, buttonsOnHover, language }: { text: string, buttonsOnHover?: ReactNode, language?: string }) => {
return (<>
<div className={`relative group w-full bg-vscode-sidebar-bg overflow-hidden isolate`}>
const isSingleLine = !text.includes('\n')
return (<>
<div className={`relative group w-full overflow-hidden`}>
{buttonsOnHover === null ? null : (
<div className="z-[1] absolute top-0 right-0 opacity-0 group-hover:opacity-100 duration-200">
<div className="flex space-x-2 p-2">{buttonsOnHover}</div>
<div className={`flex space-x-2 ${isSingleLine ? '' : 'p-2'}`}>{buttonsOnHover}</div>
</div>
)}
@@ -82,18 +80,6 @@ export const BlockCode = ({ text, buttonsOnHover, language }: { text: string, bu
initValue={text}
language={language}
/>
{/* <div
className={`overflow-x-auto rounded-sm text-vscode-editor-fg bg-vscode-editor-bg`}
>
<SyntaxHighlighter
language={language ?? 'plaintext'} // TODO must auto detect language
style={customStyle}
className={"rounded-sm"}
>
{text}
</SyntaxHighlighter>
</div> */}
</div>
</>
)
@@ -17,7 +17,7 @@ enum CopyButtonState {
const COPY_FEEDBACK_TIMEOUT = 1000 // amount of time to say 'Copied!'
const CodeButtonsOnHover = ({ diffRepr: text }: { diffRepr: string }) => {
const CodeButtonsOnHover = ({ text }: { text: string }) => {
const accessor = useAccessor()
const [copyButtonState, setCopyButtonState] = useState(CopyButtonState.Copy)
@@ -38,21 +38,26 @@ const CodeButtonsOnHover = ({ diffRepr: text }: { diffRepr: string }) => {
.catch(() => { setCopyButtonState(CopyButtonState.Error) })
}, [text, clipboardService])
const onApply = useCallback(() => {
inlineDiffService.startApplying({
featureName: 'Ctrl+L',
userMessage: text,
})
}, [inlineDiffService])
const isSingleLine = !text.includes('\n')
return <>
<button
className="btn btn-secondary btn-sm border border-vscode-input-border rounded"
className={`${isSingleLine ? '' : 'px-1 py-0.5'} text-sm bg-void-bg-1 text-void-fg-1 hover:brightness-110 border border-vscode-input-border rounded`}
onClick={onCopy}
>
{copyButtonState}
</button>
<button
className="btn btn-secondary btn-sm border border-vscode-input-border rounded"
onClick={() => {
inlineDiffService.startApplying({
featureName: 'Ctrl+L',
userMessage: text,
})
}}
// btn btn-secondary btn-sm border text-sm border-vscode-input-border rounded
className={`${isSingleLine ? '' : 'px-1 py-0.5'} text-sm bg-void-bg-1 text-void-fg-1 hover:brightness-110 border border-vscode-input-border rounded`}
onClick={onApply}
>
Apply
</button>
@@ -72,8 +77,8 @@ const RenderToken = ({ token, nested = false }: { token: Token | string, nested?
if (t.type === "code") {
return <BlockCode
text={t.text}
language={t.lang}
buttonsOnHover={<CodeButtonsOnHover diffRepr={t.text} />}
// language={t.lang} // instead use vscode to detect language
buttonsOnHover={<CodeButtonsOnHover text={t.text} />}
/>
}
@@ -192,7 +197,7 @@ const RenderToken = ({ token, nested = false }: { token: Token | string, nested?
// inline code
if (t.type === "codespan") {
return (
<code className="text-vscode-editor-fg bg-vscode-editor-bg px-1 rounded-sm font-mono">
<code className="text-vscode-text-preformat-fg bg-vscode-text-preformat-bg px-1 rounded-sm font-mono">
{t.text}
</code>
)
@@ -210,7 +215,7 @@ const RenderToken = ({ token, nested = false }: { token: Token | string, nested?
// default
return (
<div className="bg-orange-50 rounded-sm overflow-hidden">
<span className="text-xs text-orange-500">Unknown type:</span>
<span className="text-sm text-orange-500">Unknown type:</span>
{t.raw}
</div>
)
@@ -6,16 +6,16 @@
import { useEffect, useState } from 'react'
import { useIsDark, useSidebarState } from '../util/services.js'
import ErrorBoundary from '../sidebar-tsx/ErrorBoundary.js'
import { CtrlKChat } from './CtrlKChat.js'
import { QuickEditChat } from './QuickEditChat.js'
import { QuickEditPropsType } from '../../../quickEditActions.js'
export const CtrlK = (props: QuickEditPropsType) => {
export const QuickEdit = (props: QuickEditPropsType) => {
const isDark = useIsDark()
return <div className={`@@void-scope ${isDark ? 'dark' : ''}`}>
<ErrorBoundary>
<CtrlKChat {...props} />
<QuickEditChat {...props} />
</ErrorBoundary>
</div>
@@ -11,14 +11,17 @@ import { getCmdKey } from '../../../helpers/getCmdKey.js';
import { VoidInputBox } from '../util/inputs.js';
import { QuickEditPropsType } from '../../../quickEditActions.js';
import { ButtonStop, ButtonSubmit } from '../sidebar-tsx/SidebarChat.js';
import { ModelDropdown } from '../void-settings-tsx/ModelDropdown.js';
import { X } from 'lucide-react';
export const CtrlKChat = ({ diffareaid, onGetInputBox, onUserUpdateText, onChangeHeight, initText }: QuickEditPropsType) => {
export const QuickEditChat = ({ diffareaid, onGetInputBox, onUserUpdateText, onChangeHeight, initText }: QuickEditPropsType) => {
const accessor = useAccessor()
const inlineDiffsService = accessor.get('IInlineDiffsService')
const sizerRef = useRef<HTMLDivElement | null>(null)
const inputBoxRef: React.MutableRefObject<InputBox | null> = useRef(null);
useEffect(() => {
const inputContainer = sizerRef.current
if (!inputContainer) return;
@@ -60,10 +63,15 @@ export const CtrlKChat = ({ diffareaid, onGetInputBox, onUserUpdateText, onChang
const onInterrupt = useCallback(() => {
if (currentlyStreamingIdRef.current !== undefined)
inlineDiffsService.interruptStreaming(currentlyStreamingIdRef.current)
inputBoxRef.current?.enable()
setIsStreaming(false)
}, [inlineDiffsService])
const onX = useCallback(() => {
inlineDiffsService.removeCtrlKZone({ diffareaid })
}, [inlineDiffsService, diffareaid])
// sync init value
const alreadySetRef = useRef(false)
useEffect(() => {
@@ -73,11 +81,11 @@ export const CtrlKChat = ({ diffareaid, onGetInputBox, onUserUpdateText, onChang
inputBoxRef.current.value = instructions
}, [initText, instructions])
return <div className='py-2 w-full max-w-xl' ref={sizerRef}>
return <div ref={sizerRef} className='py-2 w-full max-w-xl'>
<form
// copied from SidebarChat.tsx
className={`
flex flex-col gap-2 p-1 relative input text-left shrink-0
flex flex-col gap-2 py-1 px-2 relative input text-left shrink-0
transition-all duration-200
rounded-md
bg-vscode-input-bg
@@ -99,17 +107,22 @@ export const CtrlKChat = ({ diffareaid, onGetInputBox, onUserUpdateText, onChang
onSubmit(e)
}}
onClick={(e) => {
if (e.currentTarget === e.target) {
inputBoxRef.current?.focus()
}
inputBoxRef.current?.focus()
}}
>
<div // this div is used to position the input box properly
className={`w-full p-2 z-[999]`}
{/* // this div is used to position the input box properly */}
<div
className={`w-full z-[999] relative
@@[&_textarea]:!void-bg-transparent
@@[&_textarea]:!void-outline-none
@@[&_textarea]:!void-text-vscode-input-fg
@@[&_div.monaco-inputbox]:!void-border-none
@@[&_div.monaco-inputbox]:!void-outline-none`}
>
<div className='flex flex-row justify-between items-end gap-1'>
{/* left (input) */}
<div className='flex flex-row items-center justify-between items-end gap-1'>
{/* input */}
<div // copied from SidebarChat.tsx
className={`w-full
@@[&_textarea]:!void-bg-transparent @@[&_textarea]:!void-outline-none @@[&_textarea]:!void-text-vscode-input-fg @@[&_div.monaco-inputbox]:!void-outline-none`}>
@@ -119,6 +132,12 @@ export const CtrlKChat = ({ diffareaid, onGetInputBox, onUserUpdateText, onChang
onChangeText={onChangeText}
onCreateInstance={useCallback((instance: InputBox) => {
inputBoxRef.current = instance;
// if presses the esc key, X
instance.element.addEventListener('keydown', (e) => {
if (e.key === 'Escape')
onX()
})
onGetInputBox(instance);
instance.focus()
}, [onGetInputBox])}
@@ -126,22 +145,43 @@ export const CtrlKChat = ({ diffareaid, onGetInputBox, onUserUpdateText, onChang
/>
</div>
{/* right (button) */}
<div className='flex flex-row items-end'>
{/* submit / stop button */}
{isStreaming ?
// stop button
<ButtonStop
onClick={onInterrupt}
<div className='absolute pt-1 -top-1 -right-1'>
<span className='cursor-pointer rounded-md z-1'>
<X
className='size-4 text-vscode-toolbar-foreground'
onClick={onX}
/>
:
// submit button (up arrow)
<ButtonSubmit
disabled={isDisabled}
/>
}
</span>
</div>
</div>
{/* bottom row */}
<div
className='flex flex-row justify-between items-end gap-1'
>
{/* submit options */}
<div className='max-w-[150px]
@@[&_select]:!void-border-none
@@[&_select]:!void-outline-none'
>
<ModelDropdown featureName='Ctrl+K' />
</div>
{/* submit / stop button */}
{isStreaming ?
// stop button
<ButtonStop
onClick={onInterrupt}
/>
:
// submit button (up arrow)
<ButtonSubmit
disabled={isDisabled}
/>
}
</div>
</div>
@@ -4,9 +4,9 @@
*--------------------------------------------------------------------------------------------*/
import { mountFnGenerator } from '../util/mountFnGenerator.js'
import { CtrlK } from './CtrlK.js'
import { QuickEdit } from './QuickEdit.js'
export const mountCtrlK = mountFnGenerator(CtrlK)
export const mountCtrlK = mountFnGenerator(QuickEdit)
@@ -36,38 +36,38 @@ export const ErrorDisplay = ({
return (
<div className={`rounded-lg border border-red-200 bg-red-50 p-4 overflow-auto`}>
{/* Header */}
<div className="flex items-start justify-between">
<div className="flex gap-3">
<AlertCircle className="h-5 w-5 text-red-600 mt-0.5" />
<div className="flex-1">
<h3 className="font-semibold text-red-800">
<div className='flex items-start justify-between'>
<div className='flex gap-3'>
<AlertCircle className='h-5 w-5 text-red-600 mt-0.5' />
<div className='flex-1'>
<h3 className='font-semibold text-red-800'>
{/* eg Error */}
Error
</h3>
<p className="text-red-700 mt-1">
<p className='text-red-700 mt-1'>
{/* eg Something went wrong */}
{message}
</p>
</div>
</div>
<div className="flex gap-2">
<div className='flex gap-2'>
{details && (
<button className="text-red-600 hover:text-red-800 p-1 rounded"
<button className='text-red-600 hover:text-red-800 p-1 rounded'
onClick={() => setIsExpanded(!isExpanded)}
>
{isExpanded ? (
<ChevronUp className="h-5 w-5" />
<ChevronUp className='h-5 w-5' />
) : (
<ChevronDown className="h-5 w-5" />
<ChevronDown className='h-5 w-5' />
)}
</button>
)}
{showDismiss && onDismiss && (
<button className="text-red-600 hover:text-red-800 p-1 rounded"
<button className='text-red-600 hover:text-red-800 p-1 rounded'
onClick={onDismiss}
>
<X className="h-5 w-5" />
<X className='h-5 w-5' />
</button>
)}
</div>
@@ -75,10 +75,10 @@ export const ErrorDisplay = ({
{/* Expandable Details */}
{isExpanded && details && (
<div className="mt-4 space-y-3 border-t border-red-200 pt-3 overflow-auto">
<div className='mt-4 space-y-3 border-t border-red-200 pt-3 overflow-auto'>
<div>
<span className="font-semibold text-red-800">Full Error: </span>
<pre className="text-red-700">{details}</pre>
<span className='font-semibold text-red-800'>Full Error: </span>
<pre className='text-red-700'>{details}</pre>
</div>
</div>
)}
@@ -22,8 +22,16 @@ export const Sidebar = ({ className }: { className: string }) => {
const { isHistoryOpen, currentTab: tab } = sidebarState
const isDark = useIsDark()
return <div className={`@@void-scope ${isDark ? 'dark' : ''}`} style={{ width: '100%', height: '100%' }}>
<div className={`flex flex-col px-2 py-2 w-full h-full`}>
// ${isDark ? 'dark' : ''}
return <div className={`@@void-scope`} style={{ width: '100%', height: '100%' }}>
<div
// default background + text styles for sidebar
className={`
w-full h-full py-2
bg-void-bg-2
text-void-fg-1
`}
>
{/* <span onClick={() => {
const tabs = ['chat', 'settings', 'threadSelector']
@@ -31,7 +39,7 @@ export const Sidebar = ({ className }: { className: string }) => {
sidebarStateService.setState({ currentTab: tabs[(index + 1) % tabs.length] as any })
}}>clickme {tab}</span> */}
<div className={`mb-2 w-full ${isHistoryOpen ? '' : 'hidden'} ring-2 ring-widget-shadow z-10`}>
<div className={`w-full h-auto mb-2 ${isHistoryOpen ? '' : 'hidden'} ring-inset ring-2 ring-widget-shadow z-10`}>
<ErrorBoundary>
<SidebarThreadSelector />
</ErrorBoundary>
@@ -18,7 +18,7 @@ import { ErrorDisplay } from './ErrorDisplay.js';
import { OnError, ServiceSendLLMMessageParams } from '../../../../../../../platform/void/common/llmMessageTypes.js';
import { getCmdKey } from '../../../helpers/getCmdKey.js'
import { HistoryInputBox, InputBox } from '../../../../../../../base/browser/ui/inputbox/inputBox.js';
import { VoidCodeEditor, VoidInputBox } from '../util/inputs.js';
import { VoidInputBox } from '../util/inputs.js';
import { ModelDropdown } from '../void-settings-tsx/ModelDropdown.js';
import { chat_systemMessage, chat_prompt } from '../../../prompt/prompts.js';
import { ISidebarStateService } from '../../../sidebarStateService.js';
@@ -109,6 +109,65 @@ export const IconWarning = ({ size, className = '' }: { size: number, className?
);
};
export const IconLoading = ({ className = '' }: { className?: string }) => {
const [loadingText, setLoadingText] = useState('.');
useEffect(() => {
let intervalId;
// Function to handle the animation
const toggleLoadingText = () => {
if (loadingText === '...') {
setLoadingText('.');
} else {
setLoadingText(loadingText + '.');
}
};
// Start the animation loop
intervalId = setInterval(toggleLoadingText, 300);
// Cleanup function to clear the interval when component unmounts
return () => clearInterval(intervalId);
}, [loadingText, setLoadingText]);
return <div className={`${className}`}>{loadingText}</div>;
}
const useResizeObserver = () => {
const ref = useRef(null);
const [dimensions, setDimensions] = useState({ height: 0, width: 0 });
useEffect(() => {
if (ref.current) {
const resizeObserver = new ResizeObserver((entries) => {
if (entries.length > 0) {
const entry = entries[0];
setDimensions({
height: entry.contentRect.height,
width: entry.contentRect.width
});
}
});
resizeObserver.observe(ref.current);
return () => {
if (ref.current)
resizeObserver.unobserve(ref.current);
};
}
}, []);
return [ref, dimensions] as const;
};
type ButtonProps = ButtonHTMLAttributes<HTMLButtonElement>
const DEFAULT_BUTTON_SIZE = 20;
export const ButtonSubmit = ({ className, disabled, ...props }: ButtonProps & Required<Pick<ButtonProps, 'disabled'>>) => {
@@ -213,26 +272,26 @@ export const SelectedFiles = (
return (
!!selections && selections.length !== 0 && (
<div
className='flex flex-wrap gap-2 text-left'
className='flex flex-wrap gap-0.5 text-left'
>
{selections.map((selection, i) => {
const showSelectionText = !!(selection.selectionStr && selectionIsOpened[i])
const isThisSelectionOpened = !!(selection.selectionStr && selectionIsOpened[i])
return (
<div key={i} // container for `selectionSummary` and `selectionText`
className={`${showSelectionText ? 'w-full' : ''}`}
className={`${isThisSelectionOpened ? 'w-full' : ''}`}
>
{/* selection summary */}
<div
// className="relative rounded rounded-e-2xl flex items-center space-x-2 mx-1 mb-1 disabled:cursor-default"
className={`flex items-center gap-1 relative
rounded-md p-1
className={`flex items-center gap-0.5 relative
rounded-md px-1
w-fit h-fit
select-none
bg-vscode-editor-bg hover:brightness-95
bg-void-bg-3 hover:brightness-95
text-void-fg-1 text-xs text-nowrap
border border-vscode-commandcenter-border rounded-xs
text-xs text-vscode-editor-fg text-nowrap
`}
onClick={() => {
setSelectionIsOpened(s => {
@@ -252,12 +311,7 @@ export const SelectedFiles = (
{/* X button */}
{type === 'staging' &&
<span
className='
cursor-pointer
bg-vscode-editorwidget-bg hover:bg-vscode-toolbar-hover-bg
rounded-md
z-1
'
className='cursor-pointer hover:bg-vscode-toolbar-hover-bg rounded-md z-1'
onClick={(e) => {
e.stopPropagation();
if (type !== 'staging') return;
@@ -287,8 +341,8 @@ export const SelectedFiles = (
</div>
{/* selection text */}
{showSelectionText &&
<div className='w-full p-1 rounded-sm border-vscode-editor-border bg-vscode-sidebar-bg'>
{isThisSelectionOpened &&
<div className='w-full px-1 rounded-sm border-vscode-editor-border'>
<BlockCode text={selection.selectionStr!} language={getLanguageFromFileName(selection.fileURI.path)} />
</div>
}
@@ -301,8 +355,9 @@ export const SelectedFiles = (
}
const ChatBubble = ({ chatMessage }: {
chatMessage: ChatMessage
const ChatBubble = ({ chatMessage, isLoading }: {
chatMessage: ChatMessage,
isLoading?: boolean,
}) => {
const role = chatMessage.role
@@ -322,10 +377,16 @@ const ChatBubble = ({ chatMessage }: {
chatbubbleContents = <ChatMarkdownRender string={chatMessage.displayContent} /> // sectionsHTML
}
return <div className={`${role === 'user' ? 'text-right' : 'text-left'}`}>
<div className={`text-left inline-block p-2 rounded-lg space-y-2 ${role === 'user' ? 'bg-vscode-input-bg text-vscode-input-fg' : ''} max-w-full overflow-auto`}>
{chatbubbleContents}
</div>
return <div
// style + align chatbubble accoridng to role
className={`p-2 mx-2 text-left space-y-2 rounded-lg max-w-full
${role === 'user' ? 'self-end' : 'self-start'}
${role === 'user' ? 'bg-vscode-input-bg text-vscode-input-fg' : ''}
${role === 'assistant' ? 'w-full' : ''}
`}
>
{chatbubbleContents}
{isLoading && <IconLoading className='opacity-50 text-sm' />}
</div>
}
@@ -369,8 +430,12 @@ export const SidebarChat = () => {
// state of current message
const [instructions, setInstructions] = useState('') // the user's instructions
const isDisabled = !instructions.trim()
const [formHeight, setFormHeight] = useState(0) // TODO should use resize observer instead
const [sidebarHeight, setSidebarHeight] = useState(0)
const [sidebarRef, sidebarDimensions] = useResizeObserver()
const [formRef, formDimensions] = useResizeObserver()
// const [formHeight, setFormHeight] = useState(0) // TODO should use resize observer instead
// const [sidebarHeight, setSidebarHeight] = useState(0)
const onChangeText = useCallback((newStr: string) => { setInstructions(newStr) }, [setInstructions])
@@ -455,6 +520,8 @@ export const SidebarChat = () => {
threadsStateService.setStaging([]) // clear staging
inputBoxRef.current?.focus() // focus input after submit
}
const onAbort = () => {
@@ -481,18 +548,23 @@ export const SidebarChat = () => {
// const [_test_messages, _set_test_messages] = useState<string[]>([])
return <div
ref={(ref) => { if (ref) { setSidebarHeight(ref.clientHeight); } }}
ref={sidebarRef}
className={`w-full h-full`}
>
<ScrollToBottomContainer
className={`overflow-x-hidden overflow-y-auto`}
style={{ maxHeight: sidebarHeight - formHeight - 30 }}
className={`
w-full h-auto
flex flex-col gap-0
overflow-x-hidden
overflow-y-auto
`}
style={{ maxHeight: sidebarDimensions.height - formDimensions.height - 30 }}
>
{/* previous messages */}
{previousMessages.map((message, i) => <ChatBubble key={i} chatMessage={message} />)}
{/* message stream */}
<ChatBubble chatMessage={{ role: 'assistant', content: messageStream, displayContent: messageStream || null }} />
<ChatBubble chatMessage={{ role: 'assistant', content: messageStream, displayContent: messageStream || null }} isLoading={isLoading} />
{/* {_test_messages.map((_, i) => <div key={i}>div {i}</div>)}
<div>{`totalHeight: ${sidebarHeight - formHeight - 30}`}</div>
@@ -508,7 +580,7 @@ export const SidebarChat = () => {
className={`right-0 left-0 m-2 z-[999] overflow-hidden ${previousMessages.length > 0 ? 'absolute bottom-0' : ''}`}
>
<form
ref={(ref) => { if (ref) { setFormHeight(ref.clientHeight); } }}
ref={formRef}
className={`
flex flex-col gap-2 p-2 relative input text-left shrink-0
transition-all duration-200
@@ -526,9 +598,7 @@ export const SidebarChat = () => {
onSubmit(e)
}}
onClick={(e) => {
if (e.currentTarget === e.target) {
inputBoxRef.current?.focus()
}
inputBoxRef.current?.focus()
}}
>
{/* top row */}
@@ -561,13 +631,13 @@ export const SidebarChat = () => {
// .split(' ')
// .map(style => `@@[&_div.monaco-inputbox]:!void-${style}`)
// .join(' ');
`@@[&_textarea]:!void-bg-transparent
`
@@[&_textarea]:!void-outline-none
@@[&_textarea]:!void-text-vscode-input-fg
@@[&_textarea]:!void-min-h-[81px]
@@[&_textarea]:!void-max-h-[500px]
@@[&_div.monaco-inputbox]:!void-border-none
@@[&_div.monaco-inputbox]:!void-outline-none`
@@[&_div.monaco-inputbox]:!void-outline-none
`
}
>
@@ -53,7 +53,7 @@ export const SidebarThreadSelector = () => {
</div>
{/* a list of all the past threads */}
<div className='flex flex-col gap-y-1 overflow-y-auto'>
<div className='px-1'><div className='flex flex-col gap-y-1 overflow-y-auto'>
{sortedThreadIds.map((threadId) => {
if (!allThreads)
return <>Error: Threads not found.</>
@@ -88,7 +88,7 @@ export const SidebarThreadSelector = () => {
</button>
)
})}
</div>
</div></div>
</div>
)
@@ -1,3 +1,8 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Glass Devtools, Inc. All rights reserved.
* Void Editor additions licensed under the AGPL 3.0 License.
*--------------------------------------------------------------------------------------------*/
import { mountFnGenerator } from '../util/mountFnGenerator.js'
import { Sidebar } from './Sidebar.js'
@@ -20,6 +20,16 @@
.inherit-bg-all-restyle > * {
background-color: inherit !important;
}
.bg-editor-style-override {
--vscode-sideBar-background: var(--vscode-editor-background);
}
/* html {
font-size: var(--vscode-font-size);
@@ -60,12 +60,17 @@ export const VoidInputBox = ({ onChangeText, onCreateInstance, inputBoxRef, plac
const contextViewProvider = accessor.get('IContextViewService')
return <WidgetComponent
ctor={InputBox}
className='
bg-void-bg-1
@@[&_::placeholder]:!void-text-void-fg-3
'
propsFn={useCallback((container) => [
container,
contextViewProvider,
{
inputBoxStyles: {
...defaultInputBoxStyles,
inputForeground: "var(--vscode-foreground)",
// inputBackground: 'transparent',
// inputBorder: 'none',
...styles,
@@ -74,7 +79,7 @@ export const VoidInputBox = ({ onChangeText, onCreateInstance, inputBoxRef, plac
tooltip: '',
flexibleHeight: multiline,
flexibleMaxHeight: 500,
flexibleWidth: true,
flexibleWidth: false,
}
] as const, [contextViewProvider, placeholder, multiline])}
dispose={useCallback((instance: InputBox) => {
@@ -192,11 +197,12 @@ export const VoidCheckBox = ({ label, value, onClick, className }: { label: stri
}
export const VoidSelectBox = <T,>({ onChangeSelection, onCreateInstance, selectBoxRef, options }: {
export const VoidSelectBox = <T,>({ onChangeSelection, onCreateInstance, selectBoxRef, options, className }: {
onChangeSelection: (value: T) => void;
onCreateInstance?: ((instance: SelectBox) => void | IDisposable[]);
selectBoxRef?: React.MutableRefObject<SelectBox | null>;
options: readonly { text: string, value: T }[];
className?:string;
}) => {
const accessor = useAccessor()
const contextViewProvider = accessor.get('IContextViewService')
@@ -204,7 +210,9 @@ export const VoidSelectBox = <T,>({ onChangeSelection, onCreateInstance, selectB
let containerRef = useRef<HTMLDivElement | null>(null);
return <WidgetComponent
className='@@select-child-restyle'
className={`@@select-child-restyle
@@[&_select]:!void-text-void-fg-3
${className ?? ''}`}
ctor={SelectBox}
propsFn={useCallback((container) => {
containerRef.current = container
@@ -294,14 +302,15 @@ export const VoidCodeEditor = ({ initValue, language }: { initValue: string, lan
const accessor = useAccessor()
const instantiationService = accessor.get('IInstantiationService')
const modelService = accessor.get('IModelService')
const languageDetectionService = accessor.get('ILanguageDetectionService')
initValue = normalizeIndentation(initValue)
return <div ref={divRef}>
<WidgetComponent
className='relative z-0 text-sm'
ctor={useCallback((container) =>
instantiationService.createInstance(
className='relative z-0 @@bg-editor-style-override' // text-sm
ctor={useCallback((container) => {
return instantiationService.createInstance(
CodeEditorWidget,
container,
{
@@ -345,37 +354,40 @@ export const VoidCodeEditor = ({ initValue, language }: { initValue: string, lan
{
isSimpleWidget: true,
})
, [instantiationService])
}, [instantiationService])
}
onCreateInstance={useCallback((editor: CodeEditorWidget) => {
const model = modelService.createModel(initValue, null)
const model = modelService.createModel(
initValue,
language ? {
languageId: language,
onDidChange: () => ({
dispose: () => { }
})
} : null
);
editor.setModel(model);
model.setLanguage(language ?? 'plaintext')
const container = editor.getDomNode()
const parentNode = container?.parentElement
if (parentNode) {
const height = Math.min(editor.getScrollHeight() + 1, MAX_HEIGHT);
parentNode.style.height = `${height}px`;
editor.layout();
}
// Listen for content changes and update height
const disposable = editor.onDidContentSizeChange(() => {
const resize = () => {
if (parentNode) {
const height = Math.min(editor.getScrollHeight() + 1, MAX_HEIGHT);
parentNode.style.height = `${height}px`;
editor.layout();
}
});
}
resize()
const disposable = editor.onDidContentSizeChange(() => { resize() });
return [disposable]
}, [modelService, initValue, language])}
dispose={useCallback((editor: CodeEditorWidget) => {
editor.dispose();
}, [])}
}, [modelService, languageDetectionService])}
propsFn={useCallback(() => { return [] }, [])}
/>
@@ -39,6 +39,7 @@ import { INotificationService } from '../../../../../../../platform/notification
import { IAccessibilityService } from '../../../../../../../platform/accessibility/common/accessibility.js'
import { ILanguageConfigurationService } from '../../../../../../../editor/common/languages/languageConfigurationRegistry.js'
import { ILanguageFeaturesService } from '../../../../../../../editor/common/services/languageFeatures.js'
import { ILanguageDetectionService } from '../../../../../../services/languageDetection/common/languageDetectionWorkerService.js'
@@ -145,6 +146,7 @@ export const _registerServices = (accessor: ServicesAccessor) => {
}
const getReactAccessor = (accessor: ServicesAccessor) => {
const reactAccessor = {
IModelService: accessor.get(IModelService),
@@ -169,6 +171,7 @@ const getReactAccessor = (accessor: ServicesAccessor) => {
INotificationService: accessor.get(INotificationService),
IAccessibilityService: accessor.get(IAccessibilityService),
ILanguageConfigurationService: accessor.get(ILanguageConfigurationService),
ILanguageDetectionService: accessor.get(ILanguageDetectionService),
ILanguageFeaturesService: accessor.get(ILanguageFeaturesService),
} as const
@@ -3,24 +3,37 @@
* Void Editor additions licensed under the AGPL 3.0 License.
*--------------------------------------------------------------------------------------------*/
import { useCallback, useEffect, useRef, useState } from 'react'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { FeatureName, featureNames, ModelSelection, modelSelectionsEqual, ProviderName, providerNames } from '../../../../../../../platform/void/common/voidSettingsTypes.js'
import { useSettingsState, useRefreshModelState, useAccessor } from '../util/services.js'
import { VoidSelectBox } from '../util/inputs.js'
import { SelectBox } from '../../../../../../../base/browser/ui/selectBox/selectBox.js'
import { IconWarning } from '../sidebar-tsx/SidebarChat.js'
import { VOID_OPEN_SETTINGS_ACTION_ID } from '../../../voidSettingsPane.js'
import { ModelOption } from '../../../../../../../platform/void/common/voidSettingsService.js'
const ModelSelectBox = ({ featureName }: { featureName: FeatureName }) => {
const optionsEqual = (m1: ModelOption[], m2: ModelOption[]) => {
if (m1.length !== m2.length) return false
for (let i = 0; i < m1.length; i++) {
if (!modelSelectionsEqual(m1[i].value, m2[i].value)) return false
}
return true
}
const ModelSelectBox = ({ options, featureName }: { options: ModelOption[], featureName: FeatureName }) => {
const accessor = useAccessor()
const voidSettingsService = accessor.get('IVoidSettingsService')
const settingsState = useSettingsState()
let weChangedText = false
return <VoidSelectBox
options={settingsState._modelOptions}
className='@@[&_select]:!void-text-xs'
options={options}
onChangeSelection={useCallback((newVal: ModelSelection) => {
if (weChangedText) return
voidSettingsService.setModelSelectionOfFeature(featureName, newVal)
@@ -42,6 +55,23 @@ const ModelSelectBox = ({ featureName }: { featureName: FeatureName }) => {
/>
}
const MemoizedModelSelectBox = ({ featureName }: { featureName: FeatureName }) => {
const settingsState = useSettingsState()
const oldOptionsRef = useRef<ModelOption[]>([])
const [memoizedOptions, setMemoizedOptions] = useState(oldOptionsRef.current)
useEffect(() => {
const oldOptions = oldOptionsRef.current
const newOptions = settingsState._modelOptions
if (!optionsEqual(oldOptions, newOptions)) {
setMemoizedOptions(newOptions)
}
oldOptionsRef.current = newOptions
}, [settingsState._modelOptions])
return <ModelSelectBox featureName={featureName} options={memoizedOptions} />
}
const DummySelectBox = () => {
const accessor = useAccessor()
@@ -55,17 +85,20 @@ const DummySelectBox = () => {
className={`
flex items-center
flex-nowrap text-ellipsis
text-vscode-charts-yellow
hover:brightness-90 transition-all duration-200
text-void-warning brightness-90 opacity-90
hover:brightness-75 transition-all duration-200
cursor-pointer
text-xs
`}
onClick={openSettings}
>
<IconWarning
size={20}
size={14}
className='mr-1 brightness-90'
/>
<span>Model required</span>
<span>Provider required</span>
</div>
// return <VoidSelectBox
// options={[{ text: 'Please add a model!', value: null }]}
@@ -76,6 +109,6 @@ const DummySelectBox = () => {
export const ModelDropdown = ({ featureName }: { featureName: FeatureName }) => {
const settingsState = useSettingsState()
return <>
{settingsState._modelOptions.length === 0 ? <DummySelectBox /> : <ModelSelectBox featureName={featureName} />}
{settingsState._modelOptions.length === 0 ? <DummySelectBox /> : <MemoizedModelSelectBox featureName={featureName} />}
</>
}
@@ -1,42 +1,47 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Glass Devtools, Inc. All rights reserved.
* Void Editor additions licensed under the AGPL 3.0 License.
*--------------------------------------------------------------------------------------------*/
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { InputBox } from '../../../../../../../base/browser/ui/inputbox/inputBox.js'
import { ProviderName, SettingName, displayInfoOfSettingName, providerNames, VoidModelInfo, featureFlagNames, displayInfoOfFeatureFlag, customSettingNamesOfProvider, RefreshableProviderName, refreshableProviderNames, displayInfoOfProviderName, defaultProviderSettings } from '../../../../../../../platform/void/common/voidSettingsTypes.js'
import { ProviderName, SettingName, displayInfoOfSettingName, providerNames, VoidModelInfo, featureFlagNames, displayInfoOfFeatureFlag, customSettingNamesOfProvider, RefreshableProviderName, refreshableProviderNames, displayInfoOfProviderName, defaultProviderSettings, nonlocalProviderNames, localProviderNames } from '../../../../../../../platform/void/common/voidSettingsTypes.js'
import ErrorBoundary from '../sidebar-tsx/ErrorBoundary.js'
import { VoidCheckBox, VoidInputBox, VoidSelectBox, VoidSwitch } from '../util/inputs.js'
import { useAccessor, useIsDark, useRefreshModelListener, useRefreshModelState, useSettingsState } from '../util/services.js'
import { X, RefreshCw, Loader2, Check } from 'lucide-react'
import { X, RefreshCw, Loader2, Check, MoveRight } from 'lucide-react'
import { ChatMarkdownRender } from '../markdown/ChatMarkdownRender.js'
const SubtleButton = ({ onClick, text, icon, disabled }: { onClick: () => void, text: string, icon: React.ReactNode, disabled: boolean }) => {
return <div className='flex items-center py-1 px-3 rounded-sm overflow-hidden gap-2 hover:bg-black/10 dark:hover:bg-gray-200/10'>
return <div className='flex items-center text-void-fg-3 mb-1 px-3 rounded-sm overflow-hidden gap-2 hover:bg-black/10 dark:hover:bg-gray-300/10'>
<button className='flex items-center' disabled={disabled} onClick={onClick}>
{icon}
</button>
<span className='opacity-50'>
<span>
{text}
</span>
</div>
}
// models
const RefreshModelButton = ({ providerName }: { providerName: RefreshableProviderName }) => {
const refreshModelState = useRefreshModelState()
const accessor = useAccessor()
const refreshModelService = accessor.get('IRefreshModelService')
const [justFinished, setJustSucceeded] = useState(false)
const [justFinished, setJustFinished] = useState(false)
useRefreshModelListener(
useCallback((providerName2, refreshModelState) => {
if (providerName2 !== providerName) return
const { state } = refreshModelState[providerName]
if (state !== 'success') return
// now we know we just entered 'success' state for this providerName
setJustSucceeded(true)
const tid = setTimeout(() => { setJustSucceeded(false) }, 2000)
if (state !== 'finished') return
// now we know we just entered 'finished' state for this providerName
setJustFinished(true)
const tid = setTimeout(() => { setJustFinished(false) }, 2000)
return () => clearTimeout(tid)
}, [providerName])
)
@@ -59,7 +64,9 @@ const RefreshableModels = () => {
const buttons = refreshableProviderNames.map(providerName => {
if (!settingsState.settingsOfProvider[providerName]._enabled) return null
return <RefreshModelButton key={providerName} providerName={providerName} />
return <div key={providerName} className='pb-4' >
<RefreshModelButton providerName={providerName} />
</div>
})
return <>
@@ -87,17 +94,9 @@ const AddModelMenu = ({ onSubmit }: { onSubmit: () => void }) => {
return <>
<div className='flex items-center gap-4'>
{/* model */}
<div className='max-w-40 w-full'>
<VoidInputBox
placeholder='Model Name'
onChangeText={useCallback((modelName) => { modelNameRef.current = modelName }, [])}
multiline={false}
/>
</div>
{/* provider */}
<div className='max-w-40 w-full'>
<div className='max-w-40 w-full border border-vscode-editorwidget-border'>
<VoidSelectBox
onCreateInstance={useCallback(() => { providerNameRef.current = providerOptions[0].value }, [providerOptions])} // initialize state
onChangeSelection={useCallback((providerName: ProviderName) => { providerNameRef.current = providerName }, [])}
@@ -105,6 +104,15 @@ const AddModelMenu = ({ onSubmit }: { onSubmit: () => void }) => {
/>
</div>
{/* model */}
<div className='max-w-40 w-full border border-vscode-editorwidget-border'>
<VoidInputBox
placeholder='Model Name'
onChangeText={useCallback((modelName) => { modelNameRef.current = modelName }, [])}
multiline={false}
/>
</div>
{/* button */}
<div className='max-w-40'>
<button
@@ -147,7 +155,7 @@ const AddModelMenu = ({ onSubmit }: { onSubmit: () => void }) => {
const AddModelMenuFull = () => {
const [open, setOpen] = useState(false)
return <div className='hover:bg-black/10 dark:hover:bg-gray-200/10 py-1 px-3 rounded-sm overflow-hidden '>
return <div className='hover:bg-black/10 dark:hover:bg-gray-300/10 py-1 my-4 pb-1 px-3 rounded-sm overflow-hidden '>
{open ?
<AddModelMenu onSubmit={() => { setOpen(false) }} />
: <button
@@ -180,19 +188,26 @@ export const ModelDump = () => {
})
return <div className=''>
{modelDump.map(m => {
const { isHidden, isDefault, modelName, providerName, providerEnabled } = m
{modelDump.map((m, i) => {
const { isHidden, isDefault, isAutodetected, modelName, providerName, providerEnabled } = m
const isNewProviderName = (i > 0 ? modelDump[i - 1] : undefined)?.providerName !== providerName
const disabled = !providerEnabled
return <div key={`${modelName}${providerName}`} className='flex items-center justify-between gap-4 hover:bg-black/10 dark:hover:bg-gray-200/10 py-1 px-3 rounded-sm overflow-hidden cursor-default'>
return <div key={`${modelName}${providerName}`}
className={`flex items-center justify-between gap-4 hover:bg-black/10 dark:hover:bg-gray-300/10 py-1 px-3 rounded-sm overflow-hidden cursor-default truncate
`}
>
{/* left part is width:full */}
<div className={`w-full flex items-center gap-4`}>
<span>{`${modelName} (${providerName})`}</span>
<span className='min-w-20'>{isNewProviderName ? displayInfoOfProviderName(providerName).title : ''}</span>
<span>{modelName}</span>
{/* <span>{`${modelName} (${providerName})`}</span> */}
</div>
{/* right part is anything that fits */}
<div className='w-fit flex items-center gap-4'>
<span className='opacity-50 whitespace-nowrap'>{isDefault ? '' : '(custom model)'}</span>
<span className='opacity-50'>{isAutodetected ? '(detected locally)' : isDefault ? '' : '(custom model)'}</span>
<VoidSwitch
value={disabled ? false : !isHidden}
@@ -217,7 +232,7 @@ export const ModelDump = () => {
const ProviderSetting = ({ providerName, settingName }: { providerName: ProviderName, settingName: SettingName }) => {
const { title: providerTitle, } = displayInfoOfProviderName(providerName)
// const { title: providerTitle, } = displayInfoOfProviderName(providerName)
const { title: settingTitle, placeholder, subTextMd } = displayInfoOfSettingName(providerName, settingName)
@@ -229,7 +244,8 @@ const ProviderSetting = ({ providerName, settingName }: { providerName: Provider
return <ErrorBoundary>
<div className='my-1'>
<VoidInputBox
placeholder={`${providerTitle} ${settingTitle} (${placeholder}).`}
// placeholder={`${providerTitle} ${settingTitle} (${placeholder})`}
placeholder={`${settingTitle} (${placeholder})`}
onChangeText={useCallback((newVal) => {
if (weChangedTextRef) return
voidSettingsService.setSettingOfProvider(providerName, settingName, newVal)
@@ -268,7 +284,7 @@ const ProviderSetting = ({ providerName, settingName }: { providerName: Provider
}, [voidSettingsService, providerName, settingName])}
multiline={false}
/>
{subTextMd === undefined ? null : <div className='py-1 px-3 opacity-50 text-xs'>
{subTextMd === undefined ? null : <div className='py-1 px-3 opacity-50 text-sm'>
<ChatMarkdownRender string={subTextMd} />
</div>}
@@ -313,7 +329,7 @@ const SettingsForProvider = ({ providerName }: { providerName: ProviderName }) =
}
export const VoidProviderSettings = () => {
export const VoidProviderSettings = ({ providerNames }: { providerNames: ProviderName[] }) => {
return <>
{providerNames.map(providerName =>
<SettingsForProvider key={providerName} providerName={providerName} />
@@ -344,9 +360,9 @@ export const VoidProviderSettings = () => {
// })}
// </>
// }
type TabName = 'models' | 'general'
export const VoidFeatureFlagSettings = () => {
const accessor = useAccessor()
const voidSettingsService = accessor.get('IVoidSettingsService')
@@ -354,10 +370,11 @@ export const VoidFeatureFlagSettings = () => {
return featureFlagNames.map((flagName) => {
// right now this is just `enabled_autoRefreshModels`
const enabled = voidSettingsState.featureFlagSettings[flagName]
const { description } = displayInfoOfFeatureFlag(flagName)
return <SubtleButton
return <SubtleButton key={flagName}
onClick={() => { voidSettingsService.setFeatureFlag(flagName, !enabled) }}
text={description}
icon={enabled ? <Check className='stroke-green-500 size-3' /> : <X className='stroke-red-500 size-3' />}
@@ -367,12 +384,85 @@ export const VoidFeatureFlagSettings = () => {
}
export const FeaturesTab = () => {
return <>
<h2 className={`text-3xl mb-2`}>Local Providers</h2>
{/* <h3 className={`opacity-50 mb-2`}>{`Keep your data private by hosting AI locally on your computer.`}</h3> */}
{/* <h3 className={`opacity-50 mb-2`}>{`Instructions:`}</h3> */}
{/* <h3 className={`mb-2`}>{`Void can access any model that you host locally. We automatically detect your local models by default.`}</h3> */}
<h3 className={`text-void-fg-3 mb-2`}>{`Void can access any model that you host locally. We automatically detect your local models by default.`}</h3>
<div className='pl-4 select-text opacity-50'>
<span className={`text-sm mb-2`}><ChatMarkdownRender string={`1. Download [Ollama](https://ollama.com/download).`} /></span>
<span className={`text-sm mb-2`}><ChatMarkdownRender string={`2. Open your terminal.`} /></span>
<span className={`text-sm mb-2`}><ChatMarkdownRender string={`3. Run \`ollama run llama3.1\`. This installs Meta's llama3.1 model which is best for chat and inline edits. Requires 5GB of memory.`} /></span>
<span className={`text-sm mb-2`}><ChatMarkdownRender string={`4. Run \`ollama run qwen2.5-coder:1.5b\`. This installs a faster autocomplete model. Requires 1GB of memory.`} /></span>
<span className={`text-sm mb-2`}><ChatMarkdownRender string={`Void automatically detects locally running models and enables them.`} /></span>
{/* TODO we should create UI for downloading models without user going into terminal */}
</div>
<ErrorBoundary>
<VoidProviderSettings providerNames={localProviderNames} />
</ErrorBoundary>
<h2 className={`text-3xl mb-2 mt-16`}>Providers</h2>
<h3 className={`text-void-fg-3 mb-2`}>{`Void can access models from Anthropic, OpenAI, OpenRouter, and more.`}</h3>
{/* <h3 className={`opacity-50 mb-2`}>{`Access models like ChatGPT and Claude. We recommend using Anthropic or OpenAI as providers, or Groq as a faster alternative.`}</h3> */}
<ErrorBoundary>
<VoidProviderSettings providerNames={nonlocalProviderNames} />
</ErrorBoundary>
<h2 className={`text-3xl mb-2 mt-16`}>Models</h2>
<ErrorBoundary>
<VoidFeatureFlagSettings />
<RefreshableModels />
<ModelDump />
<AddModelMenuFull />
</ErrorBoundary>
</>
}
const OneClickSwitch = () => {
}
const GeneralTab = () => {
return <>
{/* <VoidFeatureFlagSettings /> */}
{/* keyboard shortcuts */}
<h2 className={`text-3xl mb-2`}>General Settings</h2>
<h3 className={`text-void-fg-3 mb-2`}>{`VS Code's built-in settings.`}</h3>
<h2 className={`text-3xl mb-2`}>Keyboard Settings</h2>
<h3 className={`text-void-fg-3 mb-2`}>{`Void can access models from Anthropic, OpenAI, OpenRouter, and more.`}</h3>
<h2 className={`text-3xl mb-2`}>One-click Switch</h2>
Transfer your VS Code settings to Void.
<h2 className={`text-3xl mb-2`}>Theme</h2>
<h2 className={`text-3xl mb-2`}>Rules for AI</h2>
</>
}
// full settings
export const Settings = () => {
const isDark = useIsDark()
const [tab, setTab] = useState<'models' | 'features'>('models')
const [tab, setTab] = useState<TabName>('models')
return <div className={`@@void-scope ${isDark ? 'dark' : ''}`}>
<div className='w-full h-full px-10 py-10 select-none'>
@@ -391,9 +481,9 @@ export const Settings = () => {
<button className={`text-left p-1 px-3 my-0.5 rounded-sm overflow-hidden ${tab === 'models' ? 'bg-black/10 dark:bg-gray-200/10' : ''} hover:bg-black/10 hover:dark:bg-gray-200/10 active:bg-black/10 active:dark:bg-gray-200/10 `}
onClick={() => { setTab('models') }}
>Models</button>
<button className={`text-left p-1 px-3 my-0.5 rounded-sm overflow-hidden ${tab === 'features' ? 'bg-black/10 dark:bg-gray-200/10' : ''} hover:bg-black/10 hover:dark:bg-gray-200/10 active:bg-black/10 active:dark:bg-gray-200/10 `}
onClick={() => { setTab('features') }}
>Features</button>
<button className={`text-left p-1 px-3 my-0.5 rounded-sm overflow-hidden ${tab === 'general' ? 'bg-black/10 dark:bg-gray-200/10' : ''} hover:bg-black/10 hover:dark:bg-gray-200/10 active:bg-black/10 active:dark:bg-gray-200/10 `}
onClick={() => { setTab('general') }}
>General</button>
</div>
{/* separator */}
@@ -404,23 +494,11 @@ export const Settings = () => {
<div className='w-full overflow-y-auto'>
<div className={`${tab !== 'models' ? 'hidden' : ''}`}>
<h2 className={`text-3xl mb-2`}>Providers</h2>
<ErrorBoundary>
<VoidFeatureFlagSettings />
<VoidProviderSettings />
</ErrorBoundary>
<h2 className={`text-3xl mb-2 mt-4`}>Models</h2>
<ErrorBoundary>
<RefreshableModels />
<ModelDump />
<AddModelMenuFull />
</ErrorBoundary>
<FeaturesTab />
</div>
<div className={`${tab !== 'features' ? 'hidden' : ''}`}>
<h2 className={`text-3xl mb-2`} onClick={() => { setTab('features') }}>Features</h2>
{/* <VoidFeatureFlagSettings /> */}
<div className={`${tab !== 'general' ? 'hidden' : ''}`}>
<GeneralTab />
</div>
</div>
@@ -1,3 +1,8 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Glass Devtools, Inc. All rights reserved.
* Void Editor additions licensed under the AGPL 3.0 License.
*--------------------------------------------------------------------------------------------*/
import { mountFnGenerator } from '../util/mountFnGenerator.js'
import { Settings } from './Settings.js'
@@ -9,7 +9,34 @@ module.exports = {
content: ['./src2/**/*.{jsx,tsx}'], // uses these files to decide how to transform the css file
theme: {
extend: {
fontSize: {
xs: '10px',
sm: '12px',
root: '13px',
lg: '14px',
xl: '16px',
'2xl': '18px',
'3xl': '20px',
'4xl': '24px',
'5xl': '30px',
'6xl': '36px',
'7xl': '48px',
'8xl': '64px',
'9xl': '72px',
},
// common colors to use, ordered light to dark
colors: {
"void-bg-1": "var(--vscode-input-background)",
"void-bg-2": "var(--vscode-sideBar-background)",
"void-bg-3": "var(--vscode-editor-background)",
"void-fg-1": "var(--vscode-editor-foreground)",
"void-fg-2": "var(--vscode-input-foreground)",
"void-fg-3": "var(--vscode-input-placeholderForeground)",
"void-warning": "var(--vscode-charts-orange)",
vscode: {
// see: https://code.visualstudio.com/api/extension-guides/webview#theming-webview-content
@@ -39,8 +66,8 @@ module.exports = {
"input-bg": "var(--vscode-input-background)",
"input-border": "var(--vscode-input-border)",
"input-fg": "var(--vscode-input-foreground)",
"input-placeholder-fg": "var(--vscode-placeholderForeground)",
"input-active-bg": "var(--vscode-activeBackground)",
"input-placeholder-fg": "var(--vscode-input-placeholderForeground)",
"input-active-bg": "var(--vscode-input-activeBackground)",
"input-option-active-border": "var(--vscode-inputOption-activeBorder)",
"input-option-active-fg": "var(--vscode-inputOption-activeForeground)",
"input-option-hover-bg": "var(--vscode-inputOption-hoverBackground)",
@@ -89,7 +116,7 @@ module.exports = {
"sidebar-bg": "var(--vscode-sideBar-background)",
"sidebar-fg": "var(--vscode-sideBar-foreground)",
"sidebar-border": "var(--vscode-sideBar-border)",
"sidebar-drop-backdrop": "var(--vscode-sideBar-dropBackground)",
"sidebar-drop-bg": "var(--vscode-sideBar-dropBackground)",
"sidebar-title-fg": "var(--vscode-sideBarTitle-foreground)",
"sidebar-header-bg": "var(--vscode-sideBarSectionHeader-background)",
"sidebar-header-fg": "var(--vscode-sideBarSectionHeader-foreground)",
@@ -103,6 +130,10 @@ module.exports = {
// other colors (these are partially complete)
// text formatting
"text-preformat-bg": "var(--vscode-textPreformat-background)",
"text-preformat-fg": "var(--vscode-textPreformat-foreground)",
// editor colors
"editor-bg": "var(--vscode-editor-background)",
"editor-fg": "var(--vscode-editor-foreground)",
@@ -9,7 +9,7 @@ export default defineConfig({
entry: [
'./src2/sidebar-tsx/index.tsx',
'./src2/void-settings-tsx/index.tsx',
'./src2/ctrl-k-tsx/index.tsx',
'./src2/quick-edit-tsx/index.tsx',
'./src2/diff/index.tsx',
],
outDir: './out',
@@ -26,9 +26,18 @@ import { VOID_OPEN_SETTINGS_ACTION_ID } from './voidSettingsPane.js';
// ---------- Register commands and keybindings ----------
const roundRangeToLines = (range: IRange | null | undefined) => {
export const roundRangeToLines = (range: IRange | null | undefined, options: { emptySelectionBehavior: 'null' | 'line' }) => {
if (!range)
return null
// treat as no selection if selection is empty
if (range.endColumn === range.startColumn && range.endLineNumber === range.startLineNumber) {
if (options.emptySelectionBehavior === 'null')
return null
else if (options.emptySelectionBehavior === 'line')
return { startLineNumber: range.startLineNumber, startColumn: 1, endLineNumber: range.startLineNumber, endColumn: 1 }
}
// IRange is 1-indexed
const endLine = range.endColumn === 1 ? range.endLineNumber - 1 : range.endLineNumber // e.g. if the user triple clicks, it selects column=0, line=line -> column=0, line=line+1
const newRange: IRange = {
@@ -72,51 +81,49 @@ registerAction2(class extends Action2 {
stateService.fireFocusChat()
const editor = editorService.getActiveCodeEditor()
const selectionRange = roundRangeToLines(
// accessor.get(IEditorService).activeTextEditorControl?.getSelection()
editor?.getSelection()
// accessor.get(IEditorService).activeTextEditorControl?.getSelection()
const selectionRange = roundRangeToLines(editor?.getSelection(), { emptySelectionBehavior: 'null' })
// select whole lines
if (selectionRange) {
editor?.setSelection({ startLineNumber: selectionRange.startLineNumber, endLineNumber: selectionRange.endLineNumber, startColumn: 1, endColumn: Number.MAX_SAFE_INTEGER })
}
const selectionStr = getContentInRange(model, selectionRange)
const selection: CodeStagingSelection = !selectionRange || !selectionStr || (selectionRange.startLineNumber > selectionRange.endLineNumber) ? {
type: 'File',
fileURI: model.uri,
selectionStr: null,
range: null,
} : {
type: 'Selection',
fileURI: model.uri,
selectionStr: selectionStr,
range: selectionRange,
}
// add selection to staging
const threadHistoryService = accessor.get(IThreadHistoryService)
const currentStaging = threadHistoryService.state._currentStagingSelections
const currentStagingEltIdx = currentStaging?.findIndex(s =>
s.fileURI.fsPath === model.uri.fsPath
&& s.range?.startLineNumber === selection.range?.startLineNumber
&& s.range?.endLineNumber === selection.range?.endLineNumber
)
if (selectionRange) {
// select whole lines
editor?.setSelection({ startLineNumber: selectionRange.startLineNumber, endLineNumber: selectionRange.endLineNumber, startColumn: 1, endColumn: Number.MAX_SAFE_INTEGER })
const selectionStr = getContentInRange(model, selectionRange)
const selection: CodeStagingSelection = selectionStr === null || selectionRange.startLineNumber > selectionRange.endLineNumber ? {
type: 'File',
fileURI: model.uri,
selectionStr: null,
range: null,
} : {
type: 'Selection',
fileURI: model.uri,
selectionStr: selectionStr,
range: selectionRange,
}
// add selection to staging
const threadHistoryService = accessor.get(IThreadHistoryService)
const currentStaging = threadHistoryService.state._currentStagingSelections
const currentStagingEltIdx = currentStaging?.findIndex(s =>
s.fileURI.fsPath === model.uri.fsPath
&& s.range?.startLineNumber === selection.range?.startLineNumber
&& s.range?.endLineNumber === selection.range?.endLineNumber
)
// if matches with existing selection, overwrite
if (currentStagingEltIdx !== undefined && currentStagingEltIdx !== -1) {
threadHistoryService.setStaging([
...currentStaging!.slice(0, currentStagingEltIdx),
selection,
...currentStaging!.slice(currentStagingEltIdx + 1, Infinity)
])
}
// if no match, add
else {
threadHistoryService.setStaging([...(currentStaging ?? []), selection])
}
// if matches with existing selection, overwrite
if (currentStagingEltIdx !== undefined && currentStagingEltIdx !== -1) {
threadHistoryService.setStaging([
...currentStaging!.slice(0, currentStagingEltIdx),
selection,
...currentStaging!.slice(currentStagingEltIdx + 1, Infinity)
])
}
// if no match, add
else {
threadHistoryService.setStaging([...(currentStaging ?? []), selection])
}
}
@@ -1,3 +1,8 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Glass Devtools, Inc. All rights reserved.
* Void Editor additions licensed under the AGPL 3.0 License.
*--------------------------------------------------------------------------------------------*/
import { Emitter, Event } from '../../../../base/common/event.js';
import { Disposable } from '../../../../base/common/lifecycle.js';
import { ICommandService } from '../../../../platform/commands/common/commands.js';
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB