Compare commits
354 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ac8365def4 | |||
| 01efab6a1d | |||
| 7f8e1ff00d | |||
| 5b55eed08a | |||
| d618223289 | |||
| 7752278720 | |||
| 597c04f63b | |||
| a7c4a1b179 | |||
| a7aab9cf86 | |||
| c44497510b | |||
| c485d2c478 | |||
| dfdf29fb36 | |||
| 420fb1a186 | |||
| 78a48bae13 | |||
| fc187e0028 | |||
| f973f8d4e7 | |||
| 8403eed8ca | |||
| 718b8cc61b | |||
| f6924df09b | |||
| 8b25f80e9b | |||
| a42ff2d59a | |||
| 0589a2a5ec | |||
| 7dda4bdc08 | |||
| f21ef61677 | |||
| 56b6d406d6 | |||
| 2ae29a50e5 | |||
| 9673e974ea | |||
| 842d388669 | |||
| f146385e1d | |||
| bb87726b34 | |||
| deb30b49bc | |||
| 56923eec52 | |||
| d2692d2594 | |||
| 5d44fdc17d | |||
| aada60c5be | |||
| b53e5d4886 | |||
| f299998994 | |||
| 967292e615 | |||
| 19e3ebefda | |||
| 7cf6c9d798 | |||
| 14e314e5b8 | |||
| 4e6e0b3bde | |||
| 63b086b786 | |||
| bda6ad06c9 | |||
| 829d82850d | |||
| 3991372f5b | |||
| 9b427c78bf | |||
| 5dad4c8f06 | |||
| e2d39fe4b1 | |||
| 1365400f21 | |||
| 94f4d2caaf | |||
| 696abc39fd | |||
| abc7151cd6 | |||
| c86f47743e | |||
| 9d6eb62c3e | |||
| 2f26b34a96 | |||
| 5807c94a61 | |||
| df23314bd2 | |||
| 6703aa1545 | |||
| aaab289ece | |||
| d809839530 | |||
| 9e583960ae | |||
| 19737d4cdc | |||
| 901579a445 | |||
| 051f8f38e7 | |||
| 07fcb3a72c | |||
| 62cdaba44b | |||
| 3147a92463 | |||
| d175ac20ef | |||
| 42a2dec781 | |||
| dbef72c01c | |||
| 6ddf977c5d | |||
| 522f136872 | |||
| a28194c601 | |||
| cb2de5505b | |||
| f84c964d10 | |||
| bb4f99f687 | |||
| 49b43820b2 | |||
| b22bbf9639 | |||
| a1ff458007 | |||
| fb263f9d81 | |||
| 72deba43dd | |||
| 24fdb1ca0d | |||
| f20f41c804 | |||
| 66037e2049 | |||
| adb504f2e0 | |||
| 8b90d7827d | |||
| 62e1657274 | |||
| 7f776eebdb | |||
| 0db0513752 | |||
| d8b6b3c306 | |||
| f7c9d732f5 | |||
| 8a95b13711 | |||
| 55884ed1c9 | |||
| 582b25dc44 | |||
| 252af4c863 | |||
| e5ac36facd | |||
| 77fc2d2d4a | |||
| 298adac06e | |||
| 0f920fb867 | |||
| 02112fbc4f | |||
| 05241649ff | |||
| 4ba00f6216 | |||
| 296cd1b477 | |||
| 2148fe77ec | |||
| 1f692ff01a | |||
| 8e4f943e30 | |||
| a52eba91c9 | |||
| 695d6a1e30 | |||
| a647530d77 | |||
| 18e3f19512 | |||
| 86294c676a | |||
| 7cfd9e5580 | |||
| d29eca457a | |||
| 0855376e02 | |||
| e1692758c6 | |||
| 411167ee44 | |||
| 389efb4701 | |||
| 48f9ac2ba1 | |||
| f9198d4d97 | |||
| eafc3dc0c9 | |||
| f2b176dfda | |||
| 02781927e1 | |||
| 4b3f881e1f | |||
| f3653c1c48 | |||
| 3a1955901e | |||
| e3ce68d2d6 | |||
| ab3d44387e | |||
| 96e24a89e6 | |||
| 25cc6aeb3b | |||
| 4cac92689a | |||
| 7444488278 | |||
| a6cf723dbe | |||
| 7e22b80986 | |||
| 9ec4f3da01 | |||
| ef9ac7a9e3 | |||
| 48426403f3 | |||
| 86c9a17f8d | |||
| d21f2c4d62 | |||
| 733256feab | |||
| f9ce9de316 | |||
| 9f27b17f11 | |||
| 8ff4f1c6e1 | |||
| f6f6259f23 | |||
| c7e9175fd7 | |||
| bdc3c9cb7e | |||
| 790a2b6a4f | |||
| e6c779d91a | |||
| aa95ceafd2 | |||
| 9ef1aa2c60 | |||
| e4107e1257 | |||
| 1d5fef43c1 | |||
| 51280d4e77 | |||
| d5ccc6f777 | |||
| 4ea1bfa9c3 | |||
| c58af2a3af | |||
| d857d54847 | |||
| a7cbed395b | |||
| f6479a0668 | |||
| a50b5c6407 | |||
| 53d19d819c | |||
| a3e337fe2d | |||
| 1bb243bc34 | |||
| faaf3a738a | |||
| e8bbe4bf5a | |||
| 469bba178a | |||
| 879595dbe6 | |||
| f7d2e95d37 | |||
| 9dbc7f5caa | |||
| 1cc35b1d79 | |||
| 2473f210bc | |||
| 604578bb19 | |||
| 1d73aad2ed | |||
| 8508c2dd8e | |||
| a8c5c696be | |||
| 077d460b29 | |||
| f3d9893588 | |||
| c004e1af03 | |||
| 0d227f2bd6 | |||
| 0b96901aee | |||
| d12a5f3b38 | |||
| cd77542a9e | |||
| 0c2660e283 | |||
| 9a591ba29f | |||
| 4c633ee8ff | |||
| 67e9759d35 | |||
| 93dac201b3 | |||
| 946ec70136 | |||
| 161682cf75 | |||
| 6df0093786 | |||
| a527af93b7 | |||
| 775cb2e4ac | |||
| 68b5187f7c | |||
| aacbef3e33 | |||
| f5b2dcfe0a | |||
| 9d25fab5cb | |||
| 250b90634f | |||
| e56716c792 | |||
| 0bf5278168 | |||
| f2c61d2419 | |||
| bdd224990d | |||
| 0ca2ad4930 | |||
| 5140c79dcf | |||
| 91c65b6060 | |||
| afd40a59b5 | |||
| e6177d44c6 | |||
| 63778d2012 | |||
| 7a33cde334 | |||
| c4e29d1c3f | |||
| b6b5deaa58 | |||
| 689a583598 | |||
| 9f6cacdbe1 | |||
| abab461236 | |||
| 23579774b8 | |||
| 0ee85b0ffd | |||
| c99b1a2132 | |||
| 8a9e9495f9 | |||
| 1437f592d2 | |||
| 4ac509f0c8 | |||
| f37440cd3f | |||
| ea83546e42 | |||
| e2b36d735c | |||
| 1ca94080b8 | |||
| 2ad063eb30 | |||
| 2aea2b81c2 | |||
| 3fc81f1baf | |||
| 063d9312c4 | |||
| 04761c9418 | |||
| 620b335dc5 | |||
| a765d738e1 | |||
| 878ec1e782 | |||
| 84d866205b | |||
| a273780472 | |||
| dbea46ba32 | |||
| 50d21828c2 | |||
| a974a5df69 | |||
| 799f6c2d16 | |||
| b8f3182ee3 | |||
| f22bc26355 | |||
| 047cc03002 | |||
| bd49aaffde | |||
| 13f6fd5c66 | |||
| 1fe12d2bfd | |||
| 1cf371fa8b | |||
| ee72a2c3cd | |||
| a5912ba538 | |||
| 19a8d36e07 | |||
| 339aff5d31 | |||
| 4de3f1c7ef | |||
| 6e2030d4d4 | |||
| 665e5e1058 | |||
| 8b9a9c7e2b | |||
| f8f60ef6b0 | |||
| d01a5fc44c | |||
| f713a624f9 | |||
| c789ab2ab7 | |||
| 008a5812f3 | |||
| 06819e172a | |||
| 1112f79cf5 | |||
| 9a54a57cf4 | |||
| c4d620c874 | |||
| 0cc06407ad | |||
| aab065139e | |||
| aa6b5c6b6c | |||
| 9fc3e413a9 | |||
| 289da6bd6d | |||
| 1feb7ec485 | |||
| 5f97a4f8a6 | |||
| 752ebfff3b | |||
| 01d99645a6 | |||
| 00814d51f1 | |||
| e8ff4d8328 | |||
| 8da76e72df | |||
| 652f64cd14 | |||
| b52f0d8dd7 | |||
| f8917e7548 | |||
| 932e9fe9c6 | |||
| 3fd4357503 | |||
| 9b878a9fc0 | |||
| 268d03fad5 | |||
| 3aece81891 | |||
| 6b268a44bb | |||
| 70ce23862b | |||
| 1c32d3a1c9 | |||
| 2814c63765 | |||
| cc3a08e1ab | |||
| 5b3a6d240e | |||
| cc8c4d33ef | |||
| ada31ccf90 | |||
| 4a629be2e2 | |||
| 8897feb290 | |||
| 75f97ea9e1 | |||
| 83b9547701 | |||
| 791f6b1151 | |||
| 1c739e521d | |||
| 794f35fc37 | |||
| 6c93028ce3 | |||
| 49072d43ea | |||
| a5090b8025 | |||
| 36c3cd53e1 | |||
| e4e1c7fb6c | |||
| fcc8de807f | |||
| 9d972f72ea | |||
| 3d629396be | |||
| fe413cb474 | |||
| 277f99e080 | |||
| 584d026256 | |||
| 5acc53311d | |||
| b9ba3a36c4 | |||
| d7c00addd5 | |||
| b34119c4df | |||
| d098291c90 | |||
| ed9e126a4a | |||
| a8010eec15 | |||
| 8cd1ea305c | |||
| 762264b030 | |||
| 48d5369196 | |||
| aab840db2d | |||
| 3526005e1c | |||
| 0ef8c66d54 | |||
| cd9173c802 | |||
| 0c8a185df7 | |||
| 0f0719c596 | |||
| d9397d870f | |||
| 1704a7de7a | |||
| 616cd2e39b | |||
| 222f90bbea | |||
| ab5245d2d9 | |||
| a7a4488848 | |||
| 8e72c1392a | |||
| 70b6af34e1 | |||
| 119462f663 | |||
| 8ee665cd57 | |||
| ace3f181ed | |||
| d3962acd30 | |||
| 18557c8f6e | |||
| 29b037726e | |||
| 59dc193f97 | |||
| bd778d259b | |||
| 605cf236a5 | |||
| 8bc3a0d277 | |||
| d80c88bc38 | |||
| dd34a5be57 | |||
| f4be80b724 | |||
| b40c58b78a | |||
| 64468b6f4d | |||
| 8f83b063f1 | |||
| f56772a07a | |||
| 38662d2824 | |||
| a51873c06a | |||
| a29543e4a7 | |||
| d4a2ac2796 | |||
| 4b5e09c124 | |||
| f8f48fb98b |
@@ -56,7 +56,7 @@ Next: **[Try it out!](#try-it)**
|
||||
|
||||
You may see improved VNC responsiveness when accessing a codespace from VS Code client since you can use a [VNC Viewer][def]. Here's how to do it.
|
||||
|
||||
1. Install [Visual Studio Code Stable](https://code.visualstudio.com/) or [Insiders](https://code.visualstudio.com/insiders/) and the the [GitHub Codespaces extension](https://marketplace.visualstudio.com/items?itemName=GitHub.codespaces).
|
||||
1. Install [Visual Studio Code Stable](https://code.visualstudio.com/) or [Insiders](https://code.visualstudio.com/insiders/) and the [GitHub Codespaces extension](https://marketplace.visualstudio.com/items?itemName=GitHub.codespaces).
|
||||
|
||||
> **Note:** The GitHub Codespaces extension requires the Visual Studio Code distribution of Code - OSS.
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
"jsdoc",
|
||||
"header",
|
||||
"local"
|
||||
// "react" // Void
|
||||
],
|
||||
"rules": {
|
||||
"constructor-super": "warn",
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
# Created for Void
|
||||
# To learn more about how to use Nix to configure your environment
|
||||
# see: https://developers.google.com/idx/guides/customize-idx-env
|
||||
{pkgs}: {
|
||||
# Which nixpkgs channel to use.
|
||||
channel = "stable-23.11"; # or "unstable"
|
||||
# Use https://search.nixos.org/packages to find packages
|
||||
packages = [
|
||||
pkgs.nodejs_20
|
||||
pkgs.yarn
|
||||
pkgs.nodePackages.pnpm
|
||||
pkgs.bun
|
||||
pkgs.gh
|
||||
];
|
||||
# Sets environment variables in the workspace
|
||||
env = {};
|
||||
idx = {
|
||||
# Search for the extensions you want on https://open-vsx.org/ and use "publisher.id"
|
||||
extensions = [
|
||||
# "vscodevim.vim"
|
||||
];
|
||||
workspace = {
|
||||
# Runs when a workspace is first created with this `dev.nix` file
|
||||
onCreate = {
|
||||
npm-install = "npm ci --no-audit --prefer-offline --no-progress --timing";
|
||||
# Open editors for the following files by default, if they exist:
|
||||
default.openFiles = [
|
||||
# Cover all the variations of language, src-dir, router (app/pages)
|
||||
"pages/index.tsx" "pages/index.jsx"
|
||||
"src/pages/index.tsx" "src/pages/index.jsx"
|
||||
"app/page.tsx" "app/page.jsx"
|
||||
"src/app/page.tsx" "src/app/page.jsx"
|
||||
];
|
||||
};
|
||||
# To run something each time the workspace is (re)started, use the `onStart` hook
|
||||
};
|
||||
# Enable previews and customize configuration
|
||||
previews = {
|
||||
enable = true;
|
||||
previews = {
|
||||
web = {
|
||||
command = ["npm" "run" "dev" "--" "--port" "$PORT" "--hostname" "0.0.0.0"];
|
||||
manager = "web";
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
+65
-81
@@ -1,61 +1,65 @@
|
||||
# Contributing to Void
|
||||
|
||||
Welcome! 👋 This is a guide on how to contribute to Void. We want to make it as easy as possible to contribute, so if you have any questions or comments, reach out via email or discord!
|
||||
Welcome! 👋 This is the official guide on how to contribute to Void. We want to make it as easy as possible to contribute, so if you have any questions or comments, reach out via email or discord!
|
||||
|
||||
There are two main ways to contribute:
|
||||
There are a few ways to contribute:
|
||||
|
||||
- Suggest New Features ([discord](https://discord.gg/RSNjgaugJs))
|
||||
- Build New Features ([project](https://github.com/orgs/voideditor/projects/2/views/3))
|
||||
- 👨💻 Build new features - see [Issues](https://github.com/voideditor/void/issues).
|
||||
- 💡 Make suggestions in our [Discord](https://discord.gg/RSNjgaugJs).
|
||||
- ⭐️ If you want to build your AI tool into Void, feel free to get in touch! It's very easy to extend Void, and the UX you create will be much more natural than a VSCode Extension.
|
||||
|
||||
We use a [VSCode extension](https://code.visualstudio.com/api/get-started/your-first-extension) to implement most of Void's functionality. Scroll down to see 1. How to build/contribute to the Extension, or 2. How to build/contribute to the full IDE (for more native changes).
|
||||
Most of Void's code lives in `src/vs/workbench/contrib/void/browser/` and `src/vs/platform/void/`.
|
||||
|
||||
For some useful links we've compiled see [`VOID_USEFUL_LINKS.md`](https://github.com/voideditor/void/blob/main/VOID_USEFUL_LINKS.md).
|
||||
|
||||
## 1. Building the Extension
|
||||
|
||||
Here's how you can start contributing to the Void extension. This is where you should get started if you're new.
|
||||
## Building the full IDE
|
||||
|
||||
1. Clone the repository:
|
||||
### a. Build Prerequisites - Mac
|
||||
|
||||
```
|
||||
git clone https://github.com/voideditor/void
|
||||
```
|
||||
If you're using a Mac, you need Python and XCode. You probably have these by default.
|
||||
|
||||
2. Open the folder `/extensions/void` in VSCode (open it in a new workspace, _don't_ just cd into it).
|
||||
### b. Build Prerequisites - Windows
|
||||
|
||||
3. Install dependencies:
|
||||
If you're using a Windows computer, first get [Visual Studio 2022](https://visualstudio.microsoft.com/thank-you-downloading-visual-studio/?sku=Community) (recommended) or [VS Build Tools](https://visualstudio.microsoft.com/thank-you-downloading-visual-studio/?sku=BuildTools) (not recommended). If you already have both, you might need to run the next few steps on both of them.
|
||||
|
||||
```
|
||||
npm install
|
||||
```
|
||||
Go to the "Workloads" tab and select:
|
||||
- `Desktop development with C++`
|
||||
- `Node.js build tools`
|
||||
|
||||
4. Compile the React by running `npm run build`. We created this build command to convert `sidebar/index.tsx` into `dist/`.
|
||||
Go to the "Individual Components" tab and select:
|
||||
- `MSVC v143 - VS 2022 C++ x64/x86 Spectre-mitigated libs (Latest)`
|
||||
- `C++ ATL for latest build tools with Spectre Mitigations`
|
||||
- `C++ MFC for latest build tools with Spectre Mitigations`
|
||||
|
||||
```
|
||||
npm run build
|
||||
```
|
||||
Finally, click Install.
|
||||
|
||||
5. Run the project by pressing <kbd>F5</kbd>.
|
||||
### c. Build Prerequisites - Linux
|
||||
|
||||
This will start a new instance of VSCode with the extension enabled. If this doesn't work, you can press <kbd>Ctrl+Shift+P</kbd>, select "Debug: Start Debugging", and select "VSCode Extension Development".
|
||||
First, run `npm install -g node-gyp`. Then:
|
||||
|
||||
## 2. Building the full IDE
|
||||
- Debian (Ubuntu, etc): `sudo apt-get install build-essential g++ libx11-dev libxkbfile-dev libsecret-1-dev libkrb5-dev python-is-python3`.
|
||||
- Red Hat (Fedora, etc): `sudo dnf install @development-tools gcc gcc-c++ make libsecret-devel krb5-devel libX11-devel libxkbfile-devel`.
|
||||
- Others: see [How to Contribute](https://github.com/microsoft/vscode/wiki/How-to-Contribute).
|
||||
|
||||
If you want to work on the full IDE, please follow the steps below. If you have any questions/issues, you can refer to VSCode's full [How to Contribute](https://github.com/microsoft/vscode/wiki/How-to-Contribute) page, which is where the steps below come from. Also feel free to submit an issue or get in touch with us with any build errors.
|
||||
### Building Void
|
||||
|
||||
### a. Building on a Mac
|
||||
To build Void, open `void/` inside VSCode. Then:
|
||||
|
||||
To build on a Mac, open `void/` in VSCode. Make sure you've built the extension by following the steps above (or just run `cd ./extensions/void && npm install && npm run build && npm run compile && cd ../..`). Also make sure you have Python and XCode installed on your system (you probably do by default).
|
||||
1. `npm install` to install all dependencies.
|
||||
2. `npm run buildreact` to build Void's browser dependencies like React.
|
||||
3. Build.
|
||||
- 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.
|
||||
- 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+Shift+P</kbd> and run "Reload Window" inside the new window to see changes without re-building, unless they're React changes.
|
||||
|
||||
1. Install all dependencies.
|
||||
|
||||
```
|
||||
npm install
|
||||
```
|
||||
#### Building Void from Terminal
|
||||
|
||||
2. Run `npm run watch`.
|
||||
|
||||
This can take ~5 min. It's done when you see something like:
|
||||
Alternatively, if you want to build Void from the terminal, instead of pressing <kbd>Cmd+Shift+B</kbd> you can run `npm run watch`. The build is done when you see something like this:
|
||||
|
||||
```
|
||||
[watch-extensions] [00:37:39] Finished compilation extensions with 0 errors after 19303 ms
|
||||
@@ -64,75 +68,50 @@ This can take ~5 min. It's done when you see something like:
|
||||
[watch-client ] [00:38:07] Finished compilation with 0 errors after 5 ms
|
||||
```
|
||||
|
||||
<!-- 3. Press <kbd>Ctrl+Shift+B</kbd> to start the build process. -->
|
||||
|
||||
3. In a new terminal, run `./scripts/code.sh`.
|
||||
|
||||
This should open up the built IDE after loading for some time. To see new changes without restarting the build, use <kbd>Ctrl+Shift+P</kbd> and run "Reload Window".
|
||||
|
||||
To bundle the IDE, run `npm run gulp vscode-darwin-arm64`. Here are the full options: `vscode-{win32-ia32 | win32-x64 | darwin-x64 | darwin-arm64 | linux-ia32 | linux-x64 | linux-arm}(-min)`
|
||||
|
||||
Now that you're set up, feel free to check out our [Issues](https://github.com/voideditor/void/issues) page!
|
||||
|
||||
**Common Fixes:**
|
||||
### Common Fixes
|
||||
|
||||
- Make sure you follow the prerequisite steps.
|
||||
- Make sure you have the same NodeJS version as `.nvmrc`.
|
||||
|
||||
- If you see `X [ERROR] Cannot start service: Host version "0.23.1" does not match binary version "0.23.0"`, run `npm i -D esbuild@0.23.0`
|
||||
|
||||
### b. Building on Windows
|
||||
|
||||
To build on Windows, please refer to [How to Contribute](https://github.com/microsoft/vscode/wiki/How-to-Contribute). We recommend building on Mac; we're Windows users who switch to Mac to build right now.
|
||||
|
||||
<!-- Get [Visual Studio 2022](https://visualstudio.microsoft.com/thank-you-downloading-visual-studio/?sku=Community). Also find the boxes for "Desktop development with C++" and "Node.js development" and get those, too.
|
||||
|
||||
If you get a node-gyp error in the next few steps, you should also get [Visual Studio Build Tools](https://visualstudio.microsoft.com/thank-you-downloading-visual-studio/?sku=BuildTools), find the Visual Studio Build Tools box, click Install (or Modify), then in Individual Components:
|
||||
check every item under `MSVC v143 - VS 2022 C++ x64/x86 Spectre-mitigated libs (Latest)`, `C++ ATL for latest build tools with Spectre Mitigations`, and `C++ MFC for latest build tools with Spectre Mitigations`.
|
||||
- If you make any React changes, you must re-run `npm run buildreact` and re-build.
|
||||
- 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.
|
||||
|
||||
|
||||
```
|
||||
npm config set msvs_version 2022
|
||||
```
|
||||
-->
|
||||
|
||||
## Roadmap
|
||||
|
||||
Here are the most important topics on our Roadmap. More ⭐'s = more important.
|
||||
## Bundling
|
||||
|
||||
These sometimes get outdated - please refer to our Issues page for the latest issues.
|
||||
We don't usually recommend bundling. Instead, you should probably just build. If you're sure you want to bundle Void into an executable app, run one of the following commands. This will create a folder named `VSCode-darwin-arm64` (or similar) in the repo's parent's directory. Be patient - compiling can take ~25 minutes.
|
||||
|
||||
## ⭐⭐⭐ Make History work well.
|
||||
### Mac
|
||||
- `npm run gulp vscode-darwin-arm64` - most common (Apple Silicon)
|
||||
- `npm run gulp vscode-darwin-x64` (Intel)
|
||||
|
||||
When the user submits a response or presses the apply/accept/reject button, we should add these events to the history, allowing the user to undo/redo them. Right now there is unexpected behavior if the user tries to undo or redo their changes.
|
||||
### Windows
|
||||
- `npm run gulp vscode-win32-x64` - most common
|
||||
- `npm run gulp vscode-win32-ia32`
|
||||
|
||||
## ⭐⭐⭐ Build Cursor-style quick edits (ctrl+k).
|
||||
### Linux
|
||||
- `npm run gulp vscode-linux-x64` - most common
|
||||
- `npm run gulp vscode-linux-arm`
|
||||
- `npm run gulp vscode-linux-ia32`
|
||||
|
||||
When the user presses ctrl+k, an input box should appear inline with the code that they were selecting. This is somewhat difficult to do because an extension alone cannot do this, and it requires creating a new component in the IDE. We think you can modify vscode's built-in "codelens" or "zone widget" components, but we are open to alternatives.
|
||||
|
||||
## ⭐⭐⭐ Creative.
|
||||
|
||||
Examples: creating better code search, or supporting AI agents that can edit across files and make multiple LLM calls.
|
||||
|
||||
Eventually, we want to build a convenient API for creating AI tools. The API will provide methods for creating the UI (showing an autocomplete suggestion, or creating a new diff), detecting event changes (like `onKeystroke` or `onFileOpen`), and modifying the user's file-system (storing indexes associated with each file), making it much easier to make your own AI plugin. We plan on building these features further along in timeline, but we wanted to list them for completeness.
|
||||
|
||||
## ⭐ One-stars.
|
||||
|
||||
⭐ Let the user accept / reject all Diffs in an entire file via the sidebar.
|
||||
|
||||
# Guidelines
|
||||
|
||||
Please don't make big refactors without speaking with us first. We'd like to keep the codebase similar to vscode so we can periodically rebase, and if we have big changes that gets complicated.
|
||||
We're always glad to talk about new ideas, help you get set up, and make sure your changes align with our vision for the project! Feel free to shoot Mat or Andrew a message, or start chatting with us in the `#contributing` channel of our [Discord](https://discord.gg/RSNjgaugJs).
|
||||
|
||||
# Submitting a Pull Request
|
||||
|
||||
Please submit a pull request once you've made a change. Here are a few guidelines:
|
||||
## Submitting a Pull Request
|
||||
|
||||
- A PR should be about one _single_ feature change. The fewer items you change, the more likely the PR is to be accepted.
|
||||
- Please submit a pull request once you've made a change. No need to submit an Issue unless you're creating a new feature.
|
||||
- Please don't use AI to write your PR 🙂.
|
||||
|
||||
- Your PR should contain a description that first explains at a high level what you did, and then describes the exact changes you made (and to which files). Please don't use vague statements like "refactored code" or "improved types" (instead, describe what code you refactored, or what types you changed).
|
||||
|
||||
- Try to avoid refactoring and making feature changes in the same PR.
|
||||
|
||||
<!--
|
||||
# Relevant files
|
||||
|
||||
We keep track of all the files we've changed with Void so it's easy to rebase:
|
||||
@@ -156,3 +135,8 @@ We keep track of all the files we've changed with Void so it's easy to rebase:
|
||||
- build/npm/dirs.js
|
||||
|
||||
- vscode.proposed.editorInsets.d.ts - not modified, but code copied
|
||||
-->
|
||||
|
||||
## References
|
||||
|
||||
For some useful links we've compiled on VSCode, see [`VOID_USEFUL_LINKS.md`](https://github.com/voideditor/void/blob/main/VOID_USEFUL_LINKS.md).
|
||||
|
||||
@@ -1,22 +1,16 @@
|
||||
# Welcome to Void.
|
||||
|
||||
Void is the open-source Cursor alternative. This repo contains the full sourcecode for Void. We have a [waitlist](https://voideditor.com/email) for downloading the official release, but you can build and develop Void right now.
|
||||
|
||||
Void is the open-source Cursor alternative.
|
||||
|
||||
If you're new, welcome! Feel free to check out our [Project board](https://github.com/orgs/voideditor/projects/2/views/3) for the most pressing Issues to work on, and see [`CONTRIBUTING.md`](https://github.com/voideditor/void/blob/main/CONTRIBUTING.md) for instructions on building and running Void.
|
||||
|
||||
This repo contains the full sourcecode for Void. We have a [waitlist](https://voideditor.com/email) for downloading the official release, but you can build and develop Void right now.
|
||||
If you're new, welcome!
|
||||
|
||||
## Contributing
|
||||
|
||||
To build and run Void, see [`CONTRIBUTING.md`](https://github.com/voideditor/void/blob/main/CONTRIBUTING.md).
|
||||
To build and run Void, follow the steps in [`CONTRIBUTING.md`](https://github.com/voideditor/void/blob/main/CONTRIBUTING.md).
|
||||
|
||||
## Reference
|
||||
|
||||
Void is a fork of the of [vscode](https://github.com/microsoft/vscode) repository.
|
||||
|
||||
For some useful links we've compiled see [`VOID_USEFUL_LINKS.md`](https://github.com/voideditor/void/blob/main/VOID_USEFUL_LINKS.md).
|
||||
|
||||
Void is a fork of the of [vscode](https://github.com/microsoft/vscode) repository. For some useful links on VSCode, see [`VOID_USEFUL_LINKS.md`](https://github.com/voideditor/void/blob/main/VOID_USEFUL_LINKS.md).
|
||||
|
||||
## Support
|
||||
Feel free to reach out in our [Discord](https://discord.gg/RSNjgaugJs) or contact us via email.
|
||||
|
||||
@@ -2,23 +2,27 @@
|
||||
|
||||
The Void team put together this list of links to get up and running with VSCode's sourcecode. We hope it's helpful!
|
||||
|
||||
## Contributing
|
||||
|
||||
- [How VSCode's sourcecode is organized](https://github.com/microsoft/vscode/wiki/Source-Code-Organization) - this explains where the entry point files are, what `browser/` and `common/` mean, etc. This is the most important read on this whole list! We recommend reading the whole thing.
|
||||
|
||||
- [Built-in VSCode styles](https://code.visualstudio.com/api/references/theme-color) - CSS variables that are built into VSCode. Use `var(--vscode-{theme but replacing . with -})`. You can also see their [Webview theming guide](https://code.visualstudio.com/api/extension-guides/webview#theming-webview-content).
|
||||
|
||||
## Beginners / Getting started
|
||||
|
||||
- [VSCode UI guide](https://code.visualstudio.com/docs/getstarted/userinterface) - covers auxbar, panels, etc.
|
||||
|
||||
- [UX guide](https://code.visualstudio.com/api/ux-guidelines/overview) - covers Containers, Views, Items, etc.
|
||||
|
||||
## Contributing
|
||||
|
||||
- [How VS Code's sourcecode is organized](https://github.com/microsoft/vscode/wiki/Source-Code-Organization) - this explains where the entry point files are, what `browser/` and `common/` mean, etc. This is the most important read on this whole list! We recommend reading the whole thing.
|
||||
## Misc
|
||||
|
||||
|
||||
- [Every command](https://code.visualstudio.com/api/references/commands) built-in to VSCode - sometimes useful to reference.
|
||||
- [Every command](https://code.visualstudio.com/api/references/commands) built-in to VSCode - not used often, but here for reference.
|
||||
|
||||
|
||||
## VSCode's Extension API
|
||||
|
||||
Void is mainly an extension right now, and these links were very useful for us to get set up.
|
||||
Void is no longer an extension, so these links are no longer required, but they might be useful if we ever build an extension again.
|
||||
|
||||
- [Files you need in an extension](https://code.visualstudio.com/api/get-started/extension-anatomy).
|
||||
|
||||
|
||||
+5
-2
@@ -7,8 +7,6 @@ const fs = require('fs');
|
||||
|
||||
// Complete list of directories where npm should be executed to install node modules
|
||||
const dirs = [
|
||||
'extensions/void', // <-- Void
|
||||
|
||||
'',
|
||||
'build',
|
||||
'extensions',
|
||||
@@ -55,6 +53,11 @@ const dirs = [
|
||||
'test/smoke',
|
||||
'.vscode/extensions/vscode-selfhost-import-aid',
|
||||
'.vscode/extensions/vscode-selfhost-test-provider',
|
||||
|
||||
// Void added these:
|
||||
// 'extensions/void',
|
||||
// 'void-imports',
|
||||
|
||||
];
|
||||
|
||||
if (fs.existsSync(`${__dirname}/../../.build/distro/npm`)) {
|
||||
|
||||
@@ -139,3 +139,21 @@ for (let dir of dirs) {
|
||||
|
||||
cp.execSync('git config pull.rebase merges');
|
||||
cp.execSync('git config blame.ignoreRevsFile .git-blame-ignore-revs');
|
||||
|
||||
|
||||
// // Void added this (inject void-imports into project):
|
||||
// const buildVoidImports = () => {
|
||||
// console.log('\n\nVoid is injecting void-imports...')
|
||||
// cp.execSync(`npm install`, { // this goes here, not in postinstall, because we need to
|
||||
// env: process.env,
|
||||
// cwd: path.join(__dirname, '..', '..', '/void-imports'),
|
||||
// stdio: 'inherit'
|
||||
// });
|
||||
// cp.execSync(`node build-index.mjs`, {
|
||||
// env: process.env,
|
||||
// cwd: path.join(__dirname, '..', '..', '/void-imports'),
|
||||
// stdio: 'inherit'
|
||||
// });
|
||||
// console.log('Done injecting void-imports.')
|
||||
// }
|
||||
// buildVoidImports()
|
||||
|
||||
@@ -1,56 +0,0 @@
|
||||
{
|
||||
"root": true,
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 6,
|
||||
"sourceType": "module",
|
||||
"ecmaFeatures": {
|
||||
"jsx": true
|
||||
}
|
||||
},
|
||||
"plugins": [
|
||||
"@typescript-eslint",
|
||||
"react",
|
||||
"react-hooks"
|
||||
],
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:react/recommended",
|
||||
"plugin:react-hooks/recommended"
|
||||
],
|
||||
"rules": {
|
||||
"@typescript-eslint/naming-convention": [
|
||||
"warn",
|
||||
{
|
||||
"selector": "import",
|
||||
"format": [
|
||||
"camelCase",
|
||||
"PascalCase"
|
||||
]
|
||||
}
|
||||
],
|
||||
"curly": "off",
|
||||
"eqeqeq": "warn",
|
||||
"no-empty": "off",
|
||||
"no-throw-literal": "warn",
|
||||
"semi": "off",
|
||||
"no-unused-vars": "off",
|
||||
"react-hooks/exhaustive-deps": "warn"
|
||||
},
|
||||
"ignorePatterns": [
|
||||
"out",
|
||||
"dist",
|
||||
"**/*.d.ts"
|
||||
],
|
||||
"settings": {
|
||||
"react": {
|
||||
"version": "detect"
|
||||
}
|
||||
},
|
||||
"env": {
|
||||
"browser": true, // enable browser globals linting (window, document, console, etc)
|
||||
"es6": true, // enable ES6 linting
|
||||
"node": true, // enable Node linting (things like Buffer which is used in file reading, etc)
|
||||
"mocha": true // enable Mocha linting
|
||||
}
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
out
|
||||
dist
|
||||
node_modules
|
||||
.vscode-test/
|
||||
*.vsix
|
||||
@@ -1,5 +0,0 @@
|
||||
import { defineConfig } from '@vscode/test-cli';
|
||||
|
||||
export default defineConfig({
|
||||
files: 'out/test/**/*.test.js',
|
||||
});
|
||||
-8
@@ -1,8 +0,0 @@
|
||||
{
|
||||
// See http://go.microsoft.com/fwlink/?LinkId=827846
|
||||
// for the documentation about the extensions.json format
|
||||
"recommendations": [
|
||||
"dbaeumer.vscode-eslint",
|
||||
"ms-vscode.extension-test-runner"
|
||||
]
|
||||
}
|
||||
Vendored
-21
@@ -1,21 +0,0 @@
|
||||
// A launch configuration that compiles the extension and then opens it inside a new window
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Run Extension",
|
||||
"type": "extensionHost",
|
||||
"request": "launch",
|
||||
"args": [
|
||||
"--extensionDevelopmentPath=${workspaceFolder}"
|
||||
],
|
||||
"outFiles": [
|
||||
"${workspaceFolder}/out/**/*.js"
|
||||
],
|
||||
"preLaunchTask": "${defaultBuildTask}"
|
||||
}
|
||||
]
|
||||
}
|
||||
-18
@@ -1,18 +0,0 @@
|
||||
// Place your settings in this file to overwrite default and user settings.
|
||||
{
|
||||
"files.exclude": {
|
||||
"**/.git": true,
|
||||
"**/.svn": true,
|
||||
"**/.hg": true,
|
||||
"**/CVS": true,
|
||||
"**/.DS_Store": true,
|
||||
"**/Thumbs.db": true,
|
||||
"out": false,
|
||||
"**/node_modules": true
|
||||
},
|
||||
"search.exclude": {
|
||||
"out": true // set this to false to include "out" folder in search results
|
||||
},
|
||||
// Turn off tsc task auto detection since we have the necessary tasks as npm scripts
|
||||
"typescript.tsc.autoDetect": "off",
|
||||
}
|
||||
Vendored
-20
@@ -1,20 +0,0 @@
|
||||
// See https://go.microsoft.com/fwlink/?LinkId=733558
|
||||
// for the documentation about the tasks.json format
|
||||
{
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"type": "npm",
|
||||
"script": "watch",
|
||||
"problemMatcher": "$tsc-watch",
|
||||
"isBackground": true,
|
||||
"presentation": {
|
||||
"reveal": "never"
|
||||
},
|
||||
"group": {
|
||||
"kind": "build",
|
||||
"isDefault": true
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
.vscode/**
|
||||
.vscode-test/**
|
||||
src/**
|
||||
.gitignore
|
||||
.yarnrc
|
||||
vsc-extension-quickstart.md
|
||||
**/tsconfig.json
|
||||
**/.eslintrc.json
|
||||
**/*.map
|
||||
**/*.ts
|
||||
**/.vscode-test.*
|
||||
@@ -0,0 +1,333 @@
|
||||
import * as vscode from 'vscode';
|
||||
import Parser from 'tree-sitter';
|
||||
import JavaScript from 'tree-sitter-javascript';
|
||||
|
||||
interface Definition {
|
||||
file: string;
|
||||
node: Parser.SyntaxNode;
|
||||
}
|
||||
|
||||
interface DefnUse {
|
||||
parent: Parser.SyntaxNode;
|
||||
file: string;
|
||||
}
|
||||
|
||||
interface ImportInfo {
|
||||
source: string;
|
||||
imported: string;
|
||||
}
|
||||
|
||||
class ProjectAnalyzer {
|
||||
private parser: Parser;
|
||||
private graph: Map<string, Set<string>>;
|
||||
private visited: Set<string>;
|
||||
private parsedFiles: Map<string, Parser.Tree>;
|
||||
private imports: Map<string, Map<string, ImportInfo>>;
|
||||
private definitions: Map<string, Definition>;
|
||||
private fileStack: Set<string>;
|
||||
|
||||
constructor() {
|
||||
this.parser = new Parser();
|
||||
this.parser.setLanguage(JavaScript);
|
||||
this.graph = new Map();
|
||||
this.visited = new Set();
|
||||
this.parsedFiles = new Map();
|
||||
this.imports = new Map();
|
||||
this.definitions = new Map();
|
||||
this.fileStack = new Set();
|
||||
}
|
||||
|
||||
async parseFile(filePath: string): Promise<Parser.Tree | null> {
|
||||
if (this.parsedFiles.has(filePath)) {
|
||||
return this.parsedFiles.get(filePath)!;
|
||||
}
|
||||
|
||||
if (this.fileStack.has(filePath)) {
|
||||
return null; // Circular import
|
||||
}
|
||||
|
||||
this.fileStack.add(filePath);
|
||||
|
||||
try {
|
||||
const uri = vscode.Uri.file(filePath);
|
||||
const document = await vscode.workspace.openTextDocument(uri);
|
||||
const code = document.getText();
|
||||
const tree = this.parser.parse(code);
|
||||
|
||||
this.parsedFiles.set(filePath, tree);
|
||||
this.collectImports(filePath, tree);
|
||||
this.collectDefinitions(filePath, tree);
|
||||
|
||||
return tree;
|
||||
} catch (error) {
|
||||
console.error(`Error parsing ${filePath}:`, error);
|
||||
return null;
|
||||
} finally {
|
||||
this.fileStack.delete(filePath);
|
||||
}
|
||||
}
|
||||
|
||||
private collectImports(filePath: string, tree: Parser.Tree): void {
|
||||
const fileImports = new Map<string, ImportInfo>();
|
||||
|
||||
const visit = (node: Parser.SyntaxNode): void => {
|
||||
if (node.type === 'import_declaration') {
|
||||
const source = node.childForFieldName('source')?.text.slice(1, -1) ?? '';
|
||||
const specifiers = node.childForFieldName('specifiers');
|
||||
|
||||
specifiers?.children.forEach(spec => {
|
||||
if (spec.type === 'import_specifier') {
|
||||
const local = spec.childForFieldName('local')?.text ?? '';
|
||||
const imported = spec.childForFieldName('imported')?.text ?? '';
|
||||
fileImports.set(local, { source, imported });
|
||||
}
|
||||
});
|
||||
}
|
||||
node.children.forEach(visit);
|
||||
};
|
||||
|
||||
visit(tree.rootNode);
|
||||
this.imports.set(filePath, fileImports);
|
||||
}
|
||||
|
||||
private collectDefinitions(filePath: string, tree: Parser.Tree): void {
|
||||
const visit = (node: Parser.SyntaxNode): void => {
|
||||
if (node.type === 'function_declaration') {
|
||||
const name = node.childForFieldName('name')?.text ?? '';
|
||||
this.definitions.set(name, { file: filePath, node });
|
||||
}
|
||||
else if (node.type === 'variable_declarator') {
|
||||
const name = node.childForFieldName('name')?.text;
|
||||
const value = node.childForFieldName('value');
|
||||
if (name && (value?.type === 'arrow_function' || value?.type === 'function')) {
|
||||
this.definitions.set(name, { file: filePath, node: value });
|
||||
}
|
||||
}
|
||||
node.children.forEach(visit);
|
||||
};
|
||||
|
||||
visit(tree.rootNode);
|
||||
}
|
||||
|
||||
private async getTypeFromPosition(uri: vscode.Uri, position: vscode.Position): Promise<string | null> {
|
||||
const hover = await vscode.commands.executeCommand<vscode.Hover[]>(
|
||||
'vscode.executeHoverProvider',
|
||||
uri,
|
||||
position
|
||||
);
|
||||
|
||||
if (hover?.[0]?.contents.length) {
|
||||
for (const content of hover[0].contents) {
|
||||
let hoverText = typeof content === 'string' ?
|
||||
content :
|
||||
('value' in content ? content.value : '');
|
||||
|
||||
// Remove typescript backticks if present
|
||||
hoverText = hoverText.replace(/```typescript\s*/, '').replace(/```\s*$/, '');
|
||||
console.log('Processing hover text:', hoverText);
|
||||
|
||||
// Extract the type information - look for the type after the colon
|
||||
const typeMatches = [
|
||||
/:\s*([\w<>]+)(?:\[\])?/, // matches "foo: Type" or "foo: Type[]"
|
||||
/var\s+\w+:\s*([\w<>]+)/, // matches "var foo: Type"
|
||||
/\(type\)\s+[\w<>]+:\s*([\w<>]+)/, // matches "(type) foo: Type"
|
||||
/\(method\)\s*([\w<>]+)\./ // matches "(method) Type.method"
|
||||
];
|
||||
|
||||
for (const pattern of typeMatches) {
|
||||
const match = pattern.exec(hoverText);
|
||||
if (match) {
|
||||
let type = match[1];
|
||||
// Handle array types
|
||||
if (hoverText.includes('[]')) {
|
||||
return 'Array';
|
||||
}
|
||||
// Extract base type from generics
|
||||
if (type.includes('<')) {
|
||||
type = type.split('<')[0];
|
||||
}
|
||||
return type;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private async getCallsInDefn(defnNode: Parser.SyntaxNode, currentFile: string): Promise<Set<string>> {
|
||||
const calls = new Set<string>();
|
||||
const fileImports = this.imports.get(currentFile) ?? new Map();
|
||||
const uri = vscode.Uri.file(currentFile);
|
||||
|
||||
const visit = async (node: Parser.SyntaxNode): Promise<void> => {
|
||||
if (node.type === 'call_expression') {
|
||||
const callee = node.childForFieldName('function');
|
||||
if (callee?.type === 'identifier') {
|
||||
const name = callee.text;
|
||||
const importInfo = fileImports.get(name);
|
||||
if (importInfo) {
|
||||
calls.add(`${importInfo.source}:${importInfo.imported}`);
|
||||
} else {
|
||||
calls.add(name);
|
||||
}
|
||||
}
|
||||
else if (callee?.type === 'member_expression') {
|
||||
const method = callee.childForFieldName('property')?.text;
|
||||
const object = callee.childForFieldName('object');
|
||||
|
||||
if (method && object) {
|
||||
const position = new vscode.Position(
|
||||
object.startPosition.row,
|
||||
object.startPosition.column
|
||||
);
|
||||
|
||||
const type = await this.getTypeFromPosition(uri, position);
|
||||
if (type) {
|
||||
calls.add(`${type}.${method}`);
|
||||
} else {
|
||||
calls.add(`method:${method}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const child of node.children) {
|
||||
await visit(child);
|
||||
}
|
||||
};
|
||||
|
||||
await visit(defnNode);
|
||||
return calls;
|
||||
}
|
||||
|
||||
private gotoDefn(name: string): Definition | null {
|
||||
if (name.includes(':')) {
|
||||
const [file, funcName] = name.split(':');
|
||||
const def = this.definitions.get(funcName);
|
||||
return def ?? null;
|
||||
}
|
||||
|
||||
return this.definitions.get(name) ?? null;
|
||||
}
|
||||
|
||||
private getUses(defnNode: Parser.SyntaxNode, currentFile: string): DefnUse[] {
|
||||
const uses: DefnUse[] = [];
|
||||
|
||||
let fnName: string | undefined;
|
||||
if (defnNode.type === 'function_declaration') {
|
||||
fnName = defnNode.childForFieldName('name')?.text;
|
||||
} else if (defnNode.type === 'arrow_function' || defnNode.type === 'function') {
|
||||
const parent = defnNode.parent;
|
||||
if (parent?.type === 'variable_declarator') {
|
||||
fnName = parent.childForFieldName('name')?.text;
|
||||
}
|
||||
}
|
||||
|
||||
if (!fnName) return uses;
|
||||
|
||||
for (const [file, tree] of this.parsedFiles) {
|
||||
const visit = (node: Parser.SyntaxNode): void => {
|
||||
if (node.type === 'call_expression') {
|
||||
const callee = node.childForFieldName('function');
|
||||
if (callee?.type === 'identifier' && callee.text === fnName) {
|
||||
let current: Parser.SyntaxNode | null = node;
|
||||
while (current) {
|
||||
if (current.type === 'function_declaration' ||
|
||||
current.type === 'arrow_function' ||
|
||||
current.type === 'function') {
|
||||
uses.push({ parent: current, file });
|
||||
break;
|
||||
}
|
||||
current = current.parent;
|
||||
}
|
||||
}
|
||||
}
|
||||
node.children.forEach(visit);
|
||||
};
|
||||
|
||||
visit(tree.rootNode);
|
||||
}
|
||||
|
||||
return uses;
|
||||
}
|
||||
|
||||
private async visitAllNodesInGraphFromDefinition(defn: Parser.SyntaxNode, currentFile: string): Promise<void> {
|
||||
let defnName: string | undefined;
|
||||
if (defn.type === 'function_declaration') {
|
||||
defnName = defn.childForFieldName('name')?.text;
|
||||
} else if (defn.type === 'arrow_function' || defn.type === 'function') {
|
||||
const parent = defn.parent;
|
||||
if (parent?.type === 'variable_declarator') {
|
||||
defnName = parent.childForFieldName('name')?.text;
|
||||
}
|
||||
}
|
||||
|
||||
if (!defnName) return;
|
||||
|
||||
const fullName = `${currentFile}:${defnName}`;
|
||||
if (this.visited.has(fullName)) return;
|
||||
|
||||
const calls = await this.getCallsInDefn(defn, currentFile);
|
||||
this.graph.set(fullName, calls);
|
||||
this.visited.add(fullName);
|
||||
|
||||
const callDefns = Array.from(calls).map(call => this.gotoDefn(call));
|
||||
for (const callDefn of callDefns) {
|
||||
if (callDefn) {
|
||||
await this.visitAllNodesInGraphFromDefinition(callDefn.node, callDefn.file);
|
||||
}
|
||||
}
|
||||
|
||||
const defnUses = this.getUses(defn, currentFile);
|
||||
for (const defnUse of defnUses) {
|
||||
await this.visitAllNodesInGraphFromDefinition(defnUse.parent, defnUse.file);
|
||||
}
|
||||
}
|
||||
|
||||
async analyze(entryFile: string): Promise<Map<string, Set<string>>> {
|
||||
const tree = await this.parseFile(entryFile);
|
||||
if (!tree) return new Map();
|
||||
|
||||
const visit = async (node: Parser.SyntaxNode): Promise<void> => {
|
||||
if (node.type === 'function_declaration') {
|
||||
await this.visitAllNodesInGraphFromDefinition(node, entryFile);
|
||||
}
|
||||
else if (node.type === 'variable_declarator') {
|
||||
const value = node.childForFieldName('value');
|
||||
if (value?.type === 'arrow_function' || value?.type === 'function') {
|
||||
await this.visitAllNodesInGraphFromDefinition(value, entryFile);
|
||||
}
|
||||
}
|
||||
for (const child of node.children) {
|
||||
await visit(child);
|
||||
}
|
||||
};
|
||||
|
||||
await visit(tree.rootNode);
|
||||
return this.graph;
|
||||
}
|
||||
}
|
||||
|
||||
export async function runTreeSitter(filePath?: string): Promise<Map<string, Set<string>> | null> {
|
||||
const editor = vscode.window.activeTextEditor;
|
||||
if (!editor && !filePath) {
|
||||
vscode.window.showWarningMessage('No active editor found');
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const targetPath = filePath ?? editor!.document.uri.fsPath;
|
||||
const analyzer = new ProjectAnalyzer();
|
||||
const graph = await analyzer.analyze(targetPath);
|
||||
|
||||
for (const [defn, calls] of graph) {
|
||||
console.log(`${defn} calls: ${[...calls].join(', ')}`);
|
||||
}
|
||||
|
||||
return graph;
|
||||
} catch (error) {
|
||||
console.error('Error analyzing file:', error);
|
||||
vscode.window.showErrorMessage('Error analyzing file');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
import * as vscode from 'vscode';
|
||||
|
||||
const legend = new vscode.SemanticTokensLegend([], []);
|
||||
|
||||
export async function findFunctions() {
|
||||
|
||||
const editor = vscode.window.activeTextEditor;
|
||||
if (!editor) return;
|
||||
const document = editor.document;
|
||||
|
||||
const tokens = await vscode.commands.executeCommand<vscode.SemanticTokens>(
|
||||
'vscode.provideDocumentSemanticTokens',
|
||||
document.uri
|
||||
);
|
||||
|
||||
if (!tokens) {
|
||||
console.error('No tokens found');
|
||||
return [];
|
||||
}
|
||||
|
||||
const allTokens = decodeTokens(tokens, document);
|
||||
|
||||
|
||||
return allTokens;
|
||||
}
|
||||
|
||||
function decodeTokens(tokens: vscode.SemanticTokens, document: vscode.TextDocument) {
|
||||
const data = tokens.data;
|
||||
const decodedTokens = [];
|
||||
let line = 0;
|
||||
let character = 0;
|
||||
|
||||
for (let i = 0; i < data.length; i += 5) {
|
||||
const deltaLine = data[i];
|
||||
const deltaStartChar = data[i + 1];
|
||||
const length = data[i + 2];
|
||||
const tokenTypeIdx = data[i + 3];
|
||||
const tokenModifierIdx = data[i + 4];
|
||||
|
||||
line += deltaLine;
|
||||
character = deltaLine === 0 ? character + deltaStartChar : deltaStartChar;
|
||||
|
||||
const type = legend.tokenTypes[tokenTypeIdx] || `(${tokenTypeIdx})`;
|
||||
const modifier = legend.tokenModifiers[tokenModifierIdx] || `(${tokenModifierIdx})`;
|
||||
|
||||
const tokenRange = new vscode.Range(line, character, line, character + length);
|
||||
const tokenText = document.getText(tokenRange);
|
||||
|
||||
decodedTokens.push({
|
||||
line,
|
||||
startCharacter: character,
|
||||
length,
|
||||
type,
|
||||
modifier,
|
||||
text: tokenText,
|
||||
});
|
||||
|
||||
console.log(`Token: '${tokenText}' | Type: ${type} | Modifier: ${modifier} | Line: ${line}, Character: ${character}`);
|
||||
}
|
||||
|
||||
return decodedTokens;
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
Please see the `CONTRIBUTING.md` for information on how to contribute :)!
|
||||
|
||||
|
||||
Here's an overview on how the extension works:
|
||||
|
||||
- The extension mounts in `extension.ts`.
|
||||
|
||||
- The Sidebar's HTML (everything in `sidebar/`) is built in React, and it's rendered by mounting a `<script>` tag - see `SidebarWebviewProvider.ts`.
|
||||
|
||||
- Communication between the sidebar script and the extension takes place via API. You can search for "postMessage" to see where API calls happen.
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
This folder is no longer relevant, since Void is no longer just an extension.
|
||||
|
||||
See the new Void code in `src/vs/workbench/contrib/void` (and a few other locations - you can just search "Void").
|
||||
@@ -1,19 +0,0 @@
|
||||
const tailwindcss = require('tailwindcss')
|
||||
const autoprefixer = require('autoprefixer')
|
||||
const postcss = require('postcss')
|
||||
const fs = require('fs')
|
||||
|
||||
const from = 'src/sidebar/styles.css'
|
||||
const to = 'dist/sidebar/styles.css'
|
||||
|
||||
const original_css_contents = fs.readFileSync(from, 'utf8')
|
||||
|
||||
postcss([
|
||||
tailwindcss, // this compiles tailwind of all the files specified in tailwind.config.json
|
||||
autoprefixer,
|
||||
])
|
||||
.process(original_css_contents, { from, to })
|
||||
.then(processed_css_contents => { fs.writeFileSync(to, processed_css_contents.css) })
|
||||
.catch(error => {
|
||||
console.error('Error in build-css:', error)
|
||||
})
|
||||
@@ -1,13 +0,0 @@
|
||||
const esbuild = require('esbuild')
|
||||
|
||||
// Build JS
|
||||
esbuild.build({
|
||||
entryPoints: ['src/sidebar/index.tsx'],
|
||||
bundle: true,
|
||||
minify: true,
|
||||
sourcemap: true,
|
||||
outfile: 'dist/sidebar/index.js',
|
||||
format: 'iife', // apparently iife is safe for browsers (safer than cjs)
|
||||
platform: 'browser',
|
||||
external: ['vscode'],
|
||||
}).catch(() => process.exit(1));
|
||||
Generated
-9073
File diff suppressed because it is too large
Load Diff
@@ -1,155 +0,0 @@
|
||||
{
|
||||
"name": "void",
|
||||
"publisher": "void",
|
||||
"displayName": "Void",
|
||||
"description": "",
|
||||
"version": "0.0.1",
|
||||
"engines": {
|
||||
"vscode": "^1.92.0"
|
||||
},
|
||||
"categories": [
|
||||
"Other"
|
||||
],
|
||||
"activationEvents": [],
|
||||
"main": "./out/extension.js",
|
||||
"contributes": {
|
||||
"configuration": {
|
||||
"title": "Void",
|
||||
"properties": {}
|
||||
},
|
||||
"commands": [
|
||||
{
|
||||
"command": "void.ctrl+l",
|
||||
"title": "Show Sidebar"
|
||||
},
|
||||
{
|
||||
"command": "void.ctrl+k",
|
||||
"title": "Make Inline Edit"
|
||||
},
|
||||
{
|
||||
"command": "void.acceptDiff",
|
||||
"title": "Approve Diff"
|
||||
},
|
||||
{
|
||||
"command": "void.rejectDiff",
|
||||
"title": "Discard Diff"
|
||||
},
|
||||
{
|
||||
"command": "void.startNewThread",
|
||||
"title": "Start a new chat",
|
||||
"icon": "$(add)"
|
||||
},
|
||||
{
|
||||
"command": "void.toggleThreadSelector",
|
||||
"title": "View past chats",
|
||||
"icon": "$(history)"
|
||||
},
|
||||
{
|
||||
"command": "void.toggleSettings",
|
||||
"title": "Void settings",
|
||||
"icon": "$(settings-gear)"
|
||||
}
|
||||
],
|
||||
"viewsContainers": {
|
||||
"activitybar": [
|
||||
{
|
||||
"id": "voidViewContainer",
|
||||
"title": "Chat",
|
||||
"icon": "$(hubot)"
|
||||
}
|
||||
]
|
||||
},
|
||||
"views": {
|
||||
"voidViewContainer": [
|
||||
{
|
||||
"type": "webview",
|
||||
"id": "void.viewnumberone",
|
||||
"name": "Void"
|
||||
}
|
||||
]
|
||||
},
|
||||
"keybindings": [
|
||||
{
|
||||
"command": "void.ctrl+l",
|
||||
"key": "ctrl+l",
|
||||
"mac": "cmd+l"
|
||||
},
|
||||
{
|
||||
"command": "void.ctrl+k",
|
||||
"key": "ctrl+k",
|
||||
"mac": "cmd+k"
|
||||
}
|
||||
],
|
||||
"menus": {
|
||||
"view/title": [
|
||||
{
|
||||
"command": "void.startNewThread",
|
||||
"when": "view == 'void.viewnumberone'",
|
||||
"group": "navigation"
|
||||
},
|
||||
{
|
||||
"command": "void.toggleThreadSelector",
|
||||
"when": "view == 'void.viewnumberone'",
|
||||
"group": "navigation"
|
||||
},
|
||||
{
|
||||
"command": "void.toggleSettings",
|
||||
"when": "view == 'void.viewnumberone'",
|
||||
"group": "navigation"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"vscode:prepublish": "npm run compile",
|
||||
"compile": "tsc -p ./",
|
||||
"watch": "tsc -watch -p ./",
|
||||
"build": "rimraf dist && node build-tsx.js && node build-css.js",
|
||||
"pretest": "tsc -p ./ && eslint src --ext ts",
|
||||
"test": "vscode-test"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@anthropic-ai/sdk": "^0.29.2",
|
||||
"@eslint/js": "^9.9.1",
|
||||
"@monaco-editor/react": "^4.6.0",
|
||||
"@rrweb/types": "^2.0.0-alpha.17",
|
||||
"@types/diff": "^5.2.2",
|
||||
"@types/diff-match-patch": "^1.0.36",
|
||||
"@types/jest": "^29.5.12",
|
||||
"@types/lodash": "^4.17.12",
|
||||
"@types/mocha": "^10.0.8",
|
||||
"@types/node": "^22.5.1",
|
||||
"@types/react": "^18.3.4",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"@types/react-syntax-highlighter": "^15.5.13",
|
||||
"@types/uuid": "^10.0.0",
|
||||
"@types/vscode": "1.92.0",
|
||||
"@typescript-eslint/eslint-plugin": "^8.3.0",
|
||||
"@typescript-eslint/parser": "^8.3.0",
|
||||
"@vscode/test-cli": "^0.0.10",
|
||||
"@vscode/test-electron": "2.4.1",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"diff-match-patch": "^1.0.5",
|
||||
"esbuild": "^0.23.1",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-plugin-react": "^7.35.1",
|
||||
"eslint-plugin-react-hooks": "^4.6.2",
|
||||
"globals": "^15.9.0",
|
||||
"lodash": "^4.17.21",
|
||||
"marked": "^14.1.0",
|
||||
"ollama": "^0.5.9",
|
||||
"openai": "^4.68.4",
|
||||
"postcss": "^8.4.41",
|
||||
"posthog-js": "^1.176.0",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-markdown": "^9.0.1",
|
||||
"react-syntax-highlighter": "^15.6.1",
|
||||
"rimraf": "^6.0.1",
|
||||
"rrweb-snapshot": "^2.0.0-alpha.4",
|
||||
"tailwindcss": "^3.4.10",
|
||||
"typescript": "5.5.4",
|
||||
"typescript-eslint": "^8.3.0",
|
||||
"uuid": "^10.0.0"
|
||||
}
|
||||
}
|
||||
@@ -1,560 +0,0 @@
|
||||
import * as vscode from 'vscode';
|
||||
import { findDiffs } from './findDiffs';
|
||||
import { Diff, DiffArea, BaseDiff, } from './common/shared_types';
|
||||
import { readFileContentOfUri } from './common/readFileContentOfUri';
|
||||
import { throttle } from 'lodash';
|
||||
|
||||
|
||||
const THROTTLE_TIME = 100
|
||||
|
||||
// TODO in theory this should be disposed
|
||||
const greenDecoration = vscode.window.createTextEditorDecorationType({
|
||||
backgroundColor: 'rgba(0 255 51 / 0.2)',
|
||||
isWholeLine: false, // after: { contentText: ' [original]', color: 'rgba(0 255 60 / 0.5)' } // hoverMessage: originalText // this applies to hovering over after:...
|
||||
})
|
||||
const lightGrayDecoration = vscode.window.createTextEditorDecorationType({
|
||||
backgroundColor: 'rgba(218 218 218 / .2)',
|
||||
isWholeLine: true,
|
||||
})
|
||||
const darkGrayDecoration = vscode.window.createTextEditorDecorationType({
|
||||
backgroundColor: 'rgb(148 148 148 / .2)',
|
||||
isWholeLine: true,
|
||||
})
|
||||
|
||||
// responsible for displaying diffs and showing accept/reject buttons
|
||||
export class DiffProvider implements vscode.CodeLensProvider {
|
||||
|
||||
private _originalFileOfDocument: { [docUriStr: string]: string } = {}
|
||||
private _diffAreasOfDocument: { [docUriStr: string]: DiffArea[] } = {}
|
||||
private _diffsOfDocument: { [docUriStr: string]: Diff[] } = {}
|
||||
|
||||
private _diffareaidPool = 0
|
||||
private _diffidPool = 0
|
||||
|
||||
// used internally by vscode
|
||||
private _onDidChangeCodeLenses: vscode.EventEmitter<void> = new vscode.EventEmitter<void>(); // signals a UI refresh on .fire() events
|
||||
public readonly onDidChangeCodeLenses: vscode.Event<void> = this._onDidChangeCodeLenses.event;
|
||||
|
||||
// used internally by vscode
|
||||
public provideCodeLenses(document: vscode.TextDocument, token: vscode.CancellationToken): vscode.ProviderResult<vscode.CodeLens[]> {
|
||||
const docUriStr = document.uri.toString()
|
||||
return this._diffsOfDocument[docUriStr]?.flatMap(diff => diff.lenses) ?? []
|
||||
}
|
||||
|
||||
// declared by us, registered with vscode.languages.registerCodeLensProvider()
|
||||
constructor() {
|
||||
|
||||
console.log('Creating DisplayChangesProvider')
|
||||
|
||||
// this acts as a useEffect every time text changes
|
||||
vscode.workspace.onDidChangeTextDocument((e) => {
|
||||
|
||||
const editor = vscode.window.activeTextEditor
|
||||
|
||||
if (!editor) return
|
||||
|
||||
const docUriStr = editor.document.uri.toString()
|
||||
const changes = e.contentChanges.map(c => ({ startLine: c.range.start.line, endLine: c.range.end.line, text: c.text, }))
|
||||
|
||||
// on user change, grow/shrink/merge/delete diff areas
|
||||
this.refreshDiffAreasModel(docUriStr, changes, 'currentFile')
|
||||
|
||||
// refresh the diffAreas
|
||||
this.refreshStylesAndDiffs(docUriStr)
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
// used by us only
|
||||
public createDiffArea(uri: vscode.Uri, partialDiffArea: Omit<DiffArea, 'diffareaid'>, originalFile: string) {
|
||||
|
||||
const uriStr = uri.toString()
|
||||
|
||||
this._originalFileOfDocument[uriStr] = originalFile
|
||||
|
||||
// make sure array is defined
|
||||
if (!this._diffAreasOfDocument[uriStr]) this._diffAreasOfDocument[uriStr] = []
|
||||
|
||||
// remove all diffAreas that the new `diffArea` is overlapping with
|
||||
this._diffAreasOfDocument[uriStr] = this._diffAreasOfDocument[uriStr].filter(da => {
|
||||
const noOverlap = da.startLine > partialDiffArea.endLine || da.endLine < partialDiffArea.startLine
|
||||
if (!noOverlap) return false
|
||||
return true
|
||||
})
|
||||
|
||||
// add `diffArea` to storage
|
||||
const diffArea = {
|
||||
...partialDiffArea,
|
||||
diffareaid: this._diffareaidPool
|
||||
}
|
||||
this._diffAreasOfDocument[uriStr].push(diffArea)
|
||||
this._diffareaidPool += 1
|
||||
|
||||
return diffArea
|
||||
}
|
||||
|
||||
// used by us only
|
||||
// changes the start/line locations based on the changes that were recently made. does not change any of the diffs in the diff areas
|
||||
// changes tells us how many lines were inserted/deleted so we can grow/shrink the diffAreas accordingly
|
||||
public refreshDiffAreasModel(docUriStr: string, changes: { text: string, startLine: number, endLine: number }[], changesTo: 'originalFile' | 'currentFile') {
|
||||
|
||||
const diffAreas = this._diffAreasOfDocument[docUriStr] || []
|
||||
|
||||
let endName
|
||||
let startName
|
||||
if (changesTo === 'originalFile') {
|
||||
endName = 'originalEndLine' as const
|
||||
startName = 'originalStartLine' as const
|
||||
} else {
|
||||
endName = 'endLine' as const
|
||||
startName = 'startLine' as const
|
||||
}
|
||||
|
||||
for (const change of changes) {
|
||||
|
||||
// here, `change.range` is the range of the original file that gets replaced with `change.text`
|
||||
|
||||
|
||||
// compute net number of newlines lines that were added/removed
|
||||
const numNewLines = (change.text.match(/\n/g) || []).length
|
||||
const numLineDeletions = change.endLine - change.startLine
|
||||
const deltaNewlines = numNewLines - numLineDeletions
|
||||
|
||||
// compute overlap with each diffArea and shrink/elongate the diffArea accordingly
|
||||
for (const diffArea of diffAreas) {
|
||||
|
||||
// if the change is fully within the diffArea, elongate it by the delta amount of newlines
|
||||
if (change.startLine >= diffArea[startName] && change.endLine <= diffArea[endName]) {
|
||||
diffArea[endName] += deltaNewlines
|
||||
}
|
||||
// check if the `diffArea` was fully deleted and remove it if so
|
||||
if (diffArea[startName] > diffArea[endName]) {
|
||||
//remove it
|
||||
const index = diffAreas.findIndex(da => da === diffArea)
|
||||
diffAreas.splice(index, 1)
|
||||
}
|
||||
|
||||
// TODO handle other cases where eg. the change overlaps many diffAreas
|
||||
}
|
||||
|
||||
|
||||
// if a diffArea is below the last character of the change, shift the diffArea up/down by the delta amount of newlines
|
||||
for (const diffArea of diffAreas) {
|
||||
if (diffArea[startName] > change.endLine) {
|
||||
diffArea[startName] += deltaNewlines
|
||||
diffArea[endName] += deltaNewlines
|
||||
}
|
||||
}
|
||||
|
||||
// TODO merge any diffAreas if they overlap with each other as a result from the shift
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// used by us only
|
||||
// refreshes all the diffs inside each diff area, and refreshes the styles
|
||||
public refreshStylesAndDiffs(docUriStr: string) {
|
||||
|
||||
const editor = vscode.window.activeTextEditor // TODO the editor should be that of `docUri` and not necessarily the current editor
|
||||
if (!editor) {
|
||||
console.log('Error: No active editor!')
|
||||
return;
|
||||
}
|
||||
const originalFile = this._originalFileOfDocument[docUriStr]
|
||||
if (!originalFile) {
|
||||
console.log('Error: No original file!')
|
||||
return;
|
||||
}
|
||||
|
||||
const diffAreas = this._diffAreasOfDocument[docUriStr] || []
|
||||
|
||||
// reset all diffs (we update them below)
|
||||
this._diffsOfDocument[docUriStr] = []
|
||||
|
||||
// for each diffArea
|
||||
for (const diffArea of diffAreas) {
|
||||
|
||||
// get code inside of diffArea
|
||||
const originalCode = originalFile.split('\n').slice(diffArea.originalStartLine, diffArea.originalEndLine + 1).join('\n')
|
||||
const currentCode = editor.document.getText(new vscode.Range(diffArea.startLine, 0, diffArea.endLine, Number.MAX_SAFE_INTEGER)).replace(/\r\n/g, '\n')
|
||||
|
||||
// compute the diffs
|
||||
const diffs = findDiffs(originalCode, currentCode)
|
||||
|
||||
// add the diffs to `this._diffsOfDocument[docUriStr]`
|
||||
this.createDiffs(editor.document.uri, diffs, diffArea)
|
||||
|
||||
|
||||
// // print diffs
|
||||
// console.log('!ORIGINAL FILE:', JSON.stringify(originalFile))
|
||||
// console.log('!NEW FILE :', JSON.stringify(editor.document.getText().replace(/\r\n/g, '\n')))
|
||||
// console.log('!AREA originalCode:', JSON.stringify(originalCode))
|
||||
// console.log('!AREA currentCode :', JSON.stringify(currentCode))
|
||||
// for (const diff of this._diffsOfDocument[docUriStr]) {
|
||||
// console.log('------------')
|
||||
// console.log('originalCode:', JSON.stringify(diff.originalCode))
|
||||
// console.log('currentCode:', JSON.stringify(diff.code))
|
||||
// console.log('originalRange:', diff.originalRange.start.line, diff.originalRange.end.line,)
|
||||
// console.log('currentRange:', diff.range.start.line, diff.range.end.line,)
|
||||
// }
|
||||
|
||||
}
|
||||
|
||||
// update green highlighting
|
||||
editor.setDecorations(
|
||||
greenDecoration,
|
||||
(this._diffsOfDocument[docUriStr]
|
||||
.filter(diff => diff.range !== undefined)
|
||||
.map(diff => diff.range)
|
||||
)
|
||||
);
|
||||
|
||||
|
||||
// for each diffArea, highlight its sweepIndex in dark gray
|
||||
editor.setDecorations(
|
||||
darkGrayDecoration,
|
||||
(this._diffAreasOfDocument[docUriStr]
|
||||
.filter(diffArea => diffArea.sweepIndex !== null)
|
||||
.map(diffArea => {
|
||||
let s = diffArea.sweepIndex!
|
||||
return new vscode.Range(s, 0, s, 0)
|
||||
})
|
||||
)
|
||||
)
|
||||
|
||||
// for each diffArea, highlight sweepIndex+1...end in light gray
|
||||
editor.setDecorations(
|
||||
lightGrayDecoration,
|
||||
(this._diffAreasOfDocument[docUriStr]
|
||||
.filter(diffArea => diffArea.sweepIndex !== null)
|
||||
.map(diffArea => {
|
||||
return new vscode.Range(diffArea.sweepIndex! + 1, 0, diffArea.endLine, 0)
|
||||
})
|
||||
)
|
||||
)
|
||||
|
||||
// TODO update red highlighting
|
||||
// this._diffsOfDocument[docUriStr].map(diff => diff.deletedCode)
|
||||
|
||||
// update code lenses
|
||||
this._onDidChangeCodeLenses.fire()
|
||||
|
||||
}
|
||||
|
||||
// used by us only
|
||||
public createDiffs(docUri: vscode.Uri, diffs: BaseDiff[], diffArea: DiffArea) {
|
||||
|
||||
const docUriStr = docUri.toString()
|
||||
|
||||
// if no diffs, set diffs to []
|
||||
if (!this._diffsOfDocument[docUriStr])
|
||||
this._diffsOfDocument[docUriStr] = []
|
||||
|
||||
// add each diff and its codelens to the document
|
||||
for (let i = diffs.length - 1; i > -1; i -= 1) {
|
||||
let suggestedDiff = diffs[i]
|
||||
|
||||
this._diffsOfDocument[docUriStr].push({
|
||||
...suggestedDiff,
|
||||
diffid: this._diffidPool,
|
||||
// originalCode: suggestedDiff.deletedText,
|
||||
lenses: [
|
||||
new vscode.CodeLens(suggestedDiff.range, { title: 'Accept', command: 'void.acceptDiff', arguments: [{ diffid: this._diffidPool, diffareaid: diffArea.diffareaid }] }),
|
||||
new vscode.CodeLens(suggestedDiff.range, { title: 'Reject', command: 'void.rejectDiff', arguments: [{ diffid: this._diffidPool, diffareaid: diffArea.diffareaid }] })
|
||||
]
|
||||
});
|
||||
this._diffidPool += 1
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// called on void.acceptDiff
|
||||
public async acceptDiff({ diffid, diffareaid }: { diffid: number, diffareaid: number }) {
|
||||
const editor = vscode.window.activeTextEditor
|
||||
if (!editor)
|
||||
return
|
||||
|
||||
const docUriStr = editor.document.uri.toString()
|
||||
|
||||
const diffIdx = this._diffsOfDocument[docUriStr].findIndex(diff => diff.diffid === diffid);
|
||||
if (diffIdx === -1) { console.error('Error: DiffID could not be found: ', diffid, diffareaid, this._diffsOfDocument[docUriStr], this._diffAreasOfDocument[docUriStr]); return; }
|
||||
|
||||
const diffareaIdx = this._diffAreasOfDocument[docUriStr].findIndex(diff => diff.diffareaid === diffareaid);
|
||||
if (diffareaIdx === -1) { console.error('Error: DiffAreaID could not be found: ', diffid, diffareaid, this._diffsOfDocument[docUriStr], this._diffAreasOfDocument[docUriStr]); return; }
|
||||
|
||||
const diff = this._diffsOfDocument[docUriStr][diffIdx]
|
||||
const originalFile = this._originalFileOfDocument[docUriStr]
|
||||
const currentFile = await readFileContentOfUri(editor.document.uri)
|
||||
|
||||
// Fixed: Handle newlines properly by splitting into lines and joining with proper newlines
|
||||
const originalLines = originalFile.split('\n');
|
||||
const currentLines = currentFile.split('\n');
|
||||
|
||||
// Get the changed lines from current file
|
||||
const changedLines = currentLines.slice(diff.range.start.line, diff.range.end.line + 1);
|
||||
|
||||
// Create new original file content by replacing the affected lines
|
||||
const newOriginalLines = [
|
||||
...originalLines.slice(0, diff.originalRange.start.line),
|
||||
...changedLines,
|
||||
...originalLines.slice(diff.originalRange.end.line + 1)
|
||||
];
|
||||
|
||||
this._originalFileOfDocument[docUriStr] = newOriginalLines.join('\n');
|
||||
|
||||
// Update diff areas based on the change
|
||||
this.refreshDiffAreasModel(docUriStr, [{
|
||||
text: changedLines.join('\n'),
|
||||
startLine: diff.originalRange.start.line,
|
||||
endLine: diff.originalRange.end.line
|
||||
}], 'originalFile')
|
||||
|
||||
// Check if diffArea should be removed
|
||||
|
||||
const diffArea = this._diffAreasOfDocument[docUriStr][diffareaIdx]
|
||||
|
||||
const currentArea = currentLines.slice(diffArea.startLine, diffArea.endLine + 1).join('\n')
|
||||
const originalArea = newOriginalLines.slice(diffArea.originalStartLine, diffArea.originalEndLine + 1).join('\n')
|
||||
|
||||
console.log('ACCEPT change', changedLines.join('\n'), diff.originalRange.start.line, diff.originalRange.end.line)
|
||||
console.log('ACCEPT area lines', diffArea.startLine, diffArea.endLine, diffArea.originalStartLine, diffArea.originalEndLine)
|
||||
console.log('ACCEPT currentArea', currentArea)
|
||||
console.log('ACCEPT originalArea', originalArea)
|
||||
|
||||
if (originalArea === currentArea) {
|
||||
const index = this._diffAreasOfDocument[docUriStr].findIndex(da => da.diffareaid === diffArea.diffareaid)
|
||||
this._diffAreasOfDocument[docUriStr].splice(index, 1)
|
||||
}
|
||||
|
||||
this.refreshStylesAndDiffs(docUriStr)
|
||||
}
|
||||
|
||||
// called on void.rejectDiff
|
||||
public async rejectDiff({ diffid, diffareaid }: { diffid: number, diffareaid: number }) {
|
||||
const editor = vscode.window.activeTextEditor
|
||||
if (!editor)
|
||||
return
|
||||
|
||||
const docUriStr = editor.document.uri.toString()
|
||||
|
||||
const diffIdx = this._diffsOfDocument[docUriStr].findIndex(diff => diff.diffid === diffid);
|
||||
if (diffIdx === -1) { console.error('Error: DiffID could not be found: ', diffid, diffareaid, this._diffsOfDocument[docUriStr], this._diffAreasOfDocument[docUriStr]); return; }
|
||||
|
||||
const diffareaIdx = this._diffAreasOfDocument[docUriStr].findIndex(diff => diff.diffareaid === diffareaid);
|
||||
if (diffareaIdx === -1) { console.error('Error: DiffAreaID could not be found: ', diffid, diffareaid, this._diffsOfDocument[docUriStr], this._diffAreasOfDocument[docUriStr]); return; }
|
||||
|
||||
const diff = this._diffsOfDocument[docUriStr][diffIdx]
|
||||
|
||||
// Apply the rejection by replacing with original code
|
||||
// we don't have to edit the original or final file; just do a workspace edit so the code equals the original code
|
||||
const workspaceEdit = new vscode.WorkspaceEdit();
|
||||
workspaceEdit.replace(editor.document.uri, diff.range, diff.originalCode)
|
||||
await vscode.workspace.applyEdit(workspaceEdit)
|
||||
|
||||
// Check if diffArea should be removed
|
||||
const originalFile = this._originalFileOfDocument[docUriStr]
|
||||
const currentFile = await readFileContentOfUri(editor.document.uri)
|
||||
const diffArea = this._diffAreasOfDocument[docUriStr][diffareaIdx]
|
||||
const currentLines = currentFile.split('\n');
|
||||
const originalLines = originalFile.split('\n');
|
||||
|
||||
const currentArea = currentLines.slice(diffArea.startLine, diffArea.endLine + 1).join('\n')
|
||||
const originalArea = originalLines.slice(diffArea.originalStartLine, diffArea.originalEndLine + 1).join('\n')
|
||||
|
||||
console.log('REJECT diff lines', diff.originalRange.start.line, diff.originalRange.end.line)
|
||||
console.log('REJECT area lines', diffArea.startLine, diffArea.endLine, diffArea.originalStartLine, diffArea.originalEndLine)
|
||||
console.log('REJECT currentArea', currentArea)
|
||||
console.log('REJECT originalArea', originalArea)
|
||||
|
||||
if (originalArea === currentArea) {
|
||||
const index = this._diffAreasOfDocument[docUriStr].findIndex(da => da.diffareaid === diffArea.diffareaid)
|
||||
this._diffAreasOfDocument[docUriStr].splice(index, 1)
|
||||
}
|
||||
|
||||
this.refreshStylesAndDiffs(docUriStr)
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
// used by us only
|
||||
public updateStream = throttle(async (docUriStr: string, diffArea: DiffArea, newDiffAreaCode: string) => {
|
||||
|
||||
const editor = vscode.window.activeTextEditor // TODO the editor should be that of `docUri` and not necessarily the current editor
|
||||
if (!editor) {
|
||||
console.log('Error: No active editor!')
|
||||
return;
|
||||
}
|
||||
|
||||
// original code all diffs are based on in the code
|
||||
const originalDiffAreaCode = (this._originalFileOfDocument[docUriStr] || '').split('\n').slice(diffArea.originalStartLine, diffArea.originalEndLine + 1).join('\n')
|
||||
|
||||
// figure out where to highlight based on where the AI is in the stream right now, use the last diff in findDiffs to figure that out
|
||||
const diffs = findDiffs(originalDiffAreaCode, newDiffAreaCode)
|
||||
const lastDiff = diffs[diffs.length - 1] ?? null
|
||||
|
||||
// these are two different coordinate systems - new and old line number
|
||||
let newFileEndLine: number // get new[0...newStoppingPoint] with line=newStoppingPoint highlighted
|
||||
let oldFileStartLine: number // get original[oldStartingPoint...]
|
||||
|
||||
if (!lastDiff) {
|
||||
// if the writing is identical so far, display no changes
|
||||
newFileEndLine = 0
|
||||
oldFileStartLine = 0
|
||||
}
|
||||
else {
|
||||
if (lastDiff.type === 'insertion') {
|
||||
newFileEndLine = lastDiff.range.end.line
|
||||
oldFileStartLine = lastDiff.originalRange.start.line
|
||||
}
|
||||
else if (lastDiff.type === 'deletion') {
|
||||
newFileEndLine = lastDiff.range.start.line
|
||||
oldFileStartLine = lastDiff.originalRange.start.line
|
||||
}
|
||||
else if (lastDiff.type === 'edit') {
|
||||
newFileEndLine = lastDiff.range.end.line
|
||||
oldFileStartLine = lastDiff.originalRange.start.line
|
||||
}
|
||||
else {
|
||||
throw new Error(`updateStream: diff.type not recognized: ${lastDiff.type}`)
|
||||
}
|
||||
}
|
||||
|
||||
// display
|
||||
const newFileTop = newDiffAreaCode.split('\n').slice(0, newFileEndLine + 1).join('\n')
|
||||
const oldFileBottom = originalDiffAreaCode.split('\n').slice(oldFileStartLine + 1, Infinity).join('\n')
|
||||
|
||||
let newCode = `${newFileTop}\n${oldFileBottom}`
|
||||
diffArea.sweepIndex = newFileEndLine
|
||||
// replace oldDACode with newDACode with a vscode edit
|
||||
|
||||
const workspaceEdit = new vscode.WorkspaceEdit();
|
||||
|
||||
const diffareaRange = new vscode.Range(diffArea.startLine, 0, diffArea.endLine, Number.MAX_SAFE_INTEGER)
|
||||
workspaceEdit.replace(editor.document.uri, diffareaRange, newCode)
|
||||
await vscode.workspace.applyEdit(workspaceEdit)
|
||||
}, THROTTLE_TIME)
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/*
|
||||
import * as vscode from 'vscode';
|
||||
import { SuggestedEdit } from './findDiffs';
|
||||
|
||||
const greenDecoration = vscode.window.createTextEditorDecorationType({
|
||||
backgroundColor: 'rgba(0 255 51 / 0.2)',
|
||||
isWholeLine: false, // after: { contentText: ' [original]', color: 'rgba(0 255 60 / 0.5)' } // hoverMessage: originalText // this applies to hovering over after:...
|
||||
})
|
||||
|
||||
|
||||
|
||||
|
||||
export class DiffProvider {
|
||||
|
||||
originalCodeOfDocument: { [docUri: string]: string }
|
||||
|
||||
diffsOfDocument: {
|
||||
[docUri: string]: {
|
||||
startLine,
|
||||
startCol,
|
||||
endLine,
|
||||
endCol,
|
||||
originalText,
|
||||
|
||||
inset,
|
||||
diffid,
|
||||
}
|
||||
}
|
||||
|
||||
// sweep
|
||||
currentLine: { [docUri: string]: undefined | number }
|
||||
weAreEditing: boolean = false
|
||||
|
||||
|
||||
constructor() {
|
||||
|
||||
vscode.workspace.onDidChangeTextDocument((e) => {
|
||||
// on user change, grow/shrink/merge/delete diff AREAS
|
||||
// you dont have to do anything to the diffs here bc they all get recomputed in refresh()
|
||||
// user changes only get highlighted if theyre in a diffarea
|
||||
|
||||
// go thru all diff areas and adjust line numbers based on the user's change
|
||||
|
||||
|
||||
this.refreshStyles(e.document.uri.toString())
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
// refreshes styles on page
|
||||
refreshStyles(docUriStr: string) {
|
||||
|
||||
if (this.weAreEditing) return
|
||||
|
||||
// recompute all diffs on the page
|
||||
// run inset.dispose() on all diffs
|
||||
|
||||
// original and current code -> diffs
|
||||
// originalCodeOfDocument[docUriStr]
|
||||
|
||||
// create new diffs
|
||||
const inset = vscode.window.createWebviewTextEditorInset(editor, lineStart, height, {})
|
||||
inset.webview.html = `
|
||||
<html>
|
||||
<body style="pointer-events:none;">Hello World!</body>
|
||||
</html>
|
||||
`;
|
||||
|
||||
}
|
||||
|
||||
// called on void.acceptDiff
|
||||
public async acceptDiff({ diffid }: { diffid: number }) {
|
||||
|
||||
// update original based on the diff
|
||||
// refresh()
|
||||
|
||||
}
|
||||
|
||||
|
||||
// called on void.rejectDiff
|
||||
public async rejectDiff({ diffid }: { diffid: number }) {
|
||||
|
||||
// get diffs[diffid]
|
||||
|
||||
// revert current file based on diff
|
||||
// refresh()
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
// sweep
|
||||
initializeSweep({ startLine }) {
|
||||
// reject all diffs on the page
|
||||
// store original code
|
||||
// currentLine=start of sweep
|
||||
}
|
||||
|
||||
onUpdateSweep(addedText) {
|
||||
// update final
|
||||
// refresh() ?
|
||||
// currentLine += number of newlines in addedText
|
||||
}
|
||||
|
||||
onAbortSweep() {
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
*/
|
||||
@@ -1,85 +0,0 @@
|
||||
// renders the code from `src/sidebar`
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
|
||||
function generateNonce() {
|
||||
let text = "";
|
||||
const possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
||||
for (let i = 0; i < 32; i++) {
|
||||
text += possible.charAt(Math.floor(Math.random() * possible.length));
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
export class SidebarWebviewProvider implements vscode.WebviewViewProvider {
|
||||
public static readonly viewId = 'void.viewnumberone';
|
||||
|
||||
public webview: Promise<vscode.Webview> // used to send messages to the webview, resolved by _res in resolveWebviewView
|
||||
private _res: (c: vscode.Webview) => void // used to resolve the webview
|
||||
|
||||
private readonly _extensionUri: vscode.Uri
|
||||
|
||||
// private _webviewView?: vscode.WebviewView;
|
||||
private _webviewDeps: string[] = [];
|
||||
|
||||
constructor(context: vscode.ExtensionContext) {
|
||||
// const extensionPath = context.extensionPath // the directory where the extension is installed, might be useful later... was included in webviewProvider code
|
||||
this._extensionUri = context.extensionUri
|
||||
|
||||
let temp_res: typeof this._res | undefined = undefined
|
||||
this.webview = new Promise((res, rej) => { temp_res = res })
|
||||
if (!temp_res) throw new Error("Void sidebar provider: resolver was undefined")
|
||||
this._res = temp_res
|
||||
}
|
||||
|
||||
// called by us
|
||||
updateWebviewHTML(webview: vscode.Webview) {
|
||||
this._webviewDeps = []
|
||||
|
||||
const scriptUri = webview.asWebviewUri(vscode.Uri.joinPath(this._extensionUri, 'dist/sidebar/index.js'));
|
||||
const stylesUri = webview.asWebviewUri(vscode.Uri.joinPath(this._extensionUri, 'dist/sidebar/styles.css'));
|
||||
const rootUri = webview.asWebviewUri(vscode.Uri.joinPath(this._extensionUri));
|
||||
const nonce = generateNonce();
|
||||
|
||||
const webviewHTML = `<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Custom View</title>
|
||||
<meta http-equiv="Content-Security-Policy" content="img-src vscode-resource: https:; script-src 'nonce-${nonce}'; style-src vscode-resource: 'unsafe-inline' http: https: data:;">
|
||||
<base href="${rootUri}/">
|
||||
<link href="${stylesUri}" rel="stylesheet">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<div id="ctrlkroot"></div>
|
||||
<script nonce="${nonce}" src="${scriptUri}"></script>
|
||||
</body>
|
||||
</html>`;
|
||||
|
||||
webview.html = webviewHTML;
|
||||
}
|
||||
|
||||
|
||||
// called internally by vscode
|
||||
resolveWebviewView(
|
||||
webviewView: vscode.WebviewView,
|
||||
context: vscode.WebviewViewResolveContext,
|
||||
token: vscode.CancellationToken,
|
||||
) {
|
||||
|
||||
const webview = webviewView.webview;
|
||||
|
||||
webview.options = {
|
||||
enableScripts: true,
|
||||
localResourceRoots: [this._extensionUri]
|
||||
};
|
||||
|
||||
this.updateWebviewHTML(webview);
|
||||
|
||||
// resolve webview and _webviewView
|
||||
this._res(webview);
|
||||
// this._webviewView = webviewView;
|
||||
}
|
||||
}
|
||||
@@ -1,101 +0,0 @@
|
||||
import * as vscode from 'vscode';
|
||||
import { AbortRef, OnFinalMessage, OnText, sendLLMMessage } from "./sendLLMMessage"
|
||||
import { VoidConfig } from '../sidebar/contextForConfig';
|
||||
import { findDiffs } from '../findDiffs';
|
||||
import { searchDiffChunkInstructions, writeFileWithDiffInstructions } from './systemPrompts';
|
||||
import { throttle } from 'lodash';
|
||||
import { readFileContentOfUri } from './readFileContentOfUri';
|
||||
|
||||
type Res<T> = ((value: T) => void)
|
||||
|
||||
const THRTOTLE_TIME = 100 // minimum time between edits
|
||||
const LINES_PER_CHUNK = 20 // number of lines to search at a time
|
||||
|
||||
const applyCtrlLChangesToFile = throttle(
|
||||
({ fileUri, newCurrentLine, oldCurrentLine, fullCompletedStr, oldFileStr, debug }: { fileUri: vscode.Uri, newCurrentLine: number, oldCurrentLine: number, fullCompletedStr: string, oldFileStr: string, debug?: string }) => {
|
||||
|
||||
// write the change to the file
|
||||
const WRITE_TO_FILE = (
|
||||
fullCompletedStr.split('\n').slice(0, newCurrentLine + 1).join('\n') // newFile[:newCurrentLine+1]
|
||||
+ oldFileStr.split('\n').slice(oldCurrentLine + 1).join('\n') // oldFile[oldCurrentLine+1:]
|
||||
)
|
||||
const workspaceEdit = new vscode.WorkspaceEdit()
|
||||
workspaceEdit.replace(fileUri, new vscode.Range(0, 0, Number.MAX_SAFE_INTEGER, 0), WRITE_TO_FILE)
|
||||
vscode.workspace.applyEdit(workspaceEdit)
|
||||
|
||||
// highlight the `newCurrentLine` in white
|
||||
// highlight the remaining part of the file in gray
|
||||
|
||||
},
|
||||
THRTOTLE_TIME, { trailing: true }
|
||||
)
|
||||
|
||||
|
||||
const applyCtrlK = async ({ fileUri, startLine, endLine, instructions, voidConfig, abortRef }: { fileUri: vscode.Uri, startLine: number, endLine: number, instructions: string, voidConfig: VoidConfig, abortRef: AbortRef }) => {
|
||||
|
||||
const fileStr = await readFileContentOfUri(fileUri)
|
||||
const fileLines = fileStr.split('\n')
|
||||
|
||||
const prefix = fileLines.slice(startLine).join('\n')
|
||||
const suffix = fileLines.slice(endLine + 1).join('\n')
|
||||
const selection = fileLines.slice(startLine, endLine + 1).join('\n')
|
||||
|
||||
const promptContent = `Here is the user's original selection:
|
||||
\`\`\`
|
||||
<MID>${selection}</MID>
|
||||
\`\`\`
|
||||
|
||||
The user wants to apply the following instructions to the selection:
|
||||
${instructions}
|
||||
|
||||
Please rewrite the selection following the user's instructions.
|
||||
|
||||
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
|
||||
|
||||
Complete the following:
|
||||
\`\`\`
|
||||
<PRE>${prefix}</PRE>
|
||||
<SUF>${suffix}</SUF>
|
||||
<MID>`;
|
||||
|
||||
|
||||
// TODO initialize stream
|
||||
|
||||
// update stream
|
||||
sendLLMMessage({
|
||||
messages: [{ role: 'user', content: promptContent, }],
|
||||
onText: async (tokenStr, completionStr) => {
|
||||
// TODO update stream
|
||||
|
||||
|
||||
// apply the changes
|
||||
const newCode = `${prefix}\n${completionStr}\n${suffix}`
|
||||
const workspaceEdit = new vscode.WorkspaceEdit()
|
||||
workspaceEdit.replace(fileUri, new vscode.Range(0, 0, Number.MAX_SAFE_INTEGER, 0), newCode)
|
||||
vscode.workspace.applyEdit(workspaceEdit)
|
||||
},
|
||||
onFinalMessage: (completionStr) => {
|
||||
// TODO end stream
|
||||
|
||||
// apply the changes
|
||||
const newCode = `${prefix}\n${completionStr}\n${suffix}`
|
||||
const workspaceEdit = new vscode.WorkspaceEdit()
|
||||
workspaceEdit.replace(fileUri, new vscode.Range(0, 0, Number.MAX_SAFE_INTEGER, 0), newCode)
|
||||
vscode.workspace.applyEdit(workspaceEdit)
|
||||
},
|
||||
onError: (e) => {
|
||||
console.error('Error rewriting file with diff', e);
|
||||
},
|
||||
voidConfig,
|
||||
abortRef,
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
export { applyCtrlK }
|
||||
@@ -1,191 +0,0 @@
|
||||
import * as vscode from 'vscode';
|
||||
import { AbortRef, OnFinalMessage, OnText, sendLLMMessage } from "./sendLLMMessage"
|
||||
import { VoidConfig } from '../sidebar/contextForConfig';
|
||||
import { findDiffs } from '../findDiffs';
|
||||
import { searchDiffChunkInstructions, writeFileWithDiffInstructions } from './systemPrompts';
|
||||
import { throttle } from 'lodash';
|
||||
import { readFileContentOfUri } from './readFileContentOfUri';
|
||||
import { DiffProvider } from '../DiffProvider';
|
||||
import { DiffArea } from './shared_types';
|
||||
|
||||
const LINES_PER_CHUNK = 20 // number of lines to search at a time
|
||||
|
||||
// const THRTOTLE_TIME = 100 // minimum time between edits
|
||||
// throttle(
|
||||
// THRTOTLE_TIME, { trailing: true }
|
||||
// )
|
||||
|
||||
type CompetedReturn = { isFinished: true, } | { isFinished?: undefined, }
|
||||
const streamChunk = ({ diffProvider, docUri, oldFileStr, completedStr, diffRepr, diffArea, voidConfig, abortRef }: { diffProvider: DiffProvider, docUri: vscode.Uri, oldFileStr: string, completedStr: string, diffRepr: string, voidConfig: VoidConfig, diffArea: DiffArea, abortRef: AbortRef }) => {
|
||||
|
||||
const NUM_MATCHUP_TOKENS = 20
|
||||
|
||||
const promptContent = `ORIGINAL_FILE
|
||||
\`\`\`
|
||||
${oldFileStr}
|
||||
\`\`\`
|
||||
|
||||
DIFF
|
||||
\`\`\`
|
||||
${diffRepr}
|
||||
\`\`\`
|
||||
|
||||
INSTRUCTIONS
|
||||
Please finish writing the new file \`NEW_FILE\`. Return ONLY the completion of the file, without any explanation.
|
||||
|
||||
NEW_FILE
|
||||
\`\`\`
|
||||
${completedStr}
|
||||
\`\`\`
|
||||
`
|
||||
// create a promise that can be awaited
|
||||
return new Promise<CompetedReturn>((resolve, reject) => {
|
||||
|
||||
let isAnyChangeSoFar = false
|
||||
|
||||
// make LLM complete the file to include the diff
|
||||
sendLLMMessage({
|
||||
messages: [{ role: 'system', content: writeFileWithDiffInstructions, }, { role: 'user', content: promptContent, }],
|
||||
onText: (newText, fullText) => {
|
||||
const fullCompletedStr = completedStr + fullText
|
||||
|
||||
diffProvider.updateStream(docUri.toString(), diffArea, fullCompletedStr)
|
||||
|
||||
// if there was any change from the original file
|
||||
if (!oldFileStr.includes(fullCompletedStr)) {
|
||||
isAnyChangeSoFar = true
|
||||
}
|
||||
|
||||
|
||||
const isRecentMatchup = false
|
||||
// the final NUM_MATCHUP_TOKENS characters of fullCompletedStr are the same as the final NUM_MATCHUP_TOKENS characters of the last item in the diffs of oldFileStr that had 0 changes
|
||||
|
||||
if (isAnyChangeSoFar && isRecentMatchup) {
|
||||
diffProvider.updateStream(docUri.toString(), diffArea, fullCompletedStr)
|
||||
|
||||
// TODO resolve the promise
|
||||
// resolve({ speculativeIndex: newCurrentLine + 1 });
|
||||
|
||||
// abort the LLM call
|
||||
abortRef.current?.()
|
||||
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
onFinalMessage: (fullText) => {
|
||||
const newCompletedStr = completedStr + fullText
|
||||
diffProvider.updateStream(docUri.toString(), diffArea, newCompletedStr)
|
||||
resolve({ isFinished: true });
|
||||
},
|
||||
onError: (e) => {
|
||||
resolve({ isFinished: true });
|
||||
console.error('Error rewriting file with diff', e);
|
||||
},
|
||||
voidConfig,
|
||||
abortRef,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
const shouldApplyDiff = ({ diffRepr, oldFileStr: fileStr, speculationStr, voidConfig, abortRef }: { diffRepr: string, oldFileStr: string, speculationStr: string, voidConfig: VoidConfig, abortRef: AbortRef }) => {
|
||||
|
||||
const promptContent = `DIFF
|
||||
\`\`\`
|
||||
${diffRepr}
|
||||
\`\`\`
|
||||
|
||||
FILES
|
||||
\`\`\`
|
||||
${fileStr}
|
||||
\`\`\`
|
||||
|
||||
SELECTION
|
||||
\`\`\`
|
||||
${speculationStr}
|
||||
\`\`\`
|
||||
|
||||
Return \`true\` if ANY part of the chunk should be modified, and \`false\` if it should not be modified. You should respond only with \`true\` or \`false\` and nothing else.
|
||||
`
|
||||
|
||||
// create new promise
|
||||
return new Promise<boolean>((resolve, reject) => {
|
||||
// send message to LLM
|
||||
sendLLMMessage({
|
||||
messages: [{ role: 'system', content: searchDiffChunkInstructions, }, { role: 'user', content: promptContent, }],
|
||||
onFinalMessage: (finalMessage) => {
|
||||
|
||||
const containsTrue = finalMessage
|
||||
.slice(-10) // check for `true` in last 10 characters
|
||||
.toLowerCase()
|
||||
.includes('true')
|
||||
|
||||
resolve(containsTrue)
|
||||
},
|
||||
onError: (e) => {
|
||||
resolve(false);
|
||||
console.error('Error in shouldApplyDiff: ', e)
|
||||
},
|
||||
onText: () => { },
|
||||
voidConfig,
|
||||
abortRef,
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
// lazily applies the diff to the file
|
||||
// we chunk the text in the file, and ask an LLM whether it should edit each chunk
|
||||
const applyDiffLazily = async ({ docUri, oldFileStr, voidConfig, abortRef, diffRepr, diffProvider, diffArea }: { docUri: vscode.Uri, oldFileStr: string, diffRepr: string, voidConfig: VoidConfig, diffProvider: DiffProvider, diffArea: DiffArea, abortRef: AbortRef }) => {
|
||||
|
||||
|
||||
// stateful variables
|
||||
let speculativeIndex = 0
|
||||
let writtenTextSoFar: string[] = []
|
||||
|
||||
while (speculativeIndex < oldFileStr.split('\n').length) {
|
||||
|
||||
const chunkStr = oldFileStr.split('\n').slice(speculativeIndex, speculativeIndex + LINES_PER_CHUNK).join('\n')
|
||||
|
||||
// ask LLM if we should apply the diff to the chunk
|
||||
const START = new Date().getTime()
|
||||
let shouldApplyDiff_ = await shouldApplyDiff({ oldFileStr, speculationStr: chunkStr, diffRepr, voidConfig, abortRef })
|
||||
const END = new Date().getTime()
|
||||
|
||||
// if should not change the chunk
|
||||
if (!shouldApplyDiff_) {
|
||||
console.log('KEEP CHUNK time: ', END - START)
|
||||
speculativeIndex += LINES_PER_CHUNK
|
||||
writtenTextSoFar.push(chunkStr)
|
||||
diffProvider.updateStream(docUri.toString(), diffArea, writtenTextSoFar.join('\n'))
|
||||
continue;
|
||||
}
|
||||
|
||||
// ask LLM to rewrite file with diff (if there is significant matchup with the original file, we stop rewriting)
|
||||
const START2 = new Date().getTime()
|
||||
const completedStr = (await readFileContentOfUri(docUri)).split('\n').slice(0, speculativeIndex).join('\n');
|
||||
const result = await streamChunk({ diffProvider, docUri, oldFileStr, completedStr, diffRepr, voidConfig, diffArea, abortRef, })
|
||||
const END2 = new Date().getTime()
|
||||
|
||||
console.log('EDIT CHUNK time: ', END2 - START2);
|
||||
|
||||
// if we are finished, stop the loop
|
||||
if (result.isFinished) {
|
||||
break;
|
||||
}
|
||||
|
||||
// TODO
|
||||
// speculativeIndex = result.speculativeIndex
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
export { applyDiffLazily }
|
||||
@@ -1,14 +0,0 @@
|
||||
import * as vscode from 'vscode'
|
||||
|
||||
|
||||
export const readFileContentOfUri = async (uri: vscode.Uri): Promise<string> => {
|
||||
const document = await vscode.workspace.openTextDocument(uri);
|
||||
return document.getText().replace(/\r\n/g, '\n') ?? '' // Normalize line endings
|
||||
|
||||
};
|
||||
|
||||
// this is the old version, which only reads the most recently saved version
|
||||
// export const readFileContentOfUri = async (uri: vscode.Uri) => {
|
||||
// return Buffer.from(await vscode.workspace.fs.readFile(uri)).toString('utf8')
|
||||
// .replace(/\r\n/g, '\n') // replace windows \r\n with \n
|
||||
// }
|
||||
@@ -1,307 +0,0 @@
|
||||
import Anthropic from '@anthropic-ai/sdk';
|
||||
import OpenAI from 'openai';
|
||||
import { Ollama } from 'ollama/browser'
|
||||
import { VoidConfig } from '../sidebar/contextForConfig';
|
||||
|
||||
export type AbortRef = { current: (() => void) | null }
|
||||
|
||||
export type OnText = (newText: string, fullText: string) => void
|
||||
|
||||
export type OnFinalMessage = (input: string) => void
|
||||
|
||||
export type LLMMessageAnthropic = {
|
||||
role: 'user' | 'assistant',
|
||||
content: string,
|
||||
}
|
||||
|
||||
export type LLMMessage = {
|
||||
role: 'system' | 'user' | 'assistant',
|
||||
content: string,
|
||||
}
|
||||
|
||||
type SendLLMMessageFnTypeInternal = (params: {
|
||||
messages: LLMMessage[],
|
||||
onText: OnText,
|
||||
onFinalMessage: OnFinalMessage,
|
||||
onError: (error: string) => void,
|
||||
voidConfig: VoidConfig,
|
||||
abortRef: AbortRef,
|
||||
}) => void
|
||||
|
||||
type SendLLMMessageFnTypeExternal = (params: {
|
||||
messages: LLMMessage[],
|
||||
onText: OnText,
|
||||
onFinalMessage: (fullText: string) => void,
|
||||
onError: (error: string) => void,
|
||||
voidConfig: VoidConfig | null,
|
||||
abortRef: AbortRef,
|
||||
}) => void
|
||||
|
||||
|
||||
|
||||
|
||||
// Anthropic
|
||||
const sendAnthropicMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinalMessage, onError, voidConfig }) => {
|
||||
|
||||
const anthropic = new Anthropic({ apiKey: voidConfig.anthropic.apikey, dangerouslyAllowBrowser: true }); // defaults to process.env["ANTHROPIC_API_KEY"]
|
||||
|
||||
// find system messages and concatenate them
|
||||
const systemMessage = messages
|
||||
.filter(msg => msg.role === 'system')
|
||||
.map(msg => msg.content)
|
||||
.join('\n');
|
||||
|
||||
// remove system messages for Anthropic
|
||||
const anthropicMessages = messages.filter(msg => msg.role !== 'system') as LLMMessageAnthropic[]
|
||||
|
||||
const stream = anthropic.messages.stream({
|
||||
system: systemMessage,
|
||||
messages: anthropicMessages,
|
||||
model: voidConfig.anthropic.model,
|
||||
max_tokens: parseInt(voidConfig.default.maxTokens),
|
||||
});
|
||||
|
||||
let did_abort = false
|
||||
|
||||
// when receive text
|
||||
stream.on('text', (newText, fullText) => {
|
||||
if (did_abort) return
|
||||
onText(newText, fullText)
|
||||
})
|
||||
|
||||
// when we get the final message on this stream (or when error/fail)
|
||||
stream.on('finalMessage', (claude_response) => {
|
||||
if (did_abort) return
|
||||
// stringify the response's content
|
||||
let content = claude_response.content.map(c => { if (c.type === 'text') { return c.text } }).join('\n');
|
||||
onFinalMessage(content)
|
||||
})
|
||||
|
||||
stream.on('error', (error) => {
|
||||
// the most common error will be invalid API key (401), so we handle this with a nice message
|
||||
if (error instanceof Anthropic.APIError && error.status === 401) {
|
||||
onError('Invalid API key.')
|
||||
}
|
||||
else {
|
||||
onError(error.message)
|
||||
}
|
||||
})
|
||||
|
||||
// if abort is called, onFinalMessage is NOT called, and no later onTexts are called either
|
||||
const abort = () => {
|
||||
did_abort = true
|
||||
stream.controller.abort() // TODO need to test this to make sure it works, it might throw an error
|
||||
}
|
||||
|
||||
return { abort }
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
// OpenAI, OpenRouter, OpenAICompatible
|
||||
const sendOpenAIMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinalMessage, onError, voidConfig, abortRef }) => {
|
||||
|
||||
let didAbort = false
|
||||
let fullText = ''
|
||||
|
||||
// if abort is called, onFinalMessage is NOT called, and no later onTexts are called either
|
||||
abortRef.current = () => {
|
||||
didAbort = true;
|
||||
};
|
||||
|
||||
let openai: OpenAI
|
||||
let options: OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming
|
||||
|
||||
if (voidConfig.default.whichApi === 'openAI') {
|
||||
openai = new OpenAI({ apiKey: voidConfig.openAI.apikey, dangerouslyAllowBrowser: true });
|
||||
options = { model: voidConfig.openAI.model, messages: messages, stream: true, max_completion_tokens: parseInt(voidConfig.default.maxTokens) }
|
||||
}
|
||||
else if (voidConfig.default.whichApi === 'openRouter') {
|
||||
openai = new OpenAI({
|
||||
baseURL: "https://openrouter.ai/api/v1", apiKey: voidConfig.openRouter.apikey, dangerouslyAllowBrowser: true,
|
||||
defaultHeaders: {
|
||||
"HTTP-Referer": 'https://voideditor.com', // Optional, for including your app on openrouter.ai rankings.
|
||||
"X-Title": 'Void Editor', // Optional. Shows in rankings on openrouter.ai.
|
||||
},
|
||||
});
|
||||
options = { model: voidConfig.openRouter.model, messages: messages, stream: true, max_completion_tokens: parseInt(voidConfig.default.maxTokens) }
|
||||
}
|
||||
else if (voidConfig.default.whichApi === 'openAICompatible') {
|
||||
openai = new OpenAI({ baseURL: voidConfig.openAICompatible.endpoint, apiKey: voidConfig.openAICompatible.apikey, dangerouslyAllowBrowser: true })
|
||||
options = { model: voidConfig.openAICompatible.model, messages: messages, stream: true, max_completion_tokens: parseInt(voidConfig.default.maxTokens) }
|
||||
}
|
||||
else {
|
||||
console.error(`sendOpenAIMsg: invalid whichApi: ${voidConfig.default.whichApi}`)
|
||||
throw new Error(`voidConfig.whichAPI was invalid: ${voidConfig.default.whichApi}`)
|
||||
}
|
||||
|
||||
openai.chat.completions
|
||||
.create(options)
|
||||
.then(async response => {
|
||||
abortRef.current = () => {
|
||||
// response.controller.abort()
|
||||
didAbort = true;
|
||||
}
|
||||
// when receive text
|
||||
for await (const chunk of response) {
|
||||
if (didAbort) return;
|
||||
const newText = chunk.choices[0]?.delta?.content || '';
|
||||
fullText += newText;
|
||||
onText(newText, fullText);
|
||||
}
|
||||
onFinalMessage(fullText);
|
||||
})
|
||||
// when error/fail - this catches errors of both .create() and .then(for await)
|
||||
.catch(error => {
|
||||
if (error instanceof OpenAI.APIError) {
|
||||
if (error.status === 401) {
|
||||
onError('Invalid API key.');
|
||||
}
|
||||
else {
|
||||
onError(error.message);
|
||||
}
|
||||
}
|
||||
else {
|
||||
onError(error);
|
||||
}
|
||||
})
|
||||
|
||||
};
|
||||
|
||||
|
||||
// Ollama
|
||||
export const sendOllamaMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinalMessage, onError, voidConfig, abortRef }) => {
|
||||
|
||||
let didAbort = false
|
||||
let fullText = ""
|
||||
|
||||
// if abort is called, onFinalMessage is NOT called, and no later onTexts are called either
|
||||
abortRef.current = () => {
|
||||
didAbort = true;
|
||||
};
|
||||
|
||||
const ollama = new Ollama({ host: voidConfig.ollama.endpoint })
|
||||
|
||||
ollama.chat({
|
||||
model: voidConfig.ollama.model,
|
||||
messages: messages,
|
||||
stream: true,
|
||||
options: { num_predict: parseInt(voidConfig.default.maxTokens) } // this is max_tokens
|
||||
})
|
||||
.then(async stream => {
|
||||
abortRef.current = () => {
|
||||
// stream.abort()
|
||||
didAbort = true
|
||||
}
|
||||
// iterate through the stream
|
||||
for await (const chunk of stream) {
|
||||
if (didAbort) return;
|
||||
const newText = chunk.message.content;
|
||||
fullText += newText;
|
||||
onText(newText, fullText);
|
||||
}
|
||||
onFinalMessage(fullText);
|
||||
|
||||
})
|
||||
// when error/fail
|
||||
.catch(error => {
|
||||
onError(error)
|
||||
})
|
||||
|
||||
};
|
||||
|
||||
|
||||
|
||||
// Greptile
|
||||
// https://docs.greptile.com/api-reference/query
|
||||
// https://docs.greptile.com/quickstart#sample-response-streamed
|
||||
|
||||
const sendGreptileMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinalMessage, onError, voidConfig, abortRef }) => {
|
||||
|
||||
let didAbort = false
|
||||
let fullText = ''
|
||||
|
||||
// if abort is called, onFinalMessage is NOT called, and no later onTexts are called either
|
||||
abortRef.current = () => {
|
||||
didAbort = true
|
||||
}
|
||||
|
||||
|
||||
fetch('https://api.greptile.com/v2/query', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
"Authorization": `Bearer ${voidConfig.greptile.apikey}`,
|
||||
"X-Github-Token": `${voidConfig.greptile.githubPAT}`,
|
||||
"Content-Type": `application/json`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
messages,
|
||||
stream: true,
|
||||
repositories: [voidConfig.greptile.repoinfo],
|
||||
}),
|
||||
})
|
||||
// this is {message}\n{message}\n{message}...\n
|
||||
.then(async response => {
|
||||
const text = await response.text()
|
||||
console.log('got greptile', text)
|
||||
return JSON.parse(`[${text.trim().split('\n').join(',')}]`)
|
||||
})
|
||||
// TODO make this actually stream, right now it just sends one message at the end
|
||||
.then(async responseArr => {
|
||||
if (didAbort)
|
||||
return
|
||||
|
||||
for (let response of responseArr) {
|
||||
|
||||
const type: string = response['type']
|
||||
const message = response['message']
|
||||
|
||||
// when receive text
|
||||
if (type === 'message') {
|
||||
fullText += message
|
||||
onText(message, fullText)
|
||||
}
|
||||
else if (type === 'sources') {
|
||||
const { filepath, linestart, lineend } = message as { filepath: string, linestart: number | null, lineend: number | null }
|
||||
fullText += filepath
|
||||
onText(filepath, fullText)
|
||||
}
|
||||
// type: 'status' with an empty 'message' means last message
|
||||
else if (type === 'status') {
|
||||
if (!message) {
|
||||
onFinalMessage(fullText)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
})
|
||||
.catch(e => {
|
||||
onError(e)
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
export const sendLLMMessage: SendLLMMessageFnTypeExternal = ({ messages, onText, onFinalMessage, onError, voidConfig, abortRef }) => {
|
||||
if (!voidConfig) return;
|
||||
|
||||
// trim message content (Anthropic and other providers give an error if there is trailing whitespace)
|
||||
messages = messages.map(m => ({ ...m, content: m.content.trim() }))
|
||||
|
||||
switch (voidConfig.default.whichApi) {
|
||||
case 'anthropic':
|
||||
return sendAnthropicMsg({ messages, onText, onFinalMessage, onError, voidConfig, abortRef });
|
||||
case 'openAI':
|
||||
case 'openRouter':
|
||||
case 'openAICompatible':
|
||||
return sendOpenAIMsg({ messages, onText, onFinalMessage, onError, voidConfig, abortRef });
|
||||
case 'ollama':
|
||||
return sendOllamaMsg({ messages, onText, onFinalMessage, onError, voidConfig, abortRef });
|
||||
case 'greptile':
|
||||
return sendGreptileMsg({ messages, onText, onFinalMessage, onError, voidConfig, abortRef });
|
||||
default:
|
||||
onError(`Error: whichApi was ${voidConfig.default.whichApi}, which is not recognized!`)
|
||||
}
|
||||
}
|
||||
@@ -1,100 +0,0 @@
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
import { PartialVoidConfig } from '../sidebar/contextForConfig';
|
||||
|
||||
|
||||
|
||||
// a selection is a frozen snapshot
|
||||
type CodeSelection = { selectionStr: string, selectionRange: vscode.Range, filePath: vscode.Uri }
|
||||
|
||||
type File = { filepath: vscode.Uri, content: string }
|
||||
|
||||
// an area that is currently being diffed
|
||||
type DiffArea = {
|
||||
diffareaid: number,
|
||||
startLine: number,
|
||||
endLine: number,
|
||||
originalStartLine: number,
|
||||
originalEndLine: number,
|
||||
sweepIndex: number | null // null iff not sweeping
|
||||
}
|
||||
|
||||
// the return type of diff creator
|
||||
type BaseDiff = {
|
||||
type: 'edit' | 'insertion' | 'deletion';
|
||||
// repr: string; // representation of the diff in text
|
||||
originalRange: vscode.Range;
|
||||
originalCode: string;
|
||||
range: vscode.Range;
|
||||
code: string;
|
||||
}
|
||||
|
||||
// each diff on the user's screen
|
||||
type Diff = {
|
||||
diffid: number,
|
||||
lenses: vscode.CodeLens[],
|
||||
} & BaseDiff
|
||||
|
||||
// editor -> sidebar
|
||||
type MessageToSidebar = (
|
||||
| { type: 'ctrl+l', selection: CodeSelection } // user presses ctrl+l in the editor
|
||||
| { type: 'ctrl+k', selection: CodeSelection }
|
||||
| { type: 'files', files: { filepath: vscode.Uri, content: string }[] }
|
||||
| { type: 'partialVoidConfig', partialVoidConfig: PartialVoidConfig }
|
||||
| { type: 'allThreads', threads: ChatThreads }
|
||||
| { type: 'startNewThread' }
|
||||
| { type: 'toggleThreadSelector' }
|
||||
| { type: 'toggleSettings' }
|
||||
| { type: 'deviceId', deviceId: string }
|
||||
)
|
||||
|
||||
// sidebar -> editor
|
||||
type MessageFromSidebar = (
|
||||
| { type: 'applyChanges', diffRepr: string } // user clicks "apply" in the sidebar
|
||||
| { type: 'requestFiles', filepaths: vscode.Uri[] }
|
||||
| { type: 'getPartialVoidConfig' }
|
||||
| { type: 'persistPartialVoidConfig', partialVoidConfig: PartialVoidConfig }
|
||||
| { type: 'getAllThreads' }
|
||||
| { type: 'persistThread', thread: ChatThreads[string] }
|
||||
| { type: 'getDeviceId' }
|
||||
)
|
||||
|
||||
|
||||
type ChatThreads = {
|
||||
[id: string]: {
|
||||
id: string; // store the id here too
|
||||
createdAt: string; // ISO string
|
||||
lastModified: string; // ISO string
|
||||
messages: ChatMessage[];
|
||||
}
|
||||
}
|
||||
|
||||
type ChatMessage =
|
||||
| {
|
||||
role: "user";
|
||||
content: string; // content sent to the llm
|
||||
displayContent: string; // content displayed to user
|
||||
selection: CodeSelection | null; // the user's selection
|
||||
files: vscode.Uri[]; // the files sent in the message
|
||||
}
|
||||
| {
|
||||
role: "assistant";
|
||||
content: string; // content received from LLM
|
||||
displayContent: string | undefined; // content displayed to user (this is the same as content for now)
|
||||
}
|
||||
| {
|
||||
role: "system";
|
||||
content: string;
|
||||
displayContent?: undefined;
|
||||
}
|
||||
|
||||
export {
|
||||
BaseDiff, Diff,
|
||||
DiffArea,
|
||||
CodeSelection,
|
||||
File,
|
||||
MessageFromSidebar,
|
||||
MessageToSidebar,
|
||||
ChatThreads,
|
||||
ChatMessage,
|
||||
}
|
||||
@@ -1,209 +0,0 @@
|
||||
import * as vscode from 'vscode';
|
||||
import { DiffProvider } from './DiffProvider';
|
||||
import { DiffArea, ChatThreads, MessageFromSidebar, MessageToSidebar } from './common/shared_types';
|
||||
import { SidebarWebviewProvider } from './SidebarWebviewProvider';
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import { applyDiffLazily } from './common/ctrlL';
|
||||
import { getVoidConfig } from './sidebar/contextForConfig';
|
||||
import { readFileContentOfUri } from './common/readFileContentOfUri';
|
||||
import { AbortRef } from './common/sendLLMMessage';
|
||||
|
||||
// this comes from vscode.proposed.editorInsets.d.ts
|
||||
declare module 'vscode' {
|
||||
export interface WebviewEditorInset {
|
||||
readonly editor: vscode.TextEditor;
|
||||
readonly line: number;
|
||||
readonly height: number;
|
||||
readonly webview: vscode.Webview;
|
||||
readonly onDidDispose: Event<void>;
|
||||
dispose(): void;
|
||||
}
|
||||
export namespace window {
|
||||
export function createWebviewTextEditorInset(editor: vscode.TextEditor, line: number, height: number, options?: vscode.WebviewOptions): WebviewEditorInset;
|
||||
}
|
||||
}
|
||||
|
||||
const roundRangeToLines = (selection: vscode.Selection) => {
|
||||
return new vscode.Range(selection.start.line, 0, selection.end.line, Number.MAX_SAFE_INTEGER)
|
||||
}
|
||||
|
||||
export function activate(context: vscode.ExtensionContext) {
|
||||
|
||||
// 1. Mount the chat sidebar
|
||||
const sidebarWebviewProvider = new SidebarWebviewProvider(context);
|
||||
context.subscriptions.push(
|
||||
vscode.window.registerWebviewViewProvider(SidebarWebviewProvider.viewId, sidebarWebviewProvider, { webviewOptions: { retainContextWhenHidden: true } })
|
||||
);
|
||||
|
||||
|
||||
|
||||
// 2. ctrl+l
|
||||
context.subscriptions.push(
|
||||
vscode.commands.registerCommand('void.ctrl+l', () => {
|
||||
const editor = vscode.window.activeTextEditor
|
||||
if (!editor) return
|
||||
|
||||
|
||||
// const inset = vscode.window.createWebviewTextEditorInset(editor, 10, 10, {})
|
||||
// inset.webview.html = `
|
||||
// <html>
|
||||
// <body style="pointer-events:none;">Hello World!</body>
|
||||
// </html>
|
||||
// `;
|
||||
|
||||
|
||||
// show the sidebar
|
||||
vscode.commands.executeCommand('workbench.view.extension.voidViewContainer');
|
||||
// vscode.commands.executeCommand('vscode.moveViewToPanel', CustomViewProvider.viewId); // move to aux bar
|
||||
|
||||
// get the range of the selection
|
||||
const selectionRange = roundRangeToLines(editor.selection);
|
||||
|
||||
// get the text the user is selecting
|
||||
const selectionStr = editor.document.getText(selectionRange);
|
||||
|
||||
// get the file the user is in
|
||||
const filePath = editor.document.uri;
|
||||
|
||||
// send message to the webview (Sidebar.tsx)
|
||||
sidebarWebviewProvider.webview.then(webview => webview.postMessage({ type: 'ctrl+l', selection: { selectionStr, selectionRange, filePath } } satisfies MessageToSidebar));
|
||||
})
|
||||
);
|
||||
|
||||
// 2.5: ctrl+k
|
||||
context.subscriptions.push(
|
||||
vscode.commands.registerCommand('void.ctrl+k', () => {
|
||||
console.log('CTRLK PRESSED')
|
||||
const editor = vscode.window.activeTextEditor
|
||||
if (!editor) return
|
||||
|
||||
// get the range of the selection
|
||||
const selectionRange = roundRangeToLines(editor.selection);
|
||||
|
||||
// get the text the user is selecting
|
||||
const selectionStr = editor.document.getText(selectionRange);
|
||||
|
||||
// get the file the user is in
|
||||
const filePath = editor.document.uri;
|
||||
|
||||
// send message to the webview (Sidebar.tsx)
|
||||
sidebarWebviewProvider.webview.then(webview => webview.postMessage({ type: 'ctrl+k', selection: { selectionStr, selectionRange, filePath } } satisfies MessageToSidebar));
|
||||
})
|
||||
);
|
||||
|
||||
// 3. Show an approve/reject codelens above each change
|
||||
const diffProvider = new DiffProvider();
|
||||
context.subscriptions.push(vscode.languages.registerCodeLensProvider('*', diffProvider));
|
||||
|
||||
// 4. Add approve/reject commands
|
||||
context.subscriptions.push(vscode.commands.registerCommand('void.acceptDiff', async (params) => {
|
||||
diffProvider.acceptDiff(params)
|
||||
}));
|
||||
context.subscriptions.push(vscode.commands.registerCommand('void.rejectDiff', async (params) => {
|
||||
diffProvider.rejectDiff(params)
|
||||
}));
|
||||
|
||||
// 5. Receive messages from sidebar
|
||||
sidebarWebviewProvider.webview.then(
|
||||
webview => {
|
||||
|
||||
// top navigation bar commands
|
||||
context.subscriptions.push(vscode.commands.registerCommand('void.startNewThread', async () => {
|
||||
webview.postMessage({ type: 'startNewThread' } satisfies MessageToSidebar)
|
||||
}))
|
||||
context.subscriptions.push(vscode.commands.registerCommand('void.toggleThreadSelector', async () => {
|
||||
webview.postMessage({ type: 'toggleThreadSelector' } satisfies MessageToSidebar)
|
||||
}))
|
||||
context.subscriptions.push(vscode.commands.registerCommand('void.toggleSettings', async () => {
|
||||
webview.postMessage({ type: 'toggleSettings' } satisfies MessageToSidebar)
|
||||
}));
|
||||
|
||||
// Receive messages in the extension from the sidebar webview (messages are sent using `postMessage`)
|
||||
webview.onDidReceiveMessage(async (m: MessageFromSidebar) => {
|
||||
|
||||
const abortApplyRef: AbortRef = { current: null }
|
||||
|
||||
if (m.type === 'requestFiles') {
|
||||
|
||||
// get contents of all file paths
|
||||
const files = await Promise.all(
|
||||
m.filepaths.map(async (filepath) => ({ filepath, content: await readFileContentOfUri(filepath) }))
|
||||
)
|
||||
|
||||
// send contents to webview
|
||||
webview.postMessage({ type: 'files', files, } satisfies MessageToSidebar)
|
||||
|
||||
}
|
||||
else if (m.type === 'applyChanges') {
|
||||
|
||||
const editor = vscode.window.activeTextEditor
|
||||
if (!editor) {
|
||||
vscode.window.showInformationMessage('No active editor!')
|
||||
return
|
||||
}
|
||||
// create an area to show diffs
|
||||
const partialDiffArea: Omit<DiffArea, 'diffareaid'> = {
|
||||
startLine: 0, // in ctrl+L the start and end lines are the full document
|
||||
endLine: editor.document.lineCount,
|
||||
originalStartLine: 0,
|
||||
originalEndLine: editor.document.lineCount,
|
||||
sweepIndex: null,
|
||||
}
|
||||
const diffArea = diffProvider.createDiffArea(editor.document.uri, partialDiffArea, await readFileContentOfUri(editor.document.uri))
|
||||
|
||||
const docUri = editor.document.uri
|
||||
const fileStr = await readFileContentOfUri(docUri)
|
||||
const voidConfig = getVoidConfig(context.globalState.get('partialVoidConfig') ?? {})
|
||||
|
||||
await applyDiffLazily({ docUri, oldFileStr: fileStr, diffRepr: m.diffRepr, voidConfig, diffProvider, diffArea, abortRef: abortApplyRef })
|
||||
}
|
||||
else if (m.type === 'getPartialVoidConfig') {
|
||||
const partialVoidConfig = context.globalState.get('partialVoidConfig') ?? {}
|
||||
webview.postMessage({ type: 'partialVoidConfig', partialVoidConfig } satisfies MessageToSidebar)
|
||||
}
|
||||
else if (m.type === 'persistPartialVoidConfig') {
|
||||
const partialVoidConfig = m.partialVoidConfig
|
||||
context.globalState.update('partialVoidConfig', partialVoidConfig)
|
||||
}
|
||||
else if (m.type === 'getAllThreads') {
|
||||
const threads: ChatThreads = context.workspaceState.get('allThreads') ?? {}
|
||||
webview.postMessage({ type: 'allThreads', threads } satisfies MessageToSidebar)
|
||||
}
|
||||
else if (m.type === 'persistThread') {
|
||||
const threads: ChatThreads = context.workspaceState.get('allThreads') ?? {}
|
||||
const updatedThreads: ChatThreads = { ...threads, [m.thread.id]: m.thread }
|
||||
context.workspaceState.update('allThreads', updatedThreads)
|
||||
}
|
||||
else if (m.type === 'getDeviceId') {
|
||||
let deviceId = context.globalState.get('void_deviceid')
|
||||
if (!deviceId || typeof deviceId !== 'string') {
|
||||
deviceId = uuidv4()
|
||||
context.globalState.update('void_deviceid', deviceId)
|
||||
}
|
||||
webview.postMessage({ type: 'deviceId', deviceId: deviceId as string } satisfies MessageToSidebar)
|
||||
}
|
||||
else {
|
||||
console.error('unrecognized command', m)
|
||||
}
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
|
||||
|
||||
// Gets called when user presses ctrl + k (mounts ctrl+k-style codelens)
|
||||
// TODO need to build this
|
||||
// const ctrlKCodeLensProvider = new CtrlKCodeLensProvider();
|
||||
// context.subscriptions.push(vscode.languages.registerCodeLensProvider('*', ctrlKCodeLensProvider));
|
||||
// context.subscriptions.push(
|
||||
// vscode.commands.registerCommand('void.ctrl+k', () => {
|
||||
// const editor = vscode.window.activeTextEditor;
|
||||
// if (!editor)
|
||||
// return
|
||||
// ctrlKCodeLensProvider.addNewCodeLens(editor.document, editor.selection);
|
||||
// // vscode.commands.executeCommand('editor.action.showHover'); // apparently this refreshes the codelenses by having the internals call provideCodeLenses
|
||||
// })
|
||||
// )
|
||||
|
||||
}
|
||||
@@ -1,131 +0,0 @@
|
||||
|
||||
import { Range } from 'vscode';
|
||||
import { diffLines, Change } from 'diff';
|
||||
import { BaseDiff } from './common/shared_types';
|
||||
|
||||
|
||||
// class Range {
|
||||
// range: any;
|
||||
// constructor(startLine, startCol, endLine, endCol) {
|
||||
// const range = {
|
||||
// startLine,
|
||||
// startCol,
|
||||
// endLine,
|
||||
// endCol,
|
||||
// };
|
||||
// this.range = range;
|
||||
// }
|
||||
// }
|
||||
|
||||
|
||||
|
||||
// Andrew diff algo:
|
||||
export type SuggestedEdit = {
|
||||
// start/end of current file
|
||||
newRange: Range;
|
||||
|
||||
// start/end of original file
|
||||
originalRange: Range;
|
||||
type: 'insertion' | 'deletion' | 'edit',
|
||||
originalContent: string, // original content (originalfile[originalStart...originalEnd])
|
||||
newContent: string,
|
||||
}
|
||||
|
||||
export function findDiffs(oldStr: string, newStr: string) {
|
||||
// an ordered list of every original line, line added to the new file, and line removed from the old file (order is unambiguous, think about it)
|
||||
const lineByLineChanges: Change[] = diffLines(oldStr, newStr);
|
||||
lineByLineChanges.push({ value: '' }) // add a dummy so we flush any streaks we haven't yet at the very end (!line.added && !line.removed)
|
||||
|
||||
let oldFileLineNum: number = 0;
|
||||
let newFileLineNum: number = 0;
|
||||
|
||||
let streakStartInNewFile: number | undefined = undefined
|
||||
let streakStartInOldFile: number | undefined = undefined
|
||||
|
||||
let oldStrLines = oldStr.split('\n')
|
||||
let newStrLines = newStr.split('\n')
|
||||
|
||||
const replacements: BaseDiff[] = []
|
||||
for (let line of lineByLineChanges) {
|
||||
|
||||
// no change on this line
|
||||
if (!line.added && !line.removed) {
|
||||
|
||||
// do nothing
|
||||
|
||||
// if we were on a streak of +s and -s, end it
|
||||
if (streakStartInNewFile !== undefined) {
|
||||
let type: 'edit' | 'insertion' | 'deletion' = 'edit'
|
||||
|
||||
let startLine = streakStartInNewFile
|
||||
let endLine = newFileLineNum - 1 // don't include current line, the edit was up to this line but not including it
|
||||
let startCol = 0
|
||||
let endCol = Number.MAX_SAFE_INTEGER
|
||||
|
||||
let originalStartLine = streakStartInOldFile!
|
||||
let originalEndLine = oldFileLineNum - 1 // don't include current line, the edit was up to this line but not including it
|
||||
let originalStartCol = 0
|
||||
let originalEndCol = Number.MAX_SAFE_INTEGER
|
||||
|
||||
let newContent = newStrLines.slice(startLine, endLine + 1).join('\n')
|
||||
let originalContent = oldStrLines.slice(originalStartLine, originalEndLine + 1).join('\n')
|
||||
|
||||
// if the range is empty, mark it as a deletion / insertion (both won't be true at once)
|
||||
// DELETION
|
||||
if (endLine === startLine - 1) {
|
||||
type = 'deletion'
|
||||
endLine = startLine
|
||||
startCol = 0
|
||||
endCol = 0
|
||||
newContent += '\n'
|
||||
}
|
||||
|
||||
// INSERTION
|
||||
else if (originalEndLine === originalStartLine - 1) {
|
||||
type = 'insertion'
|
||||
originalEndLine = originalStartLine
|
||||
originalStartCol = 0
|
||||
originalEndCol = 0
|
||||
}
|
||||
|
||||
const replacement: BaseDiff = {
|
||||
type,
|
||||
range: new Range(startLine, startCol, endLine, endCol),
|
||||
code: newContent,
|
||||
originalRange: new Range(originalStartLine, originalStartCol, originalEndLine, originalEndCol),
|
||||
originalCode: originalContent,
|
||||
}
|
||||
|
||||
replacements.push(replacement)
|
||||
|
||||
streakStartInNewFile = undefined
|
||||
streakStartInOldFile = undefined
|
||||
}
|
||||
oldFileLineNum += line.count ?? 0;
|
||||
newFileLineNum += line.count ?? 0;
|
||||
}
|
||||
|
||||
// line was removed from old file
|
||||
else if (line.removed) {
|
||||
// if we weren't on a streak, start one on this current line num
|
||||
if (streakStartInNewFile === undefined) {
|
||||
streakStartInNewFile = newFileLineNum
|
||||
streakStartInOldFile = oldFileLineNum
|
||||
}
|
||||
oldFileLineNum += line.count ?? 0 // we processed the line so add 1
|
||||
}
|
||||
|
||||
// line was added to new file
|
||||
else if (line.added) {
|
||||
// if we weren't on a streak, start one on this current line num
|
||||
if (streakStartInNewFile === undefined) {
|
||||
streakStartInNewFile = newFileLineNum
|
||||
streakStartInOldFile = oldFileLineNum
|
||||
}
|
||||
newFileLineNum += line.count ?? 0; // we processed the line so add 1
|
||||
}
|
||||
} // end for
|
||||
|
||||
console.debug('Replacements', replacements)
|
||||
return replacements
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useOnVSCodeMessage } from './getVscodeApi';
|
||||
|
||||
|
||||
export const CtrlK = () => {
|
||||
|
||||
const [x, sx] = useState('abc')
|
||||
|
||||
useOnVSCodeMessage('ctrl+k', () => {
|
||||
console.log('Ctrl+K pressed')
|
||||
sx('Pressed ctrl+k')
|
||||
})
|
||||
|
||||
return <>
|
||||
<div>
|
||||
{x}
|
||||
</div>
|
||||
</>
|
||||
};
|
||||
|
||||
@@ -1,67 +0,0 @@
|
||||
import React, { useState, useEffect, useRef, useCallback, FormEvent } from "react"
|
||||
import { CodeSelection, ChatMessage, MessageToSidebar } from "../common/shared_types"
|
||||
import { awaitVSCodeResponse, getVSCodeAPI, onMessageFromVSCode, useOnVSCodeMessage } from "./getVscodeApi"
|
||||
|
||||
import { SidebarThreadSelector } from "./SidebarThreadSelector";
|
||||
import { SidebarChat } from "./SidebarChat";
|
||||
import { SidebarSettings } from './SidebarSettings';
|
||||
import { identifyUser } from "./metrics/posthog";
|
||||
|
||||
|
||||
const Sidebar = () => {
|
||||
|
||||
const chatInputRef = useRef<HTMLTextAreaElement | null>(null)
|
||||
|
||||
const [tab, setTab] = useState<'threadSelector' | 'chat' | 'settings'>('chat')
|
||||
|
||||
// if they pressed the + to add a new chat
|
||||
useOnVSCodeMessage('startNewThread', (m) => {
|
||||
setTab('chat');
|
||||
chatInputRef.current?.focus();
|
||||
})
|
||||
|
||||
// ctrl+l should switch back to chat
|
||||
useOnVSCodeMessage('ctrl+l', (m) => {
|
||||
setTab('chat');
|
||||
chatInputRef.current?.focus();
|
||||
})
|
||||
|
||||
// if they toggled thread selector
|
||||
useOnVSCodeMessage('toggleThreadSelector', (m) => {
|
||||
if (tab === 'threadSelector') {
|
||||
setTab('chat')
|
||||
chatInputRef.current?.blur();
|
||||
} else
|
||||
setTab('threadSelector')
|
||||
})
|
||||
|
||||
// if they toggled settings
|
||||
useOnVSCodeMessage('toggleSettings', (m) => {
|
||||
if (tab === 'settings') {
|
||||
setTab('chat')
|
||||
chatInputRef.current?.blur();
|
||||
} else
|
||||
setTab('settings')
|
||||
})
|
||||
|
||||
return <>
|
||||
<div className={`flex flex-col h-screen w-full`}>
|
||||
|
||||
<div className={`mb-2 h-[30vh] ${tab !== 'threadSelector' ? 'hidden' : ''}`}>
|
||||
<SidebarThreadSelector onClose={() => setTab('chat')} />
|
||||
</div>
|
||||
|
||||
<div className={`${tab !== 'chat' && tab !== 'threadSelector' ? 'hidden' : ''}`}>
|
||||
<SidebarChat chatInputRef={chatInputRef} />
|
||||
</div>
|
||||
|
||||
<div className={`${tab !== 'settings' ? 'hidden' : ''}`}>
|
||||
<SidebarSettings />
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</>
|
||||
|
||||
}
|
||||
|
||||
export default Sidebar
|
||||
@@ -1,382 +0,0 @@
|
||||
import React, { FormEvent, useCallback, useEffect, useRef, useState } from "react";
|
||||
|
||||
|
||||
import { marked } from 'marked';
|
||||
import MarkdownRender from "./markdown/MarkdownRender";
|
||||
import BlockCode from "./markdown/BlockCode";
|
||||
import { File, ChatMessage, CodeSelection } from "../common/shared_types";
|
||||
import * as vscode from 'vscode'
|
||||
import { awaitVSCodeResponse, getVSCodeAPI, onMessageFromVSCode, useOnVSCodeMessage } from "./getVscodeApi";
|
||||
import { useThreads } from "./contextForThreads";
|
||||
import { sendLLMMessage } from "../common/sendLLMMessage";
|
||||
import { useVoidConfig } from "./contextForConfig";
|
||||
import { generateDiffInstructions } from "../common/systemPrompts";
|
||||
import { captureEvent } from "./metrics/posthog";
|
||||
|
||||
|
||||
|
||||
const filesStr = (fullFiles: File[]) => {
|
||||
return fullFiles.map(({ filepath, content }) =>
|
||||
`
|
||||
${filepath.fsPath}
|
||||
\`\`\`
|
||||
${content}
|
||||
\`\`\``).join('\n')
|
||||
}
|
||||
|
||||
const userInstructionsStr = (instructions: string, files: File[], selection: CodeSelection | null) => {
|
||||
let str = '';
|
||||
|
||||
if (files.length > 0) {
|
||||
str += filesStr(files);
|
||||
}
|
||||
|
||||
if (selection) {
|
||||
str += `
|
||||
I am currently selecting this code:
|
||||
\t\`\`\`${selection.selectionStr}\`\`\`
|
||||
`;
|
||||
}
|
||||
|
||||
if (files.length > 0 && selection) {
|
||||
str += `
|
||||
Please edit the selected code or the entire file following these instructions:
|
||||
`;
|
||||
} else if (files.length > 0) {
|
||||
str += `
|
||||
Please edit the file following these instructions:
|
||||
`;
|
||||
} else if (selection) {
|
||||
str += `
|
||||
Please edit the selected code following these instructions:
|
||||
`;
|
||||
}
|
||||
|
||||
str += `
|
||||
\t${instructions}
|
||||
`;
|
||||
if (files.length > 0) {
|
||||
str += `
|
||||
\tIf you make a change, rewrite the entire file.
|
||||
`; // TODO don't rewrite the whole file on prompt, instead rewrite it when click Apply
|
||||
}
|
||||
return str;
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
const getBasename = (pathStr: string) => {
|
||||
// "unixify" path
|
||||
pathStr = pathStr.replace(/[/\\]+/g, "/") // replace any / or \ or \\ with /
|
||||
const parts = pathStr.split("/") // split on /
|
||||
return parts[parts.length - 1]
|
||||
}
|
||||
|
||||
export const SelectedFiles = ({ files, setFiles, }: { files: vscode.Uri[], setFiles: null | ((files: vscode.Uri[]) => void) }) => {
|
||||
return (
|
||||
files.length !== 0 && (
|
||||
<div className="flex flex-wrap -mx-1 -mb-1">
|
||||
{files.map((filename, i) => (
|
||||
<button
|
||||
key={filename.path}
|
||||
disabled={!setFiles}
|
||||
className={`btn btn-secondary btn-sm border border-vscode-input-border rounded flex items-center space-x-2 mx-1 mb-1 disabled:cursor-default`}
|
||||
type="button"
|
||||
onClick={() => setFiles?.([...files.slice(0, i), ...files.slice(i + 1, Infinity)])}
|
||||
>
|
||||
<span>{getBasename(filename.fsPath)}</span>
|
||||
|
||||
{/* X button */}
|
||||
{!!setFiles && <span className="">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
className="size-4"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M6 18 18 6M6 6l12 12"
|
||||
/>
|
||||
</svg>
|
||||
</span>}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
const ChatBubble = ({ chatMessage }: { chatMessage: ChatMessage }) => {
|
||||
|
||||
const role = chatMessage.role
|
||||
const children = chatMessage.displayContent
|
||||
|
||||
if (!children)
|
||||
return null
|
||||
|
||||
let chatbubbleContents: React.ReactNode
|
||||
|
||||
if (role === 'user') {
|
||||
chatbubbleContents = <>
|
||||
<SelectedFiles files={chatMessage.files} setFiles={null} />
|
||||
{chatMessage.selection?.selectionStr && <BlockCode
|
||||
text={chatMessage.selection.selectionStr}
|
||||
buttonsOnHover={null}
|
||||
/>}
|
||||
{children}
|
||||
</>
|
||||
}
|
||||
else if (role === 'assistant') {
|
||||
chatbubbleContents = <MarkdownRender string={children} /> // sectionsHTML
|
||||
}
|
||||
|
||||
return <div className={`${role === 'user' ? 'text-right' : 'text-left'}`}>
|
||||
<div className={`inline-block p-2 rounded-lg space-y-2 ${role === 'user' ? 'bg-vscode-input-bg text-vscode-input-fg' : ''} max-w-full`}>
|
||||
{chatbubbleContents}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
|
||||
|
||||
export const SidebarChat = ({ chatInputRef }: { chatInputRef: React.RefObject<HTMLTextAreaElement> }) => {
|
||||
|
||||
|
||||
// state of current message
|
||||
const [selection, setSelection] = useState<CodeSelection | null>(null) // the code the user is selecting
|
||||
const [files, setFiles] = useState<vscode.Uri[]>([]) // the names of the files in the chat
|
||||
const [instructions, setInstructions] = useState('') // the user's instructions
|
||||
|
||||
// state of chat
|
||||
const [messageStream, setMessageStream] = useState('')
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const abortFnRef = useRef<(() => void) | null>(null)
|
||||
|
||||
const [latestError, setLatestError] = useState('')
|
||||
|
||||
// higher level state
|
||||
const { getAllThreads, getCurrentThread, addMessageToHistory, startNewThread, switchToThread } = useThreads()
|
||||
|
||||
const { voidConfig } = useVoidConfig()
|
||||
|
||||
|
||||
|
||||
// only captures number of messages and message "shape", no actual code, instructions, prompts, etc
|
||||
const captureChatEvent = useCallback((eventId: string, extras?: object) => {
|
||||
const whichApi = voidConfig.default['whichApi']
|
||||
const messages = getCurrentThread()?.messages
|
||||
|
||||
captureEvent(eventId, {
|
||||
whichApi: whichApi,
|
||||
numMessages: messages?.length,
|
||||
messagesShape: messages?.map(msg => ({ role: msg.role, length: msg.displayContent?.length })),
|
||||
version: '2024-10-19',
|
||||
...extras,
|
||||
})
|
||||
}, [getCurrentThread, voidConfig.default])
|
||||
|
||||
|
||||
// if they pressed the + to add a new chat
|
||||
useOnVSCodeMessage('startNewThread', (m) => {
|
||||
const allThreads = getAllThreads()
|
||||
// find a thread with 0 messages and switch to it
|
||||
for (let threadId in allThreads) {
|
||||
if (allThreads[threadId].messages.length === 0) {
|
||||
switchToThread(threadId)
|
||||
return
|
||||
}
|
||||
}
|
||||
// start a new thread
|
||||
startNewThread()
|
||||
})
|
||||
|
||||
// if user pressed ctrl+l, add their selection to the sidebar
|
||||
useOnVSCodeMessage('ctrl+l', (m) => {
|
||||
setSelection(m.selection)
|
||||
const filepath = m.selection.filePath
|
||||
|
||||
// add current file to the context if it's not already in the files array
|
||||
if (!files.find(f => f.fsPath === filepath.fsPath))
|
||||
setFiles(files => [...files, filepath])
|
||||
})
|
||||
|
||||
|
||||
const isDisabled = !instructions
|
||||
|
||||
const formRef = useRef<HTMLFormElement | null>(null)
|
||||
const onSubmit = async (e: FormEvent<HTMLFormElement>) => {
|
||||
|
||||
e.preventDefault()
|
||||
if (isDisabled) return
|
||||
if (isLoading) return
|
||||
|
||||
setIsLoading(true)
|
||||
setInstructions('');
|
||||
formRef.current?.reset(); // reset the form's text when clear instructions or unexpected behavior happens
|
||||
setSelection(null)
|
||||
setFiles([])
|
||||
setLatestError('')
|
||||
|
||||
// request file content from vscode and await response
|
||||
getVSCodeAPI().postMessage({ type: 'requestFiles', filepaths: files })
|
||||
const relevantFiles = await awaitVSCodeResponse('files')
|
||||
|
||||
// add system message to chat history
|
||||
const systemPromptElt: ChatMessage = { role: 'system', content: generateDiffInstructions }
|
||||
addMessageToHistory(systemPromptElt)
|
||||
|
||||
const userContent = userInstructionsStr(instructions, relevantFiles.files, selection)
|
||||
const newHistoryElt: ChatMessage = { role: 'user', content: userContent, displayContent: instructions, selection, files }
|
||||
addMessageToHistory(newHistoryElt)
|
||||
|
||||
captureChatEvent('Chat - Sending Message', { messageLength: instructions.length })
|
||||
const submit_time = new Date()
|
||||
|
||||
// send message to LLM
|
||||
sendLLMMessage({
|
||||
messages: [...(getCurrentThread()?.messages ?? []).map(m => ({ role: m.role, content: m.content })),],
|
||||
onText: (newText, fullText) => setMessageStream(fullText),
|
||||
onFinalMessage: (content) => {
|
||||
captureChatEvent('Chat - Received Full Message', { messageLength: content.length, duration: new Date().getMilliseconds() - submit_time.getMilliseconds() })
|
||||
|
||||
// add assistant's message to chat history, and clear selection
|
||||
const newHistoryElt: ChatMessage = { role: 'assistant', content, displayContent: content }
|
||||
addMessageToHistory(newHistoryElt)
|
||||
setMessageStream('')
|
||||
setIsLoading(false)
|
||||
},
|
||||
onError: (error) => {
|
||||
captureChatEvent('Chat - Error', { error })
|
||||
|
||||
// add assistant's message to chat history, and clear selection
|
||||
let content = messageStream; // just use the current content
|
||||
const newHistoryElt: ChatMessage = { role: 'assistant', content, displayContent: content, }
|
||||
addMessageToHistory(newHistoryElt)
|
||||
setMessageStream('')
|
||||
setIsLoading(false)
|
||||
|
||||
setLatestError(error)
|
||||
},
|
||||
voidConfig,
|
||||
abortRef: abortFnRef,
|
||||
})
|
||||
|
||||
|
||||
}
|
||||
|
||||
const onAbort = useCallback(() => {
|
||||
|
||||
captureChatEvent('Chat - Abort', { messageLengthSoFar: messageStream.length })
|
||||
|
||||
// abort claude
|
||||
abortFnRef.current?.()
|
||||
|
||||
// if messageStream was not empty, add it to the history
|
||||
const llmContent = messageStream || '(null)'
|
||||
const newHistoryElt: ChatMessage = { role: 'assistant', content: llmContent, displayContent: messageStream, }
|
||||
addMessageToHistory(newHistoryElt)
|
||||
|
||||
setMessageStream('')
|
||||
setIsLoading(false)
|
||||
|
||||
}, [captureChatEvent, messageStream, addMessageToHistory])
|
||||
|
||||
|
||||
return <>
|
||||
<div className="overflow-x-hidden space-y-4">
|
||||
{/* previous messages */}
|
||||
{getCurrentThread() !== null && getCurrentThread()?.messages.map((message, i) =>
|
||||
<ChatBubble key={i} chatMessage={message} />
|
||||
)}
|
||||
{/* message stream */}
|
||||
<ChatBubble chatMessage={{ role: 'assistant', content: messageStream, displayContent: messageStream }} />
|
||||
</div>
|
||||
{/* chatbar */}
|
||||
<div className="shrink-0 py-4">
|
||||
{/* selection */}
|
||||
<div className="text-left">
|
||||
<div className="relative">
|
||||
<div className="input">
|
||||
{/* selection */}
|
||||
{(files.length || selection?.selectionStr) && <div className="p-2 pb-0 space-y-2">
|
||||
{/* selected files */}
|
||||
<SelectedFiles files={files} setFiles={setFiles} />
|
||||
{/* selected code */}
|
||||
{!!selection?.selectionStr && (
|
||||
<BlockCode text={selection.selectionStr}
|
||||
buttonsOnHover={(
|
||||
<button
|
||||
onClick={() => setSelection(null)}
|
||||
className="btn btn-secondary btn-sm border border-vscode-input-border rounded"
|
||||
>
|
||||
Remove
|
||||
</button>
|
||||
)} />
|
||||
)}
|
||||
</div>}
|
||||
|
||||
<form
|
||||
ref={formRef}
|
||||
className="flex flex-row items-center rounded-md p-2"
|
||||
onKeyDown={(e) => { if (e.key === 'Enter' && !e.shiftKey) onSubmit(e) }}
|
||||
|
||||
onSubmit={(e) => {
|
||||
console.log('submit!')
|
||||
onSubmit(e)
|
||||
}}>
|
||||
{/* input */}
|
||||
|
||||
<textarea
|
||||
ref={chatInputRef}
|
||||
onChange={(e) => { setInstructions(e.target.value) }}
|
||||
className="w-full p-2 leading-tight resize-none max-h-[50vh] overflow-hidden bg-transparent border-none !outline-none"
|
||||
placeholder="Ctrl+L to select"
|
||||
rows={1}
|
||||
onInput={e => { e.currentTarget.style.height = 'auto'; e.currentTarget.style.height = e.currentTarget.scrollHeight + 'px' }} // Adjust height dynamically
|
||||
/>
|
||||
{isLoading ?
|
||||
// stop button
|
||||
<button
|
||||
onClick={onAbort}
|
||||
type='button'
|
||||
className="btn btn-primary font-bold size-8 flex justify-center items-center rounded-full p-2 max-h-10"
|
||||
>
|
||||
<svg
|
||||
className='scale-50'
|
||||
stroke="currentColor" fill="currentColor" strokeWidth="0" viewBox="0 0 24 24" height="24" width="24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M24 24H0V0h24v24z"></path>
|
||||
</svg>
|
||||
</button>
|
||||
:
|
||||
// submit button (up arrow)
|
||||
<button
|
||||
className="btn btn-primary font-bold size-8 flex justify-center items-center rounded-full p-2 max-h-10"
|
||||
disabled={isDisabled}
|
||||
type='submit'
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<line x1="12" y1="19" x2="12" y2="5"></line>
|
||||
<polyline points="5 12 12 5 19 12"></polyline>
|
||||
</svg>
|
||||
</button>
|
||||
}
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* error message */}
|
||||
{!latestError ? null : <div>
|
||||
{latestError}
|
||||
</div>}
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
|
||||
|
||||
@@ -1,105 +0,0 @@
|
||||
import React, { useState } from "react";
|
||||
import { configFields, useVoidConfig, VoidConfigField } from "./contextForConfig";
|
||||
|
||||
|
||||
const SettingOfFieldAndParam = ({ field, param }: { field: VoidConfigField, param: string }) => {
|
||||
const { voidConfig, partialVoidConfig, voidConfigInfo, setConfigParam } = useVoidConfig()
|
||||
const { enumArr, defaultVal, description } = voidConfigInfo[field][param]
|
||||
const val = partialVoidConfig[field]?.[param] ?? defaultVal // current value of this item
|
||||
|
||||
const updateState = (newValue: string) => { setConfigParam(field, param, newValue) }
|
||||
|
||||
const resetButton = <button
|
||||
disabled={val === defaultVal}
|
||||
title={val === defaultVal ? 'This is the default value.' : `Revert value to '${defaultVal}'?`}
|
||||
className='group btn btn-sm disabled:opacity-75 disabled:cursor-default'
|
||||
onClick={() => updateState(defaultVal)}
|
||||
>
|
||||
<svg
|
||||
className='size-5 group-disabled:stroke-current group-disabled:fill-current group-hover:stroke-red-600 group-hover:fill-red-600 duration-200'
|
||||
fill="currentColor" strokeWidth="0" viewBox="0 0 16 16" height="200px" width="200px" xmlns="http://www.w3.org/2000/svg"><path fillRule="evenodd" clipRule="evenodd" d="M3.5 2v3.5L4 6h3.5V5H4.979l.941-.941a3.552 3.552 0 1 1 5.023 5.023L5.746 14.28l.72.72 5.198-5.198A4.57 4.57 0 0 0 5.2 3.339l-.7.7V2h-1z"></path>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
const inputElement = enumArr === undefined ?
|
||||
// string
|
||||
(<input
|
||||
className='input p-1 w-full'
|
||||
type="text"
|
||||
value={val}
|
||||
onChange={(e) => updateState(e.target.value)}
|
||||
/>)
|
||||
:
|
||||
// enum
|
||||
(<select
|
||||
className='dropdown p-1 w-full'
|
||||
value={val}
|
||||
onChange={(e) => updateState(e.target.value)}
|
||||
>
|
||||
{enumArr.map((option) => (
|
||||
<option key={option} value={option}>
|
||||
{option}
|
||||
</option>
|
||||
))}
|
||||
</select>)
|
||||
|
||||
return <div>
|
||||
<label className='hidden'>{param}</label>
|
||||
<span>{description}</span>
|
||||
<div className='flex items-center'>
|
||||
{inputElement}
|
||||
{resetButton}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
export const SidebarSettings = () => {
|
||||
|
||||
const { voidConfig, voidConfigInfo } = useVoidConfig()
|
||||
|
||||
const current_field = voidConfig.default['whichApi'] as VoidConfigField
|
||||
|
||||
|
||||
return (
|
||||
<div className='space-y-4 py-2 overflow-y-auto'>
|
||||
|
||||
{/* choose the field */}
|
||||
<div className='outline-vscode-input-bg'>
|
||||
<SettingOfFieldAndParam
|
||||
field='default'
|
||||
param='whichApi'
|
||||
/>
|
||||
<SettingOfFieldAndParam
|
||||
field='default'
|
||||
param='maxTokens'
|
||||
/>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
{/* render all fields, but hide the ones not visible for fast tab switching */}
|
||||
{configFields.map(field => {
|
||||
return <div
|
||||
key={field}
|
||||
className={`flex flex-col gap-y-2 ${field !== current_field ? 'hidden' : ''}`}
|
||||
>
|
||||
{Object.keys(voidConfigInfo[field]).map((param) => (
|
||||
<SettingOfFieldAndParam
|
||||
key={param}
|
||||
field={field}
|
||||
param={param}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
})}
|
||||
|
||||
{/* Remove this after 10/21/24, this is just to give developers a heads up about the recent change */}
|
||||
<div className='pt-20'>
|
||||
{`We recently updated Settings. To copy your old Void settings over, press Ctrl+Shift+P, `}
|
||||
{`type 'Open User Settings (JSON)',`}
|
||||
{` and look for 'void.'. `}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,79 +0,0 @@
|
||||
import React from "react";
|
||||
import { ThreadsProvider, useThreads } from "./contextForThreads";
|
||||
|
||||
|
||||
const truncate = (s: string) => {
|
||||
let len = s.length
|
||||
const TRUNC_AFTER = 16
|
||||
if (len >= TRUNC_AFTER)
|
||||
s = s.substring(0, TRUNC_AFTER) + '...'
|
||||
return s
|
||||
}
|
||||
|
||||
|
||||
export const SidebarThreadSelector = ({ onClose }: { onClose: () => void }) => {
|
||||
const { getAllThreads, getCurrentThread, switchToThread } = useThreads()
|
||||
|
||||
const allThreads = getAllThreads()
|
||||
|
||||
// sorted by most recent to least recent
|
||||
const sortedThreadIds = Object.keys(allThreads ?? {}).sort((threadId1, threadId2) => allThreads![threadId1].lastModified > allThreads![threadId2].lastModified ? 1 : -1)
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-y-1">
|
||||
|
||||
{/* X button at top right */}
|
||||
<div className="text-right">
|
||||
<button className="btn btn-sm" onClick={onClose}>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
className="size-4"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M6 18 18 6M6 6l12 12"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* a list of all the past threads */}
|
||||
<div className='flex flex-col gap-y-1 max-h-80 overflow-y-auto'>
|
||||
{sortedThreadIds.map((threadId) => {
|
||||
if (!allThreads)
|
||||
return <>Error: Threads not found.</>
|
||||
const pastThread = allThreads[threadId]
|
||||
|
||||
let btnStringArr = []
|
||||
|
||||
let msg1 = truncate(allThreads[threadId].messages[0]?.displayContent ?? '(empty)')
|
||||
btnStringArr.push(msg1)
|
||||
|
||||
let msg2 = truncate(allThreads[threadId].messages[1]?.displayContent ?? '')
|
||||
if (msg2)
|
||||
btnStringArr.push(msg2)
|
||||
|
||||
btnStringArr.push(allThreads[threadId].messages.length)
|
||||
|
||||
const btnString = btnStringArr.join(' / ')
|
||||
|
||||
return (
|
||||
<button
|
||||
key={pastThread.id}
|
||||
className={`btn btn-sm rounded-sm ${pastThread.id === getCurrentThread()?.id ? "btn-primary" : "btn-secondary"}`}
|
||||
onClick={() => switchToThread(pastThread.id)}
|
||||
title={new Date(pastThread.createdAt).toLocaleString()}
|
||||
>
|
||||
{btnString}
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,275 +0,0 @@
|
||||
import React, { ReactNode, createContext, useCallback, useContext, useEffect, useRef, useState, } from "react"
|
||||
import { awaitVSCodeResponse, getVSCodeAPI, useOnVSCodeMessage } from "./getVscodeApi"
|
||||
|
||||
const configEnum = <EnumArr extends readonly string[]>(description: string, defaultVal: EnumArr[number], enumArr: EnumArr) => {
|
||||
return {
|
||||
description,
|
||||
defaultVal,
|
||||
enumArr,
|
||||
}
|
||||
}
|
||||
|
||||
const configString = (description: string, defaultVal: string) => {
|
||||
return {
|
||||
description,
|
||||
defaultVal,
|
||||
enumArr: undefined,
|
||||
}
|
||||
}
|
||||
|
||||
// fields you can customize (don't forget 'default' - it isn't included here!)
|
||||
export const configFields = [
|
||||
'anthropic',
|
||||
'openAI',
|
||||
'greptile',
|
||||
'ollama',
|
||||
'openRouter',
|
||||
'openAICompatible',
|
||||
'azure'
|
||||
] as const
|
||||
|
||||
|
||||
|
||||
const voidConfigInfo: Record<
|
||||
typeof configFields[number] | 'default', {
|
||||
[prop: string]: {
|
||||
description: string,
|
||||
enumArr?: readonly string[] | undefined,
|
||||
defaultVal: string,
|
||||
},
|
||||
}
|
||||
> = {
|
||||
default: {
|
||||
whichApi: configEnum(
|
||||
"API Provider.",
|
||||
'anthropic',
|
||||
configFields,
|
||||
),
|
||||
|
||||
maxTokens: configEnum(
|
||||
"Max number of tokens to output.",
|
||||
'1024',
|
||||
[
|
||||
"1024",
|
||||
"2048",
|
||||
"4096",
|
||||
"8192"
|
||||
] as const,
|
||||
),
|
||||
|
||||
},
|
||||
anthropic: {
|
||||
apikey: configString('Anthropic API key.', ''),
|
||||
model: configEnum(
|
||||
"Anthropic model to use.",
|
||||
'claude-3-5-sonnet-20240620',
|
||||
[
|
||||
"claude-3-5-sonnet-20240620",
|
||||
"claude-3-opus-20240229",
|
||||
"claude-3-sonnet-20240229",
|
||||
"claude-3-haiku-20240307"
|
||||
] as const,
|
||||
),
|
||||
},
|
||||
openAI: {
|
||||
apikey: configString('OpenAI API key.', ''),
|
||||
model: configEnum(
|
||||
'OpenAI model to use.',
|
||||
'gpt-4o',
|
||||
[
|
||||
"o1-preview",
|
||||
"o1-mini",
|
||||
"gpt-4o",
|
||||
"gpt-4o-2024-05-13",
|
||||
"gpt-4o-2024-08-06",
|
||||
"gpt-4o-mini",
|
||||
"gpt-4o-mini-2024-07-18",
|
||||
"gpt-4-turbo",
|
||||
"gpt-4-turbo-2024-04-09",
|
||||
"gpt-4-turbo-preview",
|
||||
"gpt-4-0125-preview",
|
||||
"gpt-4-1106-preview",
|
||||
"gpt-4",
|
||||
"gpt-4-0613",
|
||||
"gpt-3.5-turbo-0125",
|
||||
"gpt-3.5-turbo",
|
||||
"gpt-3.5-turbo-1106"
|
||||
] as const
|
||||
),
|
||||
},
|
||||
greptile: {
|
||||
apikey: configString('Greptile API key.', ''),
|
||||
githubPAT: configString('Github PAT that Greptile uses to access your repository', ''),
|
||||
remote: configEnum(
|
||||
'Repo location',
|
||||
'github',
|
||||
[
|
||||
'github',
|
||||
'gitlab'
|
||||
] as const
|
||||
),
|
||||
repository: configString('Repository identifier in "owner/repository" format.', ''),
|
||||
branch: configString('Name of the branch to use.', 'main'),
|
||||
},
|
||||
ollama: {
|
||||
endpoint: configString(
|
||||
'The endpoint of your Ollama instance. Start Ollama by running `OLLAMA_ORIGINS="vscode-webview://*" ollama serve`.',
|
||||
'http://127.0.0.1:11434'
|
||||
),
|
||||
// TODO we should allow user to select model inside Void, but for now we'll just let them handle the Ollama setup on their own
|
||||
// model: configEnum(
|
||||
// 'Ollama model to use.',
|
||||
// 'llama3.1',
|
||||
// ["codegemma", "codegemma:2b", "codegemma:7b", "codellama", "codellama:7b", "codellama:13b", "codellama:34b", "codellama:70b", "codellama:code", "codellama:python", "command-r", "command-r:35b", "command-r-plus", "command-r-plus:104b", "deepseek-coder-v2", "deepseek-coder-v2:16b", "deepseek-coder-v2:236b", "falcon2", "falcon2:11b", "firefunction-v2", "firefunction-v2:70b", "gemma", "gemma:2b", "gemma:7b", "gemma2", "gemma2:2b", "gemma2:9b", "gemma2:27b", "llama2", "llama2:7b", "llama2:13b", "llama2:70b", "llama3", "llama3:8b", "llama3:70b", "llama3-chatqa", "llama3-chatqa:8b", "llama3-chatqa:70b", "llama3-gradient", "llama3-gradient:8b", "llama3-gradient:70b", "llama3.1", "llama3.1:8b", "llama3.1:70b", "llama3.1:405b", "llava", "llava:7b", "llava:13b", "llava:34b", "llava-llama3", "llava-llama3:8b", "llava-phi3", "llava-phi3:3.8b", "mistral", "mistral:7b", "mistral-large", "mistral-large:123b", "mistral-nemo", "mistral-nemo:12b", "mixtral", "mixtral:8x7b", "mixtral:8x22b", "moondream", "moondream:1.8b", "openhermes", "openhermes:v2.5", "phi3", "phi3:3.8b", "phi3:14b", "phi3.5", "phi3.5:3.8b", "qwen", "qwen:7b", "qwen:14b", "qwen:32b", "qwen:72b", "qwen:110b", "qwen2", "qwen2:0.5b", "qwen2:1.5b", "qwen2:7b", "qwen2:72b", "smollm", "smollm:135m", "smollm:360m", "smollm:1.7b"] as const
|
||||
// ),
|
||||
},
|
||||
openRouter: {
|
||||
model: configString(
|
||||
'OpenRouter model to use.',
|
||||
'openai/gpt-4o'
|
||||
),
|
||||
apikey: configString('OpenRouter API key.', ''),
|
||||
},
|
||||
openAICompatible: {
|
||||
endpoint: configString('The baseUrl (exluding /chat/completions).', 'http://127.0.0.1:11434/v1'),
|
||||
model: configString('The name of the model to use.', 'gpt-4o'),
|
||||
apikey: configString('Your API key.', ''),
|
||||
},
|
||||
azure: {
|
||||
// "void.azure.apiKey": {
|
||||
// "type": "string",
|
||||
// "description": "Azure API key."
|
||||
// },
|
||||
// "void.azure.deploymentId": {
|
||||
// "type": "string",
|
||||
// "description": "Azure API deployment ID."
|
||||
// },
|
||||
// "void.azure.resourceName": {
|
||||
// "type": "string",
|
||||
// "description": "Name of the Azure OpenAI resource. Either this or `baseURL` can be used. \nThe resource name is used in the assembled URL: `https://{resourceName}.openai.azure.com/openai/deployments/{modelId}{path}`"
|
||||
// },
|
||||
// "void.azure.providerSettings": {
|
||||
// "type": "object",
|
||||
// "properties": {
|
||||
// "baseURL": {
|
||||
// "type": "string",
|
||||
// "default": "https://${resourceName}.openai.azure.com/openai/deployments",
|
||||
// "description": "Azure API base URL."
|
||||
// },
|
||||
// "headers": {
|
||||
// "type": "object",
|
||||
// "description": "Custom headers to include in the requests."
|
||||
// }
|
||||
// }
|
||||
// },
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
// this is the type that comes with metadata like desc, default val, etc
|
||||
type VoidConfigInfo = typeof voidConfigInfo
|
||||
export type VoidConfigField = keyof typeof voidConfigInfo // typeof configFields[number]
|
||||
|
||||
// this is the type that specifies the user's actual config
|
||||
export type PartialVoidConfig = {
|
||||
[K in keyof typeof voidConfigInfo]?: {
|
||||
[P in keyof typeof voidConfigInfo[K]]?: typeof voidConfigInfo[K][P]['defaultVal']
|
||||
}
|
||||
}
|
||||
|
||||
export type VoidConfig = {
|
||||
[K in keyof typeof voidConfigInfo]: {
|
||||
[P in keyof typeof voidConfigInfo[K]]: typeof voidConfigInfo[K][P]['defaultVal']
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
export const getVoidConfig = (currentConfig: PartialVoidConfig): VoidConfig => {
|
||||
const config = {} as PartialVoidConfig
|
||||
for (let field of [...configFields, 'default'] as const) {
|
||||
config[field] = {}
|
||||
for (let prop in voidConfigInfo[field]) {
|
||||
config[field][prop] = currentConfig[field]?.[prop] || voidConfigInfo[field][prop].defaultVal
|
||||
}
|
||||
}
|
||||
return config as VoidConfig
|
||||
}
|
||||
|
||||
const defaultVoidConfig: VoidConfig = getVoidConfig({})
|
||||
|
||||
// const [stateRef, setState] = useInstantState(initVal)
|
||||
// setState instantly changes the value of stateRef instead of having to wait until the next render
|
||||
const useInstantState = <T,>(initVal: T) => {
|
||||
const stateRef = useRef<T>(initVal)
|
||||
const [_, setS] = useState<T>(initVal)
|
||||
const setState = useCallback((newVal: T) => {
|
||||
setS(newVal);
|
||||
stateRef.current = newVal;
|
||||
}, [])
|
||||
return [stateRef as React.RefObject<T>, setState] as const // make s.current readonly - setState handles all changes
|
||||
}
|
||||
|
||||
|
||||
|
||||
type SetConfigParamType = <K extends VoidConfigField>(field: K, param: keyof VoidConfigInfo[K], newVal: string) => void
|
||||
|
||||
type ConfigValueType = {
|
||||
voidConfig: VoidConfig,
|
||||
voidConfigInfo: VoidConfigInfo,
|
||||
partialVoidConfig: PartialVoidConfig,
|
||||
setConfigParam: SetConfigParamType
|
||||
}
|
||||
|
||||
|
||||
const ConfigContext = createContext<ConfigValueType>(undefined as unknown as ConfigValueType)
|
||||
|
||||
export function ConfigProvider({ children }: { children: ReactNode }) {
|
||||
const [partialVoidConfig, setPartialVoidConfig] = useInstantState<PartialVoidConfig>({}) // the user's selections
|
||||
const [voidConfig, setVoidConfig] = useState<VoidConfig>(defaultVoidConfig)
|
||||
|
||||
|
||||
// get the config on mount
|
||||
useEffect(() => {
|
||||
getVSCodeAPI().postMessage({ type: 'getPartialVoidConfig' })
|
||||
awaitVSCodeResponse('partialVoidConfig').then((m) => {
|
||||
setPartialVoidConfig(m.partialVoidConfig)
|
||||
const newFullConfig = getVoidConfig(m.partialVoidConfig)
|
||||
setVoidConfig(newFullConfig)
|
||||
})
|
||||
}, [setPartialVoidConfig])
|
||||
|
||||
// return the provider
|
||||
return (<ConfigContext.Provider
|
||||
value={{
|
||||
voidConfig,
|
||||
voidConfigInfo,
|
||||
partialVoidConfig: partialVoidConfig.current ?? {},
|
||||
setConfigParam: (field, param, newVal) => {
|
||||
const newPartialConfig: PartialVoidConfig = {
|
||||
...partialVoidConfig.current,
|
||||
[field]: {
|
||||
...partialVoidConfig.current?.[field],
|
||||
[param]: newVal
|
||||
}
|
||||
}
|
||||
setPartialVoidConfig(newPartialConfig)
|
||||
const newFullConfig = getVoidConfig(newPartialConfig)
|
||||
setVoidConfig(newFullConfig)
|
||||
getVSCodeAPI().postMessage({ type: 'persistPartialVoidConfig', partialVoidConfig: newPartialConfig })
|
||||
}
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</ConfigContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export function useVoidConfig(): ConfigValueType {
|
||||
const context = useContext<ConfigValueType>(ConfigContext)
|
||||
if (context === undefined) {
|
||||
throw new Error("useVoidConfig missing Provider")
|
||||
}
|
||||
return context
|
||||
}
|
||||
|
||||
@@ -1,106 +0,0 @@
|
||||
import React, { ReactNode, createContext, useCallback, useContext, useEffect, useRef, useState, } from "react"
|
||||
import { ChatMessage, ChatThreads } from "../common/shared_types"
|
||||
import { awaitVSCodeResponse, getVSCodeAPI } from "./getVscodeApi"
|
||||
|
||||
|
||||
// a "thread" means a chat message history
|
||||
type ConfigForThreadsValueType = {
|
||||
readonly getAllThreads: () => ChatThreads;
|
||||
readonly getCurrentThread: () => ChatThreads[string] | null;
|
||||
addMessageToHistory: (message: ChatMessage) => void;
|
||||
switchToThread: (threadId: string) => void;
|
||||
startNewThread: () => void;
|
||||
}
|
||||
|
||||
const ThreadsContext = createContext<ConfigForThreadsValueType>(undefined as unknown as ConfigForThreadsValueType)
|
||||
|
||||
const createNewThread = () => {
|
||||
const now = new Date().toISOString()
|
||||
return {
|
||||
id: new Date().getTime().toString(),
|
||||
createdAt: now,
|
||||
lastModified: now,
|
||||
messages: [],
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// const [stateRef, setState] = useInstantState(initVal)
|
||||
// setState instantly changes the value of stateRef instead of having to wait until the next render
|
||||
const useInstantState = <T,>(initVal: T) => {
|
||||
const stateRef = useRef<T>(initVal)
|
||||
const [_, setS] = useState<T>(initVal)
|
||||
const setState = useCallback((newVal: T) => {
|
||||
setS(newVal);
|
||||
stateRef.current = newVal;
|
||||
}, [])
|
||||
return [stateRef as React.RefObject<T>, setState] as const // make s.current readonly - setState handles all changes
|
||||
}
|
||||
|
||||
|
||||
export function ThreadsProvider({ children }: { children: ReactNode }) {
|
||||
const [allThreadsRef, setAllThreads] = useInstantState<ChatThreads>({})
|
||||
const [currentThreadIdRef, setCurrentThreadId] = useInstantState<string | null>(null)
|
||||
|
||||
// this loads allThreads in on mount
|
||||
useEffect(() => {
|
||||
getVSCodeAPI().postMessage({ type: 'getAllThreads' })
|
||||
awaitVSCodeResponse('allThreads')
|
||||
.then(response => {
|
||||
setAllThreads(response.threads)
|
||||
})
|
||||
}, [setAllThreads])
|
||||
|
||||
|
||||
return (
|
||||
<ThreadsContext.Provider
|
||||
value={{
|
||||
getAllThreads: () => allThreadsRef.current ?? {},
|
||||
getCurrentThread: () => currentThreadIdRef.current ? allThreadsRef.current?.[currentThreadIdRef.current] ?? null : null,
|
||||
addMessageToHistory: (message: ChatMessage) => {
|
||||
let currentThread: ChatThreads[string]
|
||||
if (!(currentThreadIdRef.current === null || allThreadsRef.current === null)) {
|
||||
currentThread = allThreadsRef.current[currentThreadIdRef.current]
|
||||
}
|
||||
else {
|
||||
currentThread = createNewThread()
|
||||
setCurrentThreadId(currentThread.id)
|
||||
}
|
||||
|
||||
setAllThreads({
|
||||
...allThreadsRef.current,
|
||||
[currentThread.id]: {
|
||||
...currentThread,
|
||||
lastModified: new Date().toISOString(),
|
||||
messages: [...currentThread.messages, message],
|
||||
}
|
||||
})
|
||||
|
||||
getVSCodeAPI().postMessage({ type: "persistThread", thread: currentThread })
|
||||
},
|
||||
switchToThread: (threadId: string) => {
|
||||
setCurrentThreadId(threadId);
|
||||
},
|
||||
startNewThread: () => {
|
||||
const newThread = createNewThread()
|
||||
setAllThreads({
|
||||
...allThreadsRef.current,
|
||||
[newThread.id]: newThread
|
||||
})
|
||||
setCurrentThreadId(newThread.id)
|
||||
},
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</ThreadsContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export function useThreads(): ConfigForThreadsValueType {
|
||||
const context = useContext<ConfigForThreadsValueType>(ThreadsContext)
|
||||
if (context === undefined) {
|
||||
throw new Error("useThreads missing Provider")
|
||||
}
|
||||
return context
|
||||
}
|
||||
|
||||
@@ -1,94 +0,0 @@
|
||||
import { useEffect } from "react";
|
||||
import { MessageFromSidebar, MessageToSidebar, } from "../common/shared_types";
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
|
||||
type Command = MessageToSidebar['type']
|
||||
|
||||
// messageType -> res[]
|
||||
const onetimeCallbacks: { [C in Command]: ((res: any) => void)[] } = {
|
||||
"ctrl+l": [],
|
||||
"ctrl+k": [],
|
||||
"files": [],
|
||||
"partialVoidConfig": [],
|
||||
"startNewThread": [],
|
||||
"allThreads": [],
|
||||
"toggleThreadSelector": [],
|
||||
"toggleSettings": [],
|
||||
"deviceId": [],
|
||||
}
|
||||
|
||||
// messageType -> id -> res
|
||||
const callbacks: { [C in Command]: { [id: string]: ((res: any) => void) } } = {
|
||||
"ctrl+l": {},
|
||||
"ctrl+k": {},
|
||||
"files": {},
|
||||
"partialVoidConfig": {},
|
||||
"startNewThread": {},
|
||||
"allThreads": {},
|
||||
"toggleThreadSelector": {},
|
||||
"toggleSettings": {},
|
||||
"deviceId": {}
|
||||
}
|
||||
|
||||
|
||||
// use this function to await responses
|
||||
export const awaitVSCodeResponse = <C extends Command>(c: C) => {
|
||||
let result: Promise<MessageToSidebar & { type: C }> = new Promise((res, rej) => {
|
||||
onetimeCallbacks[c].push(res)
|
||||
})
|
||||
return result
|
||||
}
|
||||
|
||||
|
||||
// use this function to add a listener to a certain type of message
|
||||
export const useOnVSCodeMessage = <C extends Command>(messageType: C, fn: (e: MessageToSidebar & { type: C }) => void) => {
|
||||
useEffect(() => {
|
||||
const mType = messageType
|
||||
const callbackId: string = uuidv4();
|
||||
// @ts-ignore
|
||||
callbacks[mType][callbackId] = fn;
|
||||
return () => { delete callbacks[mType][callbackId] }
|
||||
}, [messageType, fn])
|
||||
}
|
||||
|
||||
|
||||
|
||||
// this function gets called whenever sidebar receives a message - it should only mount once
|
||||
export const onMessageFromVSCode = (m: MessageToSidebar) => {
|
||||
// resolve all promises for this message type
|
||||
for (let res of onetimeCallbacks[m.type]) {
|
||||
res(m)
|
||||
onetimeCallbacks[m.type].splice(0) // clear the array
|
||||
}
|
||||
// call the listener for this message type
|
||||
for (let res of Object.values(callbacks[m.type])) {
|
||||
res(m)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
type AcquireVsCodeApiType = () => {
|
||||
postMessage(message: MessageFromSidebar): void;
|
||||
// setState(state: any): void; // getState and setState are made obsolete by us using { retainContextWhenHidden: true }
|
||||
// getState(): any;
|
||||
};
|
||||
|
||||
// VS Code exposes the function acquireVsCodeApi() to us, this variable makes sure it only gets called once
|
||||
let vsCodeApi: ReturnType<AcquireVsCodeApiType> | undefined;
|
||||
|
||||
export function getVSCodeAPI(): ReturnType<AcquireVsCodeApiType> {
|
||||
if (vsCodeApi)
|
||||
return vsCodeApi;
|
||||
|
||||
try {
|
||||
// @ts-expect-error
|
||||
// eslint-disable-next-line no-undef
|
||||
vsCodeApi = acquireVsCodeApi();
|
||||
return vsCodeApi!;
|
||||
} catch (error) {
|
||||
console.error('Failed to acquire VS Code API:', error);
|
||||
throw new Error('This script must be run in a VS Code webview context');
|
||||
}
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
import * as React from "react"
|
||||
import { useEffect } from "react"
|
||||
import * as ReactDOM from "react-dom/client"
|
||||
import Sidebar from "./Sidebar"
|
||||
import { CtrlK } from "./CtrlK"
|
||||
import { ThreadsProvider } from "./contextForThreads"
|
||||
import { ConfigProvider } from "./contextForConfig"
|
||||
import { MessageToSidebar } from "../common/shared_types"
|
||||
import { awaitVSCodeResponse, getVSCodeAPI, onMessageFromVSCode } from "./getVscodeApi"
|
||||
import { identifyUser, initPosthog } from "./metrics/posthog"
|
||||
|
||||
if (typeof document === "undefined") {
|
||||
console.log("index.tsx error: document was undefined")
|
||||
}
|
||||
|
||||
|
||||
const CommonEffects = () => {
|
||||
// initialize posthog
|
||||
useEffect(() => {
|
||||
initPosthog()
|
||||
}, [])
|
||||
|
||||
// when we get the deviceid, identify the user
|
||||
useEffect(() => {
|
||||
getVSCodeAPI().postMessage({ type: 'getDeviceId' });
|
||||
awaitVSCodeResponse('deviceId').then((m => {
|
||||
identifyUser(m.deviceId)
|
||||
}))
|
||||
}, [])
|
||||
|
||||
// Receive messages from the VSCode extension
|
||||
useEffect(() => {
|
||||
const listener = (event: MessageEvent) => {
|
||||
const m = event.data as MessageToSidebar;
|
||||
onMessageFromVSCode(m)
|
||||
}
|
||||
window.addEventListener('message', listener);
|
||||
return () => window.removeEventListener('message', listener)
|
||||
}, [])
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
(() => {
|
||||
// mount the sidebar on the id="root" element
|
||||
const rootElement = document.getElementById("root")!
|
||||
console.log("Void root Element:", rootElement)
|
||||
|
||||
const sidebar = (<>
|
||||
<CommonEffects />
|
||||
|
||||
<ThreadsProvider>
|
||||
<ConfigProvider>
|
||||
<Sidebar />
|
||||
</ConfigProvider>
|
||||
</ThreadsProvider>
|
||||
|
||||
<ConfigProvider>
|
||||
<CtrlK />
|
||||
</ConfigProvider>
|
||||
|
||||
</>)
|
||||
const root = ReactDOM.createRoot(rootElement)
|
||||
root.render(sidebar)
|
||||
})();
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
import posthog from 'posthog-js'
|
||||
|
||||
|
||||
export const identifyUser = (id: string) => {
|
||||
posthog.identify(id)
|
||||
}
|
||||
|
||||
export const captureEvent = (eventId: string, properties: object) => {
|
||||
posthog.capture(eventId, properties)
|
||||
}
|
||||
|
||||
export const initPosthog = () => {
|
||||
// We send absolutely no code to the server. We only track usage metrics like button clicks, etc. This might change and we might eventually add an opt-in or opt-out.
|
||||
posthog.init('phc_UanIdujHiLp55BkUTjB1AuBXcasVkdqRwgnwRlWESH2',
|
||||
{
|
||||
api_host: 'https://us.i.posthog.com',
|
||||
person_profiles: 'identified_only' // we only track events from identified users. We identify them in Sidebar
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
import * as assert from 'assert';
|
||||
|
||||
// You can import and use all API from the 'vscode' module
|
||||
// as well as import your extension to test it
|
||||
import * as vscode from 'vscode';
|
||||
// import * as myExtension from '../../extension';
|
||||
|
||||
suite('Extension Test Suite', () => {
|
||||
vscode.window.showInformationMessage('Start all tests.');
|
||||
|
||||
test('Sample test', () => {
|
||||
assert.strictEqual(-1, [1, 2, 3].indexOf(5));
|
||||
assert.strictEqual(-1, [1, 2, 3].indexOf(0));
|
||||
});
|
||||
});
|
||||
@@ -1,31 +0,0 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
|
||||
// inject user's vscode theme colors: https://code.visualstudio.com/api/extension-guides/webview#theming-webview-content
|
||||
module.exports = {
|
||||
content: ["./src/sidebar/**/*.{html,js,ts,jsx,tsx}"],
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
vscode: {
|
||||
"sidebar-bg": "var(--vscode-sideBar-background)",
|
||||
"editor-bg": "var(--vscode-editor-background)",
|
||||
"editor-fg": "var(--vscode-editor-foreground)",
|
||||
"input-bg": "var(--vscode-input-background)",
|
||||
"input-fg": "var(--vscode-input-foreground)",
|
||||
"input-border": "var(--vscode-input-border)",
|
||||
"button-fg": "var(--vscode-button-foreground)",
|
||||
"button-bg": "var(--vscode-button-background)",
|
||||
"button-hoverBg": "var(--vscode-button-hoverBackground)",
|
||||
"button-secondary-fg": "var(--vscode-button-secondaryForeground)",
|
||||
"button-secondary-bg": "var(--vscode-button-secondaryBackground)",
|
||||
"button-secondary-hoverBg": "var(--vscode-button-secondaryHoverBackground)",
|
||||
"dropdown-bg": "var(--vscode-settings-dropdownBackground)",
|
||||
"dropdown-foreground": "var(--vscode-settings-dropdownForeground)",
|
||||
"dropdown-border": "var(--vscode-settings-dropdownBorder)",
|
||||
"focus-border": "var(--vscode-focusBorder)",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [],
|
||||
};
|
||||
@@ -1,30 +0,0 @@
|
||||
{
|
||||
"include": [
|
||||
"src/**/*"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
],
|
||||
"compilerOptions": {
|
||||
"jsx": "react",
|
||||
"module": "Node16",
|
||||
"target": "ES6",
|
||||
"outDir": "out",
|
||||
"types": [
|
||||
"node",
|
||||
"mocha",
|
||||
],
|
||||
"lib": [
|
||||
"dom",
|
||||
"es6",
|
||||
"dom.iterable",
|
||||
],
|
||||
"sourceMap": true,
|
||||
"rootDir": "src",
|
||||
"strict": true, /* enable all strict type-checking options */
|
||||
/* Additional Checks */
|
||||
// "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
|
||||
// "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
|
||||
// "noUnusedParameters": true, /* Report errors on unused parameters. */
|
||||
}
|
||||
}
|
||||
Generated
+3035
-445
File diff suppressed because it is too large
Load Diff
+25
-1
@@ -9,6 +9,7 @@
|
||||
"main": "./out/main",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"buildreact": "cd ./src/vs/workbench/contrib/void/browser/react/ && node build.js && cd ../../../../../../../",
|
||||
"test": "echo Please run any of the test scripts from the scripts folder.",
|
||||
"test-browser": "npx playwright install && node test/unit/browser/index.js",
|
||||
"test-browser-amd": "npx playwright install && node test/unit/browser/index.amd.js",
|
||||
@@ -72,9 +73,13 @@
|
||||
"update-build-ts-version": "npm install typescript@next && tsc -p ./build/tsconfig.build.json"
|
||||
},
|
||||
"dependencies": {
|
||||
"@anthropic-ai/sdk": "^0.32.1",
|
||||
"@google/generative-ai": "^0.21.0",
|
||||
"@microsoft/1ds-core-js": "^3.2.13",
|
||||
"@microsoft/1ds-post-js": "^3.2.13",
|
||||
"@parcel/watcher": "2.1.0",
|
||||
"@rrweb/record": "^2.0.0-alpha.17",
|
||||
"@rrweb/types": "^2.0.0-alpha.17",
|
||||
"@vscode/deviceid": "^0.1.1",
|
||||
"@vscode/iconv-lite-umd": "0.7.0",
|
||||
"@vscode/policy-watcher": "^1.1.4",
|
||||
@@ -85,6 +90,7 @@
|
||||
"@vscode/sudo-prompt": "9.3.1",
|
||||
"@vscode/tree-sitter-wasm": "^0.0.4",
|
||||
"@vscode/vscode-languagedetection": "1.0.21",
|
||||
"@vscode/webview-ui-toolkit": "^1.4.0",
|
||||
"@vscode/windows-mutex": "^0.5.0",
|
||||
"@vscode/windows-process-tree": "^0.6.0",
|
||||
"@vscode/windows-registry": "^1.1.0",
|
||||
@@ -96,16 +102,25 @@
|
||||
"@xterm/addon-webgl": "^0.19.0-beta.64",
|
||||
"@xterm/headless": "^5.6.0-beta.64",
|
||||
"@xterm/xterm": "^5.6.0-beta.64",
|
||||
"diff": "^7.0.0",
|
||||
"groq-sdk": "^0.9.0",
|
||||
"http-proxy-agent": "^7.0.0",
|
||||
"https-proxy-agent": "^7.0.2",
|
||||
"jschardet": "3.1.3",
|
||||
"kerberos": "2.1.1",
|
||||
"lucide-react": "^0.460.0",
|
||||
"minimist": "^1.2.6",
|
||||
"native-is-elevated": "0.7.0",
|
||||
"native-keymap": "^3.3.5",
|
||||
"native-watchdog": "^1.4.1",
|
||||
"node-pty": "1.1.0-beta21",
|
||||
"ollama": "^0.5.11",
|
||||
"open": "^8.4.2",
|
||||
"openai": "^4.76.1",
|
||||
"posthog-node": "^4.3.1",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-syntax-highlighter": "^15.6.1",
|
||||
"tas-client-umd": "0.2.0",
|
||||
"v8-inspect-profiler": "^0.1.1",
|
||||
"vscode-oniguruma": "1.7.0",
|
||||
@@ -119,12 +134,17 @@
|
||||
"@swc/core": "1.3.62",
|
||||
"@types/cookie": "^0.3.3",
|
||||
"@types/debug": "^4.1.5",
|
||||
"@types/diff": "^6.0.0",
|
||||
"@types/eslint": "^9.6.1",
|
||||
"@types/gulp-svgmin": "^1.2.1",
|
||||
"@types/http-proxy-agent": "^2.0.1",
|
||||
"@types/kerberos": "^1.1.2",
|
||||
"@types/minimist": "^1.2.1",
|
||||
"@types/mocha": "^9.1.1",
|
||||
"@types/node": "20.x",
|
||||
"@types/react": "^18.3.12",
|
||||
"@types/react-dom": "^18.3.1",
|
||||
"@types/react-syntax-highlighter": "^15.5.13",
|
||||
"@types/sinon": "^10.0.2",
|
||||
"@types/sinon-test": "^2.4.2",
|
||||
"@types/trusted-types": "^1.0.6",
|
||||
@@ -190,6 +210,7 @@
|
||||
"istanbul-lib-source-maps": "^4.0.1",
|
||||
"istanbul-reports": "^3.1.5",
|
||||
"lazy.js": "^0.4.2",
|
||||
"marked": "^15.0.0",
|
||||
"merge-options": "^1.0.1",
|
||||
"mime": "^1.4.1",
|
||||
"minimatch": "^3.0.4",
|
||||
@@ -207,16 +228,19 @@
|
||||
"postcss-nesting": "^12.0.2",
|
||||
"pump": "^1.0.1",
|
||||
"rcedit": "^1.1.0",
|
||||
"rimraf": "^2.2.8",
|
||||
"rimraf": "^2.7.1",
|
||||
"scope-tailwind": "^1.0.5",
|
||||
"sinon": "^12.0.1",
|
||||
"sinon-test": "^3.1.3",
|
||||
"source-map": "0.6.1",
|
||||
"source-map-support": "^0.3.2",
|
||||
"style-loader": "^3.3.2",
|
||||
"tailwindcss": "^3.4.14",
|
||||
"ts-loader": "^9.4.2",
|
||||
"ts-node": "^10.9.1",
|
||||
"tsec": "0.2.7",
|
||||
"tslib": "^2.6.3",
|
||||
"tsup": "^8.3.5",
|
||||
"typescript": "^5.7.0-dev.20240903",
|
||||
"util": "^0.12.4",
|
||||
"webpack": "^5.94.0",
|
||||
|
||||
@@ -31,6 +31,10 @@
|
||||
"nodejsRepository": "https://nodejs.org",
|
||||
"urlProtocol": "code-oss",
|
||||
"webviewContentExternalBaseUrlTemplate": "https://{{uuid}}.vscode-cdn.net/insider/ef65ac1ba57f57f2a3961bfe94aa20481caca4c6/out/vs/workbench/contrib/webview/browser/pre/",
|
||||
"extensionsGallery": {
|
||||
"serviceUrl": "https://open-vsx.org/vscode/gallery",
|
||||
"itemUrl": "https://open-vsx.org/vscode/item"
|
||||
},
|
||||
"builtInExtensions": [
|
||||
{
|
||||
"name": "ms-vscode.js-debug-companion",
|
||||
|
||||
+1
-1
@@ -123,7 +123,7 @@ protocol.registerSchemesAsPrivileged([
|
||||
},
|
||||
{
|
||||
scheme: 'vscode-file',
|
||||
privileges: { secure: true, standard: true, supportFetchAPI: true, corsEnabled: true, codeCache: true }
|
||||
privileges: { secure: true, standard: true, supportFetchAPI: true, corsEnabled: true, codeCache: true, }
|
||||
}
|
||||
]);
|
||||
|
||||
|
||||
+6
-1
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"extends": "./tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
// "jsx": "react", // <-- Void added this
|
||||
"esModuleInterop": true,
|
||||
"removeComments": false,
|
||||
"preserveConstEnums": true,
|
||||
@@ -51,6 +52,10 @@
|
||||
"./typings",
|
||||
"./vs/**/*.ts",
|
||||
"vscode-dts/vscode.proposed.*.d.ts",
|
||||
"vscode-dts/vscode.d.ts"
|
||||
"vscode-dts/vscode.d.ts",
|
||||
|
||||
// Void added these:
|
||||
// "./vs/workbench/contrib/void/browser/**.tsx",
|
||||
// "./vs/**/*.d.mts",
|
||||
]
|
||||
}
|
||||
|
||||
@@ -121,6 +121,10 @@ import { normalizeNFC } from '../../base/common/normalization.js';
|
||||
import { ICSSDevelopmentService, CSSDevelopmentService } from '../../platform/cssDev/node/cssDevService.js';
|
||||
import { ExtensionSignatureVerificationService, IExtensionSignatureVerificationService } from '../../platform/extensionManagement/node/extensionSignatureVerificationService.js';
|
||||
|
||||
import { LLMMessageChannel } from '../../platform/void/electron-main/llmMessageChannel.js';
|
||||
import { IMetricsService } from '../../platform/void/common/metricsService.js';
|
||||
import { MetricsMainService } from '../../platform/void/electron-main/metricsMainService.js';
|
||||
|
||||
/**
|
||||
* The main VS Code application. There will only ever be one instance,
|
||||
* even if the user starts many instances (e.g. from the command line).
|
||||
@@ -148,7 +152,7 @@ export class CodeApplication extends Disposable {
|
||||
@IStateService private readonly stateService: IStateService,
|
||||
@IFileService private readonly fileService: IFileService,
|
||||
@IProductService private readonly productService: IProductService,
|
||||
@IUserDataProfilesMainService private readonly userDataProfilesMainService: IUserDataProfilesMainService
|
||||
@IUserDataProfilesMainService private readonly userDataProfilesMainService: IUserDataProfilesMainService,
|
||||
) {
|
||||
super();
|
||||
|
||||
@@ -508,6 +512,16 @@ export class CodeApplication extends Disposable {
|
||||
});
|
||||
|
||||
//#endregion
|
||||
|
||||
// //#region Void IPC
|
||||
// validatedIpcMain.handle('vscode:sendLLMMessage', async (event, data) => {
|
||||
// try {
|
||||
// await this.sendLLMMessage(data);
|
||||
// } catch (error) {
|
||||
// console.error('Error sending LLM message:', error);
|
||||
// }
|
||||
// });
|
||||
// //#endregion
|
||||
}
|
||||
|
||||
private onUnexpectedError(error: Error): void {
|
||||
@@ -1091,6 +1105,9 @@ export class CodeApplication extends Disposable {
|
||||
services.set(ITelemetryService, NullTelemetryService);
|
||||
}
|
||||
|
||||
// Void main process services (required for services with a channel for comm between browser and electron-main (node))
|
||||
services.set(IMetricsService, new SyncDescriptor(MetricsMainService, undefined, false));
|
||||
|
||||
// Default Extensions Profile Init
|
||||
services.set(IExtensionsProfileScannerService, new SyncDescriptor(ExtensionsProfileScannerService, undefined, true));
|
||||
services.set(IExtensionsScannerService, new SyncDescriptor(ExtensionsScannerService, undefined, true));
|
||||
@@ -1225,6 +1242,12 @@ export class CodeApplication extends Disposable {
|
||||
mainProcessElectronServer.registerChannel('logger', loggerChannel);
|
||||
sharedProcessClient.then(client => client.registerChannel('logger', loggerChannel));
|
||||
|
||||
// Void - use loggerChannel as reference
|
||||
const metricsChannel = ProxyChannel.fromService(accessor.get(IMetricsService), disposables);
|
||||
mainProcessElectronServer.registerChannel('void-channel-metrics', metricsChannel);
|
||||
const llmMessageChannel = new LLMMessageChannel(accessor.get(IMetricsService));
|
||||
mainProcessElectronServer.registerChannel('void-channel-llmMessageService', llmMessageChannel);
|
||||
|
||||
// Extension Host Debug Broadcasting
|
||||
const electronExtensionHostDebugBroadcastChannel = new ElectronExtensionHostDebugBroadcastChannel(accessor.get(IWindowsMainService));
|
||||
mainProcessElectronServer.registerChannel('extensionhostdebugservice', electronExtensionHostDebugBroadcastChannel);
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
|
||||
import { Event } from '../../../base/common/event.js';
|
||||
import { ICodeEditor, IDiffEditor } from '../editorBrowser.js';
|
||||
import { IDecorationRenderOptions } from '../../common/editorCommon.js';
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
import { Disposable } from '../../../../base/common/lifecycle.js';
|
||||
import { registerSingleton, InstantiationType } from '../../../../platform/instantiation/common/extensions.js';
|
||||
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
|
||||
import { IModelDeltaDecoration } from '../../../common/model.js';
|
||||
import { ICodeEditor, IViewZone } from '../../editorBrowser.js';
|
||||
import { IRange } from '../../../common/core/range.js';
|
||||
import { EditorOption } from '../../../common/config/editorOptions.js';
|
||||
|
||||
export interface IInlineDiffService {
|
||||
readonly _serviceBrand: undefined;
|
||||
addDiff(editor: ICodeEditor, originalText: string, modifiedRange: IRange): void;
|
||||
removeDiffs(editor: ICodeEditor): void;
|
||||
}
|
||||
|
||||
export const IInlineDiffService = createDecorator<IInlineDiffService>('inlineDiffServiceOld');
|
||||
|
||||
class InlineDiffService extends Disposable implements IInlineDiffService {
|
||||
private readonly _diffDecorations = new Map<ICodeEditor, string[]>();
|
||||
private readonly _diffZones = new Map<ICodeEditor, string[]>();
|
||||
_serviceBrand: undefined;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
initStream() {
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
public addDiff: IInlineDiffService['addDiff'] = (editor, originalText, modifiedRange) => {
|
||||
// Clear existing diffs
|
||||
this.removeDiffs(editor);
|
||||
|
||||
// green decoration and gutter decoration
|
||||
const greenDecoration: IModelDeltaDecoration[] = [{
|
||||
range: modifiedRange,
|
||||
options: {
|
||||
className: 'line-insert', // .monaco-editor .line-insert
|
||||
description: 'line-insert',
|
||||
isWholeLine: true,
|
||||
minimap: {
|
||||
color: { id: 'minimapGutter.addedBackground' },
|
||||
position: 2
|
||||
},
|
||||
overviewRuler: {
|
||||
color: { id: 'editorOverviewRuler.addedForeground' },
|
||||
position: 7
|
||||
}
|
||||
}
|
||||
}];
|
||||
|
||||
this._diffDecorations.set(editor, editor.deltaDecorations([], greenDecoration));
|
||||
|
||||
// red in a view zone
|
||||
editor.changeViewZones(accessor => {
|
||||
// Get the editor's font info
|
||||
const fontInfo = editor.getOption(EditorOption.fontInfo);
|
||||
|
||||
const domNode = document.createElement('div');
|
||||
// domNode.className = 'monaco-editor view-zones line-delete monaco-mouse-cursor-text';
|
||||
domNode.style.fontSize = `${fontInfo.fontSize}px`;
|
||||
domNode.style.fontFamily = fontInfo.fontFamily;
|
||||
domNode.style.lineHeight = `${fontInfo.lineHeight}px`;
|
||||
|
||||
// div
|
||||
const lineContent = document.createElement('div');
|
||||
// lineContent.className = 'view-line'; // .monaco-editor .inline-deleted-text
|
||||
|
||||
// span
|
||||
const contentSpan = document.createElement('span');
|
||||
|
||||
// span
|
||||
const codeSpan = document.createElement('span');
|
||||
// codeSpan.className = 'mtk1'; // char-delete
|
||||
codeSpan.textContent = originalText;
|
||||
|
||||
// Mount
|
||||
contentSpan.appendChild(codeSpan);
|
||||
lineContent.appendChild(contentSpan);
|
||||
domNode.appendChild(lineContent);
|
||||
|
||||
const viewZone: IViewZone = {
|
||||
afterLineNumber: modifiedRange.startLineNumber - 1,
|
||||
heightInLines: originalText.split('\n').length + 1,
|
||||
domNode: domNode,
|
||||
suppressMouseDown: true,
|
||||
marginDomNode: this.createGutterElement()
|
||||
};
|
||||
|
||||
const zoneId = accessor.addZone(viewZone);
|
||||
// editor.layout();
|
||||
this._diffZones.set(editor, [zoneId]);
|
||||
});
|
||||
}
|
||||
|
||||
// gutter is the thing to the left
|
||||
private createGutterElement(): HTMLElement {
|
||||
const gutterDiv = document.createElement('div');
|
||||
gutterDiv.className = 'inline-diff-gutter';
|
||||
|
||||
const minusDiv = document.createElement('div');
|
||||
minusDiv.className = 'inline-diff-deleted-gutter';
|
||||
// minusDiv.textContent = '-';
|
||||
|
||||
gutterDiv.appendChild(minusDiv);
|
||||
return gutterDiv;
|
||||
}
|
||||
|
||||
public removeDiffs(editor: ICodeEditor): void {
|
||||
const decorationIds = this._diffDecorations.get(editor) || [];
|
||||
editor.deltaDecorations(decorationIds, []);
|
||||
this._diffDecorations.delete(editor);
|
||||
|
||||
editor.changeViewZones(accessor => {
|
||||
const zoneIds = this._diffZones.get(editor) || [];
|
||||
zoneIds.forEach(id => accessor.removeZone(id));
|
||||
});
|
||||
this._diffZones.delete(editor);
|
||||
}
|
||||
|
||||
override dispose(): void {
|
||||
super.dispose();
|
||||
this._diffDecorations.clear();
|
||||
this._diffZones.clear();
|
||||
}
|
||||
}
|
||||
|
||||
registerSingleton(IInlineDiffService, InlineDiffService, InstantiationType.Eager);
|
||||
@@ -425,3 +425,5 @@
|
||||
margin: 0 4px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
|
||||
|
||||
// ---------- common ----------
|
||||
|
||||
// llmMessage
|
||||
import '../common/llmMessageService.js'
|
||||
|
||||
// voidSettings
|
||||
import '../common/voidSettingsService.js'
|
||||
|
||||
// refreshModel
|
||||
import '../common/refreshModelService.js'
|
||||
|
||||
// metrics
|
||||
import '../common/metricsService.js'
|
||||
@@ -0,0 +1,146 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Glass Devtools, Inc. All rights reserved.
|
||||
* Void Editor additions licensed under the AGPL 3.0 License.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { EventLLMMessageOnTextParams, EventLLMMessageOnErrorParams, EventLLMMessageOnFinalMessageParams, ServiceSendLLMMessageParams, MainLLMMessageParams, MainLLMMessageAbortParams, ServiceOllamaListParams, EventOllamaListOnSuccessParams, EventOllamaListOnErrorParams, MainOllamaListParams } from './llmMessageTypes.js';
|
||||
import { IChannel } from '../../../base/parts/ipc/common/ipc.js';
|
||||
import { IMainProcessService } from '../../ipc/common/mainProcessService.js';
|
||||
import { InstantiationType, registerSingleton } from '../../instantiation/common/extensions.js';
|
||||
import { generateUuid } from '../../../base/common/uuid.js';
|
||||
import { createDecorator } from '../../instantiation/common/instantiation.js';
|
||||
import { Event } from '../../../base/common/event.js';
|
||||
import { Disposable } from '../../../base/common/lifecycle.js';
|
||||
import { IVoidSettingsService } from './voidSettingsService.js';
|
||||
// import { INotificationService } from '../../notification/common/notification.js';
|
||||
|
||||
// calls channel to implement features
|
||||
export const ILLMMessageService = createDecorator<ILLMMessageService>('llmMessageService');
|
||||
|
||||
export interface ILLMMessageService {
|
||||
readonly _serviceBrand: undefined;
|
||||
sendLLMMessage: (params: ServiceSendLLMMessageParams) => string | null;
|
||||
abort: (requestId: string) => void;
|
||||
ollamaList: (params: ServiceOllamaListParams) => void;
|
||||
}
|
||||
|
||||
export class LLMMessageService extends Disposable implements ILLMMessageService {
|
||||
|
||||
readonly _serviceBrand: undefined;
|
||||
private readonly channel: IChannel // LLMMessageChannel
|
||||
|
||||
// llmMessage
|
||||
private readonly onTextHooks_llm: { [eventId: string]: ((params: EventLLMMessageOnTextParams) => void) } = {}
|
||||
private readonly onFinalMessageHooks_llm: { [eventId: string]: ((params: EventLLMMessageOnFinalMessageParams) => void) } = {}
|
||||
private readonly onErrorHooks_llm: { [eventId: string]: ((params: EventLLMMessageOnErrorParams) => void) } = {}
|
||||
|
||||
|
||||
// ollamaList
|
||||
private readonly onSuccess_ollama: { [eventId: string]: ((params: EventOllamaListOnSuccessParams) => void) } = {}
|
||||
private readonly onError_ollama: { [eventId: string]: ((params: EventOllamaListOnErrorParams) => void) } = {}
|
||||
|
||||
|
||||
constructor(
|
||||
@IMainProcessService private readonly mainProcessService: IMainProcessService, // used as a renderer (only usable on client side)
|
||||
@IVoidSettingsService private readonly voidSettingsService: IVoidSettingsService,
|
||||
// @INotificationService private readonly notificationService: INotificationService,
|
||||
) {
|
||||
super()
|
||||
|
||||
// const service = ProxyChannel.toService<LLMMessageChannel>(mainProcessService.getChannel('void-channel-sendLLMMessage')); // lets you call it like a service
|
||||
this.channel = this.mainProcessService.getChannel('void-channel-llmMessageService')
|
||||
|
||||
// .listen sets up an IPC channel and takes a few ms, so we set up listeners immediately and add hooks to them instead
|
||||
// llm
|
||||
this._register((this.channel.listen('onText_llm') satisfies Event<EventLLMMessageOnTextParams>)(e => {
|
||||
this.onTextHooks_llm[e.requestId]?.(e)
|
||||
}))
|
||||
this._register((this.channel.listen('onFinalMessage_llm') satisfies Event<EventLLMMessageOnFinalMessageParams>)(e => {
|
||||
this.onFinalMessageHooks_llm[e.requestId]?.(e)
|
||||
this._onRequestIdDone(e.requestId)
|
||||
}))
|
||||
this._register((this.channel.listen('onError_llm') satisfies Event<EventLLMMessageOnErrorParams>)(e => {
|
||||
console.log('Error in LLMMessageService:', JSON.stringify(e))
|
||||
this.onErrorHooks_llm[e.requestId]?.(e)
|
||||
this._onRequestIdDone(e.requestId)
|
||||
}))
|
||||
// ollama
|
||||
this._register((this.channel.listen('onSuccess_ollama') satisfies Event<EventOllamaListOnSuccessParams>)(e => {
|
||||
this.onSuccess_ollama[e.requestId]?.(e)
|
||||
}))
|
||||
this._register((this.channel.listen('onError_ollama') satisfies Event<EventOllamaListOnErrorParams>)(e => {
|
||||
this.onError_ollama[e.requestId]?.(e)
|
||||
}))
|
||||
|
||||
}
|
||||
|
||||
sendLLMMessage(params: ServiceSendLLMMessageParams) {
|
||||
const { onText, onFinalMessage, onError, ...proxyParams } = params;
|
||||
const { featureName } = proxyParams
|
||||
|
||||
// end early if no provider
|
||||
const modelSelection = this.voidSettingsService.state.modelSelectionOfFeature[featureName]
|
||||
if (modelSelection === null) {
|
||||
onError({ message: 'Please add a Provider in Settings!', fullError: null })
|
||||
return null
|
||||
}
|
||||
const { providerName, modelName } = modelSelection
|
||||
|
||||
// add state for request id
|
||||
const requestId_ = generateUuid();
|
||||
this.onTextHooks_llm[requestId_] = onText
|
||||
this.onFinalMessageHooks_llm[requestId_] = onFinalMessage
|
||||
this.onErrorHooks_llm[requestId_] = onError
|
||||
|
||||
const { settingsOfProvider } = this.voidSettingsService.state
|
||||
|
||||
// params will be stripped of all its functions over the IPC channel
|
||||
this.channel.call('sendLLMMessage', {
|
||||
...proxyParams,
|
||||
requestId: requestId_,
|
||||
providerName,
|
||||
modelName,
|
||||
settingsOfProvider,
|
||||
} satisfies MainLLMMessageParams);
|
||||
|
||||
return requestId_
|
||||
}
|
||||
|
||||
|
||||
abort(requestId: string) {
|
||||
this.channel.call('abort', { requestId } satisfies MainLLMMessageAbortParams);
|
||||
this._onRequestIdDone(requestId)
|
||||
}
|
||||
|
||||
|
||||
ollamaList = (params: ServiceOllamaListParams) => {
|
||||
const { onSuccess, onError, ...proxyParams } = params
|
||||
|
||||
const { settingsOfProvider } = this.voidSettingsService.state
|
||||
|
||||
// add state for request id
|
||||
const requestId_ = generateUuid();
|
||||
this.onSuccess_ollama[requestId_] = onSuccess
|
||||
this.onError_ollama[requestId_] = onError
|
||||
|
||||
this.channel.call('ollamaList', {
|
||||
...proxyParams,
|
||||
settingsOfProvider,
|
||||
requestId: requestId_,
|
||||
} satisfies MainOllamaListParams)
|
||||
}
|
||||
|
||||
|
||||
|
||||
_onRequestIdDone(requestId: string) {
|
||||
delete this.onTextHooks_llm[requestId]
|
||||
delete this.onFinalMessageHooks_llm[requestId]
|
||||
delete this.onErrorHooks_llm[requestId]
|
||||
|
||||
delete this.onSuccess_ollama[requestId]
|
||||
delete this.onError_ollama[requestId]
|
||||
}
|
||||
}
|
||||
|
||||
registerSingleton(ILLMMessageService, LLMMessageService, InstantiationType.Eager);
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Glass Devtools, Inc. All rights reserved.
|
||||
* Void Editor additions licensed under the AGPL 3.0 License.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { IRange } from '../../../editor/common/core/range'
|
||||
import { ProviderName, SettingsOfProvider } from './voidSettingsTypes.js'
|
||||
|
||||
|
||||
export type OnText = (p: { newText: string, fullText: string }) => void
|
||||
export type OnFinalMessage = (p: { fullText: string }) => void
|
||||
export type OnError = (p: { message: string, fullError: Error | null }) => void
|
||||
export type AbortRef = { current: (() => void) | null }
|
||||
|
||||
export type LLMMessage = {
|
||||
role: 'system' | 'user' | 'assistant';
|
||||
content: string;
|
||||
}
|
||||
|
||||
export type LLMFeatureSelection = {
|
||||
featureName: 'Ctrl+K',
|
||||
range: IRange
|
||||
} | {
|
||||
featureName: 'Ctrl+L',
|
||||
} | {
|
||||
featureName: 'Autocomplete',
|
||||
range: IRange
|
||||
}
|
||||
|
||||
// params to the true sendLLMMessage function
|
||||
export type LLMMMessageParams = {
|
||||
onText: OnText;
|
||||
onFinalMessage: OnFinalMessage;
|
||||
onError: OnError;
|
||||
abortRef: AbortRef;
|
||||
|
||||
messages: LLMMessage[];
|
||||
|
||||
logging: {
|
||||
loggingName: string,
|
||||
};
|
||||
providerName: ProviderName;
|
||||
modelName: string;
|
||||
settingsOfProvider: SettingsOfProvider;
|
||||
}
|
||||
|
||||
export type ServiceSendLLMMessageParams = {
|
||||
onText: OnText;
|
||||
onFinalMessage: OnFinalMessage;
|
||||
onError: OnError;
|
||||
|
||||
messages: LLMMessage[];
|
||||
|
||||
logging: {
|
||||
loggingName: string,
|
||||
};
|
||||
} & LLMFeatureSelection
|
||||
|
||||
// can't send functions across a proxy, use listeners instead
|
||||
export type BlockedMainLLMMessageParams = 'onText' | 'onFinalMessage' | 'onError' | 'abortRef'
|
||||
|
||||
export type MainLLMMessageParams = Omit<LLMMMessageParams, BlockedMainLLMMessageParams> & { requestId: string }
|
||||
export type MainLLMMessageAbortParams = { requestId: string }
|
||||
|
||||
export type EventLLMMessageOnTextParams = Parameters<OnText>[0] & { requestId: string }
|
||||
export type EventLLMMessageOnFinalMessageParams = Parameters<OnFinalMessage>[0] & { requestId: string }
|
||||
export type EventLLMMessageOnErrorParams = Parameters<OnError>[0] & { requestId: string }
|
||||
|
||||
export type _InternalSendLLMMessageFnType = (params: {
|
||||
messages: LLMMessage[];
|
||||
onText: OnText;
|
||||
onFinalMessage: OnFinalMessage;
|
||||
onError: OnError;
|
||||
settingsOfProvider: SettingsOfProvider;
|
||||
providerName: ProviderName;
|
||||
modelName: string;
|
||||
|
||||
_setAborter: (aborter: () => void) => void;
|
||||
}) => void
|
||||
|
||||
// service -> main -> internal -> event (back to main)
|
||||
// (browser)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// These are from 'ollama' SDK
|
||||
interface ModelDetails {
|
||||
parent_model: string;
|
||||
format: string;
|
||||
family: string;
|
||||
families: string[];
|
||||
parameter_size: string;
|
||||
quantization_level: string;
|
||||
}
|
||||
|
||||
export type ModelResponse = {
|
||||
name: string;
|
||||
modified_at: Date;
|
||||
size: number;
|
||||
digest: string;
|
||||
details: ModelDetails;
|
||||
expires_at: Date;
|
||||
size_vram: number;
|
||||
}
|
||||
|
||||
|
||||
// params to the true list fn
|
||||
export type OllamaListParams = {
|
||||
settingsOfProvider: SettingsOfProvider;
|
||||
onSuccess: (param: { models: ModelResponse[] }) => void;
|
||||
onError: (param: { error: string }) => void;
|
||||
}
|
||||
|
||||
export type ServiceOllamaListParams = {
|
||||
onSuccess: (param: { models: ModelResponse[] }) => void;
|
||||
onError: (param: { error: any }) => void;
|
||||
}
|
||||
|
||||
type BlockedMainOllamaListParams = 'onSuccess' | 'onError'
|
||||
export type MainOllamaListParams = Omit<OllamaListParams, BlockedMainOllamaListParams> & { requestId: string }
|
||||
|
||||
export type EventOllamaListOnSuccessParams = Parameters<OllamaListParams['onSuccess']>[0] & { requestId: string }
|
||||
export type EventOllamaListOnErrorParams = Parameters<OllamaListParams['onError']>[0] & { requestId: string }
|
||||
|
||||
|
||||
|
||||
export type _InternalOllamaListFnType = (params: OllamaListParams) => void
|
||||
@@ -0,0 +1,39 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Glass Devtools, Inc. All rights reserved.
|
||||
* Void Editor additions licensed under the AGPL 3.0 License.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { createDecorator } from '../../instantiation/common/instantiation.js';
|
||||
import { ProxyChannel } from '../../../base/parts/ipc/common/ipc.js';
|
||||
import { IMainProcessService } from '../../ipc/common/mainProcessService.js';
|
||||
import { InstantiationType, registerSingleton } from '../../instantiation/common/extensions.js';
|
||||
|
||||
export interface IMetricsService {
|
||||
readonly _serviceBrand: undefined;
|
||||
capture(event: string, params: Record<string, any>): void;
|
||||
}
|
||||
|
||||
export const IMetricsService = createDecorator<IMetricsService>('metricsService');
|
||||
|
||||
|
||||
// implemented by calling channel
|
||||
export class MetricsService implements IMetricsService {
|
||||
|
||||
readonly _serviceBrand: undefined;
|
||||
private readonly metricsService: IMetricsService;
|
||||
|
||||
constructor(
|
||||
@IMainProcessService mainProcessService: IMainProcessService // (only usable on client side)
|
||||
) {
|
||||
this.metricsService = ProxyChannel.toService<IMetricsService>(mainProcessService.getChannel('void-channel-metrics'));
|
||||
}
|
||||
|
||||
// call capture on the channel
|
||||
capture(...params: Parameters<IMetricsService['capture']>) {
|
||||
this.metricsService.capture(...params);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
registerSingleton(IMetricsService, MetricsService, InstantiationType.Eager);
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Glass Devtools, Inc. All rights reserved.
|
||||
* Void Editor additions licensed under the AGPL 3.0 License.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { createDecorator } from '../../instantiation/common/instantiation.js';
|
||||
import { InstantiationType, registerSingleton } from '../../instantiation/common/extensions.js';
|
||||
import { IVoidSettingsService } from './voidSettingsService.js';
|
||||
import { ILLMMessageService } from './llmMessageService.js';
|
||||
import { Emitter, Event } from '../../../base/common/event.js';
|
||||
import { Disposable } from '../../../base/common/lifecycle.js';
|
||||
|
||||
|
||||
export type RefreshModelState = 'done' | 'loading'
|
||||
|
||||
// element-wise equals
|
||||
function eq<T>(a: T[], b: T[]): boolean {
|
||||
if (a.length !== b.length) return false
|
||||
for (let i = 0; i < a.length; i++) {
|
||||
if (a[i] !== b[i]) return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
export interface IRefreshModelService {
|
||||
readonly _serviceBrand: undefined;
|
||||
refreshOllamaModels(): void;
|
||||
onDidChangeState: Event<void>;
|
||||
state: RefreshModelState;
|
||||
}
|
||||
|
||||
export const IRefreshModelService = createDecorator<IRefreshModelService>('RefreshModelService');
|
||||
|
||||
export class RefreshModelService extends Disposable implements IRefreshModelService {
|
||||
|
||||
readonly _serviceBrand: undefined;
|
||||
|
||||
private readonly _onDidChangeState = new Emitter<void>();
|
||||
readonly onDidChangeState: Event<void> = this._onDidChangeState.event; // this is primarily for use in react, so react can listen + update on state changes
|
||||
|
||||
constructor(
|
||||
@IVoidSettingsService private readonly voidSettingsService: IVoidSettingsService,
|
||||
@ILLMMessageService private readonly llmMessageService: ILLMMessageService,
|
||||
) {
|
||||
super()
|
||||
|
||||
// on mount, refresh ollama models
|
||||
this.refreshOllamaModels()
|
||||
|
||||
// every time ollama.enabled changes, refresh ollama models, like useEffect
|
||||
let relevantVals = () => [this.voidSettingsService.state.settingsOfProvider.ollama.enabled, this.voidSettingsService.state.settingsOfProvider.ollama.endpoint]
|
||||
let prevVals = relevantVals()
|
||||
this._register(
|
||||
this.voidSettingsService.onDidChangeState(() => { // we might want to debounce this
|
||||
const newVals = relevantVals()
|
||||
if (!eq(prevVals, newVals)) {
|
||||
this.refreshOllamaModels()
|
||||
prevVals = newVals
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
state: RefreshModelState = 'done'
|
||||
|
||||
private _timeoutId: NodeJS.Timeout | null = null
|
||||
private _cancelTimeout = () => {
|
||||
if (this._timeoutId) {
|
||||
clearTimeout(this._timeoutId)
|
||||
this._timeoutId = null
|
||||
}
|
||||
}
|
||||
async refreshOllamaModels() {
|
||||
// cancel any existing poll
|
||||
this._cancelTimeout()
|
||||
|
||||
// if ollama is disabled, obivously done
|
||||
if (!this.voidSettingsService.state.settingsOfProvider.ollama.enabled) {
|
||||
this._setState('done')
|
||||
return
|
||||
}
|
||||
|
||||
// start loading models
|
||||
this._setState('loading')
|
||||
|
||||
this.llmMessageService.ollamaList({
|
||||
onSuccess: ({ models }) => {
|
||||
this.voidSettingsService.setDefaultModels('ollama', models.map(model => model.name))
|
||||
this._setState('done')
|
||||
},
|
||||
onError: ({ error }) => {
|
||||
// poll
|
||||
console.log('retrying ollamaList:', error)
|
||||
this._timeoutId = setTimeout(() => this.refreshOllamaModels(), 5000)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private _setState(state: RefreshModelState) {
|
||||
this.state = state
|
||||
this._onDidChangeState.fire()
|
||||
}
|
||||
}
|
||||
|
||||
registerSingleton(IRefreshModelService, RefreshModelService, InstantiationType.Eager);
|
||||
|
||||
@@ -0,0 +1,238 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 { deepClone } from '../../../base/common/objects.js';
|
||||
import { IEncryptionService } from '../../encryption/common/encryptionService.js';
|
||||
import { registerSingleton, InstantiationType } from '../../instantiation/common/extensions.js';
|
||||
import { createDecorator } from '../../instantiation/common/instantiation.js';
|
||||
import { IStorageService, StorageScope, StorageTarget } from '../../storage/common/storage.js';
|
||||
import { defaultSettingsOfProvider, FeatureName, ProviderName, ModelSelectionOfFeature, SettingsOfProvider, SettingName, providerNames, ModelSelection, modelSelectionsEqual, featureNames, modelInfoOfDefaultNames, ModelInfo } from './voidSettingsTypes.js';
|
||||
|
||||
|
||||
const STORAGE_KEY = 'void.voidSettingsI'
|
||||
|
||||
type SetSettingOfProviderFn = <S extends SettingName>(
|
||||
providerName: ProviderName,
|
||||
settingName: S,
|
||||
newVal: SettingsOfProvider[ProviderName][S extends keyof SettingsOfProvider[ProviderName] ? S : never],
|
||||
) => Promise<void>;
|
||||
|
||||
type SetModelSelectionOfFeature = <K extends FeatureName>(
|
||||
featureName: K,
|
||||
newVal: ModelSelectionOfFeature[K],
|
||||
options?: { doNotApplyEffects?: true }
|
||||
) => Promise<void>;
|
||||
|
||||
|
||||
|
||||
export type ModelOption = { text: string, value: ModelSelection }
|
||||
|
||||
|
||||
|
||||
export type VoidSettingsState = {
|
||||
readonly settingsOfProvider: SettingsOfProvider; // optionsOfProvider
|
||||
readonly modelSelectionOfFeature: ModelSelectionOfFeature; // stateOfFeature
|
||||
|
||||
readonly _modelOptions: ModelOption[] // computed based on the two above items
|
||||
}
|
||||
|
||||
|
||||
|
||||
export interface IVoidSettingsService {
|
||||
readonly _serviceBrand: undefined;
|
||||
readonly state: VoidSettingsState;
|
||||
onDidChangeState: Event<void>;
|
||||
setSettingOfProvider: SetSettingOfProviderFn;
|
||||
setModelSelectionOfFeature: SetModelSelectionOfFeature;
|
||||
|
||||
setDefaultModels(providerName: ProviderName, modelNames: string[]): void;
|
||||
toggleModelHidden(providerName: ProviderName, modelName: string): void;
|
||||
addModel(providerName: ProviderName, modelName: string): void;
|
||||
deleteModel(providerName: ProviderName, modelName: string): boolean;
|
||||
}
|
||||
|
||||
|
||||
let _computeModelOptions = (settingsOfProvider: SettingsOfProvider) => {
|
||||
let modelOptions: ModelOption[] = []
|
||||
for (const providerName of providerNames) {
|
||||
const providerConfig = settingsOfProvider[providerName]
|
||||
if (!providerConfig.enabled) continue // if disabled, don't display model options
|
||||
for (const { modelName, isHidden } of providerConfig.models) {
|
||||
if (isHidden) continue
|
||||
modelOptions.push({ text: `${modelName} (${providerName})`, value: { providerName, modelName } })
|
||||
}
|
||||
}
|
||||
return modelOptions
|
||||
}
|
||||
|
||||
|
||||
const defaultState = () => {
|
||||
const d: VoidSettingsState = {
|
||||
settingsOfProvider: deepClone(defaultSettingsOfProvider),
|
||||
modelSelectionOfFeature: { 'Ctrl+L': null, 'Ctrl+K': null, 'Autocomplete': null },
|
||||
_modelOptions: _computeModelOptions(defaultSettingsOfProvider), // computed
|
||||
}
|
||||
return d
|
||||
}
|
||||
|
||||
|
||||
export const IVoidSettingsService = createDecorator<IVoidSettingsService>('VoidSettingsService');
|
||||
class VoidSettingsService extends Disposable implements IVoidSettingsService {
|
||||
_serviceBrand: undefined;
|
||||
|
||||
private readonly _onDidChangeState = new Emitter<void>();
|
||||
readonly onDidChangeState: Event<void> = this._onDidChangeState.event; // this is primarily for use in react, so react can listen + update on state changes
|
||||
|
||||
state: VoidSettingsState;
|
||||
|
||||
constructor(
|
||||
@IStorageService private readonly _storageService: IStorageService,
|
||||
@IEncryptionService private readonly _encryptionService: IEncryptionService,
|
||||
// could have used this, but it's clearer the way it is (+ slightly different eg StorageTarget.USER)
|
||||
// @ISecretStorageService private readonly _secretStorageService: ISecretStorageService,
|
||||
) {
|
||||
super()
|
||||
|
||||
// at the start, we haven't read the partial config yet, but we need to set state to something
|
||||
this.state = defaultState()
|
||||
|
||||
// read and update the actual state immediately
|
||||
this._readState().then(s => {
|
||||
this.state = s
|
||||
this._onDidChangeState.fire()
|
||||
})
|
||||
}
|
||||
|
||||
private async _readState(): Promise<VoidSettingsState> {
|
||||
const encryptedState = this._storageService.get(STORAGE_KEY, StorageScope.APPLICATION)
|
||||
|
||||
if (!encryptedState)
|
||||
return defaultState()
|
||||
|
||||
const stateStr = await this._encryptionService.decrypt(encryptedState)
|
||||
return JSON.parse(stateStr)
|
||||
}
|
||||
|
||||
|
||||
private async _storeState() {
|
||||
const state = this.state
|
||||
const encryptedState = await this._encryptionService.encrypt(JSON.stringify(state))
|
||||
this._storageService.store(STORAGE_KEY, encryptedState, StorageScope.APPLICATION, StorageTarget.USER);
|
||||
}
|
||||
|
||||
setSettingOfProvider: SetSettingOfProviderFn = async (providerName, settingName, newVal) => {
|
||||
|
||||
const newModelSelectionOfFeature = this.state.modelSelectionOfFeature
|
||||
|
||||
const newSettingsOfProvider = {
|
||||
...this.state.settingsOfProvider,
|
||||
[providerName]: {
|
||||
...this.state.settingsOfProvider[providerName],
|
||||
[settingName]: newVal,
|
||||
}
|
||||
}
|
||||
|
||||
// if changed models or enabled a provider, recompute models list
|
||||
const modelsListChanged = settingName === 'models' || settingName === 'enabled'
|
||||
const newModelsList = modelsListChanged ? _computeModelOptions(newSettingsOfProvider) : this.state._modelOptions
|
||||
|
||||
const newState: VoidSettingsState = {
|
||||
modelSelectionOfFeature: newModelSelectionOfFeature,
|
||||
settingsOfProvider: newSettingsOfProvider,
|
||||
_modelOptions: newModelsList,
|
||||
}
|
||||
|
||||
// this must go above this.setanythingelse()
|
||||
this.state = newState
|
||||
|
||||
// if the user-selected model is no longer in the list, update the selection for each feature that needs it to something relevant (the 0th model available, or null)
|
||||
if (modelsListChanged) {
|
||||
for (const featureName of featureNames) {
|
||||
|
||||
const currentSelection = newModelSelectionOfFeature[featureName]
|
||||
const selnIdx = currentSelection === null ? -1 : newModelsList.findIndex(m => modelSelectionsEqual(m.value, currentSelection))
|
||||
|
||||
if (selnIdx === -1) {
|
||||
if (newModelsList.length !== 0)
|
||||
this.setModelSelectionOfFeature(featureName, newModelsList[0].value, { doNotApplyEffects: true })
|
||||
else
|
||||
this.setModelSelectionOfFeature(featureName, null, { doNotApplyEffects: true })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await this._storeState()
|
||||
this._onDidChangeState.fire()
|
||||
}
|
||||
|
||||
|
||||
setModelSelectionOfFeature: SetModelSelectionOfFeature = async (featureName, newVal, options) => {
|
||||
const newState: VoidSettingsState = {
|
||||
...this.state,
|
||||
modelSelectionOfFeature: {
|
||||
...this.state.modelSelectionOfFeature,
|
||||
[featureName]: newVal
|
||||
}
|
||||
}
|
||||
|
||||
this.state = newState
|
||||
|
||||
if (options?.doNotApplyEffects)
|
||||
return
|
||||
|
||||
await this._storeState()
|
||||
this._onDidChangeState.fire()
|
||||
}
|
||||
|
||||
|
||||
|
||||
setDefaultModels(providerName: ProviderName, newDefaultModelNames: string[]) {
|
||||
const { models } = this.state.settingsOfProvider[providerName]
|
||||
const newDefaultModels = modelInfoOfDefaultNames(newDefaultModelNames)
|
||||
const newModels = [
|
||||
...newDefaultModels,
|
||||
...models.filter(m => !m.isDefault), // keep any non-default models
|
||||
]
|
||||
this.setSettingOfProvider(providerName, 'models', newModels)
|
||||
}
|
||||
toggleModelHidden(providerName: ProviderName, modelName: string) {
|
||||
const { models } = this.state.settingsOfProvider[providerName]
|
||||
const modelIdx = models.findIndex(m => m.modelName === modelName)
|
||||
if (modelIdx === -1) return
|
||||
const newModels: ModelInfo[] = [
|
||||
...models.slice(0, modelIdx),
|
||||
{ ...models[modelIdx], isHidden: !models[modelIdx].isHidden },
|
||||
...models.slice(modelIdx + 1, Infinity)
|
||||
]
|
||||
this.setSettingOfProvider(providerName, 'models', newModels)
|
||||
}
|
||||
addModel(providerName: ProviderName, modelName: string) {
|
||||
const { models } = this.state.settingsOfProvider[providerName]
|
||||
const existingIdx = models.findIndex(m => m.modelName === modelName)
|
||||
if (existingIdx !== -1) return // if exists, do nothing
|
||||
const newModels = [
|
||||
...models,
|
||||
{ modelName, isDefault: false, isHidden: false }
|
||||
]
|
||||
this.setSettingOfProvider(providerName, 'models', newModels)
|
||||
}
|
||||
deleteModel(providerName: ProviderName, modelName: string): boolean {
|
||||
const { models } = this.state.settingsOfProvider[providerName]
|
||||
const delIdx = models.findIndex(m => m.modelName === modelName)
|
||||
if (delIdx === -1) return false
|
||||
const newModels = [
|
||||
...models.slice(0, delIdx), // delete the idx
|
||||
...models.slice(delIdx + 1, Infinity)
|
||||
]
|
||||
this.setSettingOfProvider(providerName, 'models', newModels)
|
||||
return true
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
registerSingleton(IVoidSettingsService, VoidSettingsService, InstantiationType.Eager);
|
||||
@@ -0,0 +1,308 @@
|
||||
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Glass Devtools, Inc. All rights reserved.
|
||||
* Void Editor additions licensed under the AGPL 3.0 License.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
|
||||
|
||||
|
||||
export type ModelInfo = {
|
||||
modelName: string,
|
||||
isDefault: boolean, // whether or not it's a default for its provider
|
||||
isHidden: boolean, // whether or not the user is hiding it
|
||||
}
|
||||
|
||||
|
||||
export const modelInfoOfDefaultNames = (modelNames: string[]): ModelInfo[] => {
|
||||
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 }))
|
||||
}
|
||||
|
||||
// https://docs.anthropic.com/en/docs/about-claude/models
|
||||
export const defaultAnthropicModels = modelInfoOfDefaultNames([
|
||||
'claude-3-5-sonnet-20241022',
|
||||
'claude-3-5-haiku-20241022',
|
||||
'claude-3-opus-20240229',
|
||||
'claude-3-sonnet-20240229',
|
||||
// 'claude-3-haiku-20240307',
|
||||
])
|
||||
|
||||
|
||||
// https://platform.openai.com/docs/models/gp
|
||||
export const defaultOpenAIModels = modelInfoOfDefaultNames([
|
||||
'o1-preview',
|
||||
'o1-mini',
|
||||
'gpt-4o',
|
||||
'gpt-4o-mini',
|
||||
// 'gpt-4o-2024-05-13',
|
||||
// 'gpt-4o-2024-08-06',
|
||||
// 'gpt-4o-mini-2024-07-18',
|
||||
// 'gpt-4-turbo',
|
||||
// 'gpt-4-turbo-2024-04-09',
|
||||
// 'gpt-4-turbo-preview',
|
||||
// 'gpt-4-0125-preview',
|
||||
// 'gpt-4-1106-preview',
|
||||
// 'gpt-4',
|
||||
// 'gpt-4-0613',
|
||||
// 'gpt-3.5-turbo-0125',
|
||||
// 'gpt-3.5-turbo',
|
||||
// 'gpt-3.5-turbo-1106',
|
||||
])
|
||||
|
||||
|
||||
|
||||
// https://console.groq.com/docs/models
|
||||
export const defaultGroqModels = modelInfoOfDefaultNames([
|
||||
"mixtral-8x7b-32768",
|
||||
"llama2-70b-4096",
|
||||
"gemma-7b-it"
|
||||
])
|
||||
|
||||
|
||||
export const defaultGeminiModels = modelInfoOfDefaultNames([
|
||||
'gemini-1.5-flash',
|
||||
'gemini-1.5-pro',
|
||||
'gemini-1.5-flash-8b',
|
||||
'gemini-1.0-pro'
|
||||
])
|
||||
|
||||
|
||||
|
||||
// export const parseMaxTokensStr = (maxTokensStr: string) => {
|
||||
// // parse the string but only if the full string is a valid number, eg parseInt('100abc') should return NaN
|
||||
// const int = isNaN(Number(maxTokensStr)) ? undefined : parseInt(maxTokensStr)
|
||||
// if (Number.isNaN(int))
|
||||
// return undefined
|
||||
// return int
|
||||
// }
|
||||
|
||||
|
||||
|
||||
|
||||
export const anthropicMaxPossibleTokens = (modelName: string) => {
|
||||
if (modelName === 'claude-3-5-sonnet-20241022'
|
||||
|| modelName === 'claude-3-5-haiku-20241022')
|
||||
return 8192
|
||||
if (modelName === 'claude-3-opus-20240229'
|
||||
|| modelName === 'claude-3-sonnet-20240229'
|
||||
|| modelName === 'claude-3-haiku-20240307')
|
||||
return 4096
|
||||
return 1024 // return a reasonably small number if they're using a different model
|
||||
}
|
||||
|
||||
|
||||
type UnionOfKeys<T> = T extends T ? keyof T : never;
|
||||
|
||||
|
||||
|
||||
export const customProviderSettingsDefaults = {
|
||||
anthropic: {
|
||||
apiKey: '',
|
||||
},
|
||||
openAI: {
|
||||
apiKey: '',
|
||||
},
|
||||
ollama: {
|
||||
endpoint: 'http://127.0.0.1:11434',
|
||||
},
|
||||
openRouter: {
|
||||
apiKey: '',
|
||||
},
|
||||
openAICompatible: {
|
||||
apiKey: '',
|
||||
endpoint: '',
|
||||
},
|
||||
gemini: {
|
||||
apiKey: '',
|
||||
},
|
||||
groq: {
|
||||
apiKey: ''
|
||||
}
|
||||
} as const
|
||||
|
||||
export type ProviderName = keyof typeof customProviderSettingsDefaults
|
||||
export const providerNames = Object.keys(customProviderSettingsDefaults) as ProviderName[]
|
||||
|
||||
|
||||
type CustomSettingName = UnionOfKeys<typeof customProviderSettingsDefaults[ProviderName]>
|
||||
|
||||
type CustomProviderSettings<providerName extends ProviderName> = {
|
||||
[k in CustomSettingName]: k extends keyof typeof customProviderSettingsDefaults[providerName] ? string : undefined
|
||||
}
|
||||
|
||||
type CommonProviderSettings = {
|
||||
enabled: boolean,
|
||||
models: ModelInfo[], // if null, user can type in any string as a model
|
||||
}
|
||||
|
||||
type SettingsForProvider<providerName extends ProviderName> = CustomProviderSettings<providerName> & CommonProviderSettings
|
||||
|
||||
// part of state
|
||||
export type SettingsOfProvider = {
|
||||
[providerName in ProviderName]: SettingsForProvider<providerName>
|
||||
}
|
||||
|
||||
|
||||
export type SettingName = keyof SettingsForProvider<ProviderName>
|
||||
|
||||
|
||||
|
||||
export const titleOfProviderName = (providerName: ProviderName) => {
|
||||
if (providerName === 'anthropic')
|
||||
return 'Anthropic'
|
||||
else if (providerName === 'openAI')
|
||||
return 'OpenAI'
|
||||
else if (providerName === 'ollama')
|
||||
return 'Ollama'
|
||||
else if (providerName === 'openRouter')
|
||||
return 'OpenRouter'
|
||||
else if (providerName === 'openAICompatible')
|
||||
return 'OpenAI-Compatible'
|
||||
else if (providerName === 'gemini')
|
||||
return 'Gemini'
|
||||
else if (providerName === 'groq')
|
||||
return 'Groq'
|
||||
|
||||
throw new Error(`descOfProviderName: Unknown provider name: "${providerName}"`)
|
||||
}
|
||||
|
||||
type DisplayInfo = {
|
||||
title: string,
|
||||
placeholder: string,
|
||||
}
|
||||
export const displayInfoOfSettingName = (providerName: ProviderName, settingName: SettingName): DisplayInfo => {
|
||||
if (settingName === 'apiKey') {
|
||||
return {
|
||||
title: 'API Key',
|
||||
placeholder: providerName === 'anthropic' ? 'sk-ant-key...' : // sk-ant-api03-key
|
||||
providerName === 'openAI' ? 'sk-proj-key...' :
|
||||
providerName === 'openRouter' ? 'sk-or-key...' : // sk-or-v1-key
|
||||
providerName === 'gemini' ? 'key...' :
|
||||
providerName === 'groq' ? 'gsk_key...' :
|
||||
providerName === 'openAICompatible' ? 'sk-key...' :
|
||||
'(never)',
|
||||
}
|
||||
}
|
||||
else if (settingName === 'endpoint') {
|
||||
return {
|
||||
title: providerName === 'ollama' ? 'Your Ollama endpoint' :
|
||||
providerName === 'openAICompatible' ? 'baseURL' // (do not include /chat/completions)
|
||||
: '(never)',
|
||||
placeholder: providerName === 'ollama' ? customProviderSettingsDefaults.ollama.endpoint
|
||||
: providerName === 'openAICompatible' ? 'https://my-website.com/v1'
|
||||
: '(never)',
|
||||
}
|
||||
}
|
||||
else if (settingName === 'enabled') {
|
||||
return {
|
||||
title: '(never)',
|
||||
placeholder: '(never)',
|
||||
}
|
||||
}
|
||||
else if (settingName === 'models') {
|
||||
return {
|
||||
title: '(never)',
|
||||
placeholder: '(never)',
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(`displayInfo: Unknown setting name: "${settingName}"`)
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
const defaultCustomSettings: Record<CustomSettingName, undefined> = {
|
||||
apiKey: undefined,
|
||||
endpoint: undefined,
|
||||
}
|
||||
|
||||
export const voidInitModelOptions = {
|
||||
anthropic: {
|
||||
models: defaultAnthropicModels,
|
||||
},
|
||||
openAI: {
|
||||
models: defaultOpenAIModels,
|
||||
},
|
||||
ollama: {
|
||||
models: [],
|
||||
},
|
||||
openRouter: {
|
||||
models: [], // any string
|
||||
},
|
||||
openAICompatible: {
|
||||
models: [],
|
||||
},
|
||||
gemini: {
|
||||
models: defaultGeminiModels,
|
||||
},
|
||||
groq: {
|
||||
models: defaultGroqModels,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
// used when waiting and for a type reference
|
||||
export const defaultSettingsOfProvider: SettingsOfProvider = {
|
||||
anthropic: {
|
||||
...defaultCustomSettings,
|
||||
...customProviderSettingsDefaults.anthropic,
|
||||
...voidInitModelOptions.anthropic,
|
||||
enabled: false,
|
||||
},
|
||||
openAI: {
|
||||
...defaultCustomSettings,
|
||||
...customProviderSettingsDefaults.openAI,
|
||||
...voidInitModelOptions.openAI,
|
||||
enabled: false,
|
||||
},
|
||||
gemini: {
|
||||
...defaultCustomSettings,
|
||||
...customProviderSettingsDefaults.gemini,
|
||||
...voidInitModelOptions.gemini,
|
||||
enabled: false,
|
||||
},
|
||||
groq: {
|
||||
...defaultCustomSettings,
|
||||
...customProviderSettingsDefaults.groq,
|
||||
...voidInitModelOptions.groq,
|
||||
enabled: false,
|
||||
},
|
||||
ollama: {
|
||||
...defaultCustomSettings,
|
||||
...customProviderSettingsDefaults.ollama,
|
||||
...voidInitModelOptions.ollama,
|
||||
enabled: false,
|
||||
},
|
||||
openRouter: {
|
||||
...defaultCustomSettings,
|
||||
...customProviderSettingsDefaults.openRouter,
|
||||
...voidInitModelOptions.openRouter,
|
||||
enabled: false,
|
||||
},
|
||||
openAICompatible: {
|
||||
...defaultCustomSettings,
|
||||
...customProviderSettingsDefaults.openAICompatible,
|
||||
...voidInitModelOptions.openAICompatible,
|
||||
enabled: false,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
export type ModelSelection = { providerName: ProviderName, modelName: string }
|
||||
|
||||
export const modelSelectionsEqual = (m1: ModelSelection, m2: ModelSelection) => {
|
||||
return m1.modelName === m2.modelName && m1.providerName === m2.providerName
|
||||
}
|
||||
|
||||
// this is a state
|
||||
export type ModelSelectionOfFeature = {
|
||||
'Ctrl+L': ModelSelection | null,
|
||||
'Ctrl+K': ModelSelection | null,
|
||||
'Autocomplete': ModelSelection | null,
|
||||
}
|
||||
export type FeatureName = keyof ModelSelectionOfFeature
|
||||
export const featureNames = ['Ctrl+L', 'Ctrl+K', 'Autocomplete'] as const
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Glass Devtools, Inc. All rights reserved.
|
||||
* Void Editor additions licensed under the AGPL 3.0 License.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import Anthropic from '@anthropic-ai/sdk';
|
||||
import { _InternalSendLLMMessageFnType } from '../../common/llmMessageTypes.js';
|
||||
import { anthropicMaxPossibleTokens } from '../../common/voidSettingsTypes.js';
|
||||
|
||||
// Anthropic
|
||||
type LLMMessageAnthropic = {
|
||||
role: 'user' | 'assistant';
|
||||
content: string;
|
||||
}
|
||||
export const sendAnthropicMsg: _InternalSendLLMMessageFnType = ({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter }) => {
|
||||
|
||||
const thisConfig = settingsOfProvider.anthropic
|
||||
|
||||
const maxTokens = anthropicMaxPossibleTokens(modelName)
|
||||
if (maxTokens === undefined) {
|
||||
onError({ message: `Please set a value for Max Tokens.`, fullError: null })
|
||||
return
|
||||
}
|
||||
|
||||
const anthropic = new Anthropic({ apiKey: thisConfig.apiKey, dangerouslyAllowBrowser: true });
|
||||
|
||||
// find system messages and concatenate them
|
||||
const systemMessage = messages
|
||||
.filter(msg => msg.role === 'system')
|
||||
.map(msg => msg.content)
|
||||
.join('\n');
|
||||
|
||||
// remove system messages for Anthropic
|
||||
const anthropicMessages = messages.filter(msg => msg.role !== 'system') as LLMMessageAnthropic[]
|
||||
|
||||
|
||||
|
||||
const stream = anthropic.messages.stream({
|
||||
system: systemMessage,
|
||||
messages: anthropicMessages,
|
||||
model: modelName,
|
||||
max_tokens: maxTokens,
|
||||
});
|
||||
|
||||
|
||||
// when receive text
|
||||
stream.on('text', (newText, fullText) => {
|
||||
onText({ newText, fullText })
|
||||
})
|
||||
|
||||
// when we get the final message on this stream (or when error/fail)
|
||||
stream.on('finalMessage', (claude_response) => {
|
||||
// stringify the response's content
|
||||
const content = claude_response.content.map(c => c.type === 'text' ? c.text : c.type).join('\n');
|
||||
onFinalMessage({ fullText: content })
|
||||
})
|
||||
|
||||
stream.on('error', (error) => {
|
||||
// the most common error will be invalid API key (401), so we handle this with a nice message
|
||||
if (error instanceof Anthropic.APIError && error.status === 401) {
|
||||
onError({ message: 'Invalid API key.', fullError: error })
|
||||
}
|
||||
else {
|
||||
onError({ message: error + '', fullError: error }) // anthropic errors can be stringified nicely like this
|
||||
}
|
||||
})
|
||||
|
||||
// TODO need to test this to make sure it works, it might throw an error
|
||||
_setAborter(() => stream.controller.abort())
|
||||
|
||||
};
|
||||
@@ -0,0 +1,53 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Glass Devtools, Inc. All rights reserved.
|
||||
* Void Editor additions licensed under the AGPL 3.0 License.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Content, GoogleGenerativeAI, GoogleGenerativeAIFetchError } from '@google/generative-ai';
|
||||
import { _InternalSendLLMMessageFnType } from '../../common/llmMessageTypes.js';
|
||||
|
||||
// Gemini
|
||||
export const sendGeminiMsg: _InternalSendLLMMessageFnType = async ({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter }) => {
|
||||
|
||||
let fullText = ''
|
||||
|
||||
const thisConfig = settingsOfProvider.gemini
|
||||
|
||||
const genAI = new GoogleGenerativeAI(thisConfig.apiKey);
|
||||
const model = genAI.getGenerativeModel({ model: modelName });
|
||||
|
||||
// remove system messages that get sent to Gemini
|
||||
// str of all system messages
|
||||
const systemMessage = messages
|
||||
.filter(msg => msg.role === 'system')
|
||||
.map(msg => msg.content)
|
||||
.join('\n');
|
||||
|
||||
// Convert messages to Gemini format
|
||||
const geminiMessages: Content[] = messages
|
||||
.filter(msg => msg.role !== 'system')
|
||||
.map((msg, i) => ({
|
||||
parts: [{ text: msg.content }],
|
||||
role: msg.role === 'assistant' ? 'model' : 'user'
|
||||
}))
|
||||
|
||||
model.generateContentStream({ contents: geminiMessages, systemInstruction: systemMessage, })
|
||||
.then(async response => {
|
||||
_setAborter(() => response.stream.return(fullText))
|
||||
|
||||
for await (const chunk of response.stream) {
|
||||
const newText = chunk.text();
|
||||
fullText += newText;
|
||||
onText({ newText, fullText });
|
||||
}
|
||||
onFinalMessage({ fullText });
|
||||
})
|
||||
.catch((error) => {
|
||||
if (error instanceof GoogleGenerativeAIFetchError && error.status === 400) {
|
||||
onError({ message: 'Invalid API key.', fullError: null });
|
||||
}
|
||||
else {
|
||||
onError({ message: error + '', fullError: error });
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Glass Devtools, Inc. All rights reserved.
|
||||
* Void Editor additions licensed under the AGPL 3.0 License.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
// // Greptile
|
||||
// // https://docs.greptile.com/api-reference/query
|
||||
// // https://docs.greptile.com/quickstart#sample-response-streamed
|
||||
|
||||
// import { SendLLMMessageFnTypeInternal } from '../../common/llmMessageTypes.js';
|
||||
|
||||
// export const sendGreptileMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinalMessage, onError, settingsOfProvider, _setAborter }) => {
|
||||
|
||||
// let fullText = ''
|
||||
|
||||
// const thisConfig = settingsOfProvider.greptile
|
||||
|
||||
// fetch('https://api.greptile.com/v2/query', {
|
||||
// method: 'POST',
|
||||
// headers: {
|
||||
// 'Authorization': `Bearer ${thisConfig.apikey}`,
|
||||
// 'X-Github-Token': `${thisConfig.githubPAT}`,
|
||||
// 'Content-Type': `application/json`,
|
||||
// },
|
||||
// body: JSON.stringify({
|
||||
// messages,
|
||||
// stream: true,
|
||||
// repositories: [thisConfig.repoinfo],
|
||||
// }),
|
||||
// })
|
||||
// // this is {message}\n{message}\n{message}...\n
|
||||
// .then(async response => {
|
||||
// const text = await response.text()
|
||||
// console.log('got greptile', text)
|
||||
// return JSON.parse(`[${text.trim().split('\n').join(',')}]`)
|
||||
// })
|
||||
// // TODO make this actually stream, right now it just sends one message at the end
|
||||
// // TODO add _setAborter() when add streaming
|
||||
// .then(async responseArr => {
|
||||
|
||||
// for (const response of responseArr) {
|
||||
// const type: string = response['type']
|
||||
// const message = response['message']
|
||||
|
||||
// // when receive text
|
||||
// if (type === 'message') {
|
||||
// fullText += message
|
||||
// onText({ newText: message, fullText })
|
||||
// }
|
||||
// else if (type === 'sources') {
|
||||
// const { filepath, linestart: _, lineend: _2 } = message as { filepath: string; linestart: number | null; lineend: number | null }
|
||||
// fullText += filepath
|
||||
// onText({ newText: filepath, fullText })
|
||||
// }
|
||||
// // type: 'status' with an empty 'message' means last message
|
||||
// else if (type === 'status') {
|
||||
// if (!message) {
|
||||
// onFinalMessage({ fullText })
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// })
|
||||
// .catch(error => {
|
||||
// onError({ error })
|
||||
// });
|
||||
|
||||
// }
|
||||
@@ -0,0 +1,46 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Glass Devtools, Inc. All rights reserved.
|
||||
* Void Editor additions licensed under the AGPL 3.0 License.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import Groq from 'groq-sdk';
|
||||
import { _InternalSendLLMMessageFnType } from '../../common/llmMessageTypes.js';
|
||||
|
||||
// Groq
|
||||
export const sendGroqMsg: _InternalSendLLMMessageFnType = async ({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter }) => {
|
||||
let fullText = '';
|
||||
|
||||
const thisConfig = settingsOfProvider.groq
|
||||
|
||||
const groq = new Groq({
|
||||
apiKey: thisConfig.apiKey,
|
||||
dangerouslyAllowBrowser: true
|
||||
});
|
||||
|
||||
await groq.chat.completions
|
||||
.create({
|
||||
messages: messages,
|
||||
model: modelName,
|
||||
stream: true,
|
||||
temperature: 0.7,
|
||||
// max_tokens: parseMaxTokensStr(thisConfig.maxTokens),
|
||||
})
|
||||
.then(async response => {
|
||||
_setAborter(() => response.controller.abort())
|
||||
// when receive text
|
||||
for await (const chunk of response) {
|
||||
const newText = chunk.choices[0]?.delta?.content || '';
|
||||
if (newText) {
|
||||
fullText += newText;
|
||||
onText({ newText, fullText });
|
||||
}
|
||||
}
|
||||
|
||||
onFinalMessage({ fullText });
|
||||
})
|
||||
.catch(error => {
|
||||
onError({ message: error + '', fullError: error });
|
||||
})
|
||||
|
||||
|
||||
};
|
||||
@@ -0,0 +1,81 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Glass Devtools, Inc. All rights reserved.
|
||||
* Void Editor additions licensed under the AGPL 3.0 License.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Ollama } from 'ollama';
|
||||
import { _InternalOllamaListFnType, _InternalSendLLMMessageFnType, ModelResponse } from '../../common/llmMessageTypes.js';
|
||||
|
||||
export const ollamaList: _InternalOllamaListFnType = async ({ onSuccess: onSuccess_, onError: onError_, settingsOfProvider }) => {
|
||||
|
||||
const onSuccess = ({ models }: { models: ModelResponse[] }) => {
|
||||
onSuccess_({ models })
|
||||
}
|
||||
|
||||
const onError = ({ error }: { error: string }) => {
|
||||
onError_({ error })
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
const thisConfig = settingsOfProvider.ollama
|
||||
const ollama = new Ollama({ host: thisConfig.endpoint })
|
||||
ollama.list()
|
||||
.then((response) => {
|
||||
const { models } = response
|
||||
onSuccess({ models })
|
||||
})
|
||||
.catch((error) => {
|
||||
onError({ error: error + '' })
|
||||
})
|
||||
}
|
||||
catch (error) {
|
||||
onError({ error: error + '' })
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Ollama
|
||||
export const sendOllamaMsg: _InternalSendLLMMessageFnType = ({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter }) => {
|
||||
|
||||
const thisConfig = settingsOfProvider.ollama
|
||||
|
||||
let fullText = ''
|
||||
|
||||
const ollama = new Ollama({ host: thisConfig.endpoint })
|
||||
|
||||
ollama.chat({
|
||||
model: modelName,
|
||||
messages: messages,
|
||||
stream: true,
|
||||
// options: { num_predict: parseMaxTokensStr(thisConfig.maxTokens) } // this is max_tokens
|
||||
})
|
||||
.then(async stream => {
|
||||
_setAborter(() => stream.abort())
|
||||
// iterate through the stream
|
||||
for await (const chunk of stream) {
|
||||
const newText = chunk.message.content;
|
||||
fullText += newText;
|
||||
onText({ newText, fullText });
|
||||
}
|
||||
onFinalMessage({ fullText });
|
||||
|
||||
})
|
||||
// when error/fail
|
||||
.catch((error) => {
|
||||
// if (typeof error === 'object') {
|
||||
// const e = error.error as ErrorResponse['error']
|
||||
// if (e) {
|
||||
// const name = error.name ?? 'Error'
|
||||
// onError({ error: `${name}: ${e}` })
|
||||
// return;
|
||||
// }
|
||||
// }
|
||||
onError({ message: error + '', fullError: error })
|
||||
})
|
||||
|
||||
};
|
||||
|
||||
|
||||
|
||||
// ['codestral', 'qwen2.5-coder', 'qwen2.5-coder:0.5b', 'qwen2.5-coder:1.5b', 'qwen2.5-coder:3b', 'qwen2.5-coder:7b', 'qwen2.5-coder:14b', 'qwen2.5-coder:32b', 'codegemma', 'codegemma:2b', 'codegemma:7b', 'codellama', 'codellama:7b', 'codellama:13b', 'codellama:34b', 'codellama:70b', 'codellama:code', 'codellama:python', 'command-r', 'command-r:35b', 'command-r-plus', 'command-r-plus:104b', 'deepseek-coder-v2', 'deepseek-coder-v2:16b', 'deepseek-coder-v2:236b', 'falcon2', 'falcon2:11b', 'firefunction-v2', 'firefunction-v2:70b', 'gemma', 'gemma:2b', 'gemma:7b', 'gemma2', 'gemma2:2b', 'gemma2:9b', 'gemma2:27b', 'llama2', 'llama2:7b', 'llama2:13b', 'llama2:70b', 'llama3', 'llama3:8b', 'llama3:70b', 'llama3-chatqa', 'llama3-chatqa:8b', 'llama3-chatqa:70b', 'llama3-gradient', 'llama3-gradient:8b', 'llama3-gradient:70b', 'llama3.1', 'llama3.1:8b', 'llama3.1:70b', 'llama3.1:405b', 'llava', 'llava:7b', 'llava:13b', 'llava:34b', 'llava-llama3', 'llava-llama3:8b', 'llava-phi3', 'llava-phi3:3.8b', 'mistral', 'mistral:7b', 'mistral-large', 'mistral-large:123b', 'mistral-nemo', 'mistral-nemo:12b', 'mixtral', 'mixtral:8x7b', 'mixtral:8x22b', 'moondream', 'moondream:1.8b', 'openhermes', 'openhermes:v2.5', 'phi3', 'phi3:3.8b', 'phi3:14b', 'phi3.5', 'phi3.5:3.8b', 'qwen', 'qwen:7b', 'qwen:14b', 'qwen:32b', 'qwen:72b', 'qwen:110b', 'qwen2', 'qwen2:0.5b', 'qwen2:1.5b', 'qwen2:7b', 'qwen2:72b', 'smollm', 'smollm:135m', 'smollm:360m', 'smollm:1.7b',]
|
||||
@@ -0,0 +1,68 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Glass Devtools, Inc. All rights reserved.
|
||||
* Void Editor additions licensed under the AGPL 3.0 License.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import OpenAI from 'openai';
|
||||
import { _InternalSendLLMMessageFnType } from '../../common/llmMessageTypes.js';
|
||||
// import { parseMaxTokensStr } from './util.js';
|
||||
|
||||
|
||||
// OpenAI, OpenRouter, OpenAICompatible
|
||||
export const sendOpenAIMsg: _InternalSendLLMMessageFnType = ({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName }) => {
|
||||
|
||||
let fullText = ''
|
||||
|
||||
let openai: OpenAI
|
||||
let options: OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming
|
||||
|
||||
|
||||
if (providerName === 'openAI') {
|
||||
const thisConfig = settingsOfProvider.openAI
|
||||
openai = new OpenAI({ apiKey: thisConfig.apiKey, dangerouslyAllowBrowser: true });
|
||||
options = { model: modelName, messages: messages, stream: true, /*max_completion_tokens: parseMaxTokensStr(thisConfig.maxTokens)*/ }
|
||||
}
|
||||
else if (providerName === 'openRouter') {
|
||||
const thisConfig = settingsOfProvider.openRouter
|
||||
openai = new OpenAI({
|
||||
baseURL: 'https://openrouter.ai/api/v1', apiKey: thisConfig.apiKey, dangerouslyAllowBrowser: true,
|
||||
defaultHeaders: {
|
||||
'HTTP-Referer': 'https://voideditor.com', // Optional, for including your app on openrouter.ai rankings.
|
||||
'X-Title': 'Void Editor', // Optional. Shows in rankings on openrouter.ai.
|
||||
},
|
||||
});
|
||||
options = { model: modelName, messages: messages, stream: true, /*max_completion_tokens: parseMaxTokensStr(thisConfig.maxTokens)*/ }
|
||||
}
|
||||
else if (providerName === 'openAICompatible') {
|
||||
const thisConfig = settingsOfProvider.openAICompatible
|
||||
openai = new OpenAI({ baseURL: thisConfig.endpoint, apiKey: thisConfig.apiKey, dangerouslyAllowBrowser: true })
|
||||
options = { model: modelName, messages: messages, stream: true, /*max_completion_tokens: parseMaxTokensStr(thisConfig.maxTokens)*/ }
|
||||
}
|
||||
else {
|
||||
console.error(`sendOpenAIMsg: invalid providerName: ${providerName}`)
|
||||
throw new Error(`providerName was invalid: ${providerName}`)
|
||||
}
|
||||
|
||||
openai.chat.completions
|
||||
.create(options)
|
||||
.then(async response => {
|
||||
_setAborter(() => response.controller.abort())
|
||||
// when receive text
|
||||
for await (const chunk of response) {
|
||||
const newText = chunk.choices[0]?.delta?.content || '';
|
||||
fullText += newText;
|
||||
onText({ newText, fullText });
|
||||
}
|
||||
onFinalMessage({ fullText });
|
||||
})
|
||||
// when error/fail - this catches errors of both .create() and .then(for await)
|
||||
.catch(error => {
|
||||
if (error instanceof OpenAI.APIError && error.status === 401) {
|
||||
onError({ message: 'Invalid API key.', fullError: error });
|
||||
}
|
||||
else {
|
||||
onError({ message: error, fullError: error });
|
||||
}
|
||||
})
|
||||
|
||||
};
|
||||
@@ -0,0 +1,115 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Glass Devtools, Inc. All rights reserved.
|
||||
* Void Editor additions licensed under the AGPL 3.0 License.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { LLMMMessageParams, OnText, OnFinalMessage, OnError } from '../../common/llmMessageTypes.js';
|
||||
import { IMetricsService } from '../../common/metricsService.js';
|
||||
|
||||
import { sendAnthropicMsg } from './anthropic.js';
|
||||
import { sendOllamaMsg } from './ollama.js';
|
||||
import { sendOpenAIMsg } from './openai.js';
|
||||
import { sendGeminiMsg } from './gemini.js';
|
||||
import { sendGroqMsg } from './groq.js';
|
||||
|
||||
export const sendLLMMessage = ({
|
||||
messages,
|
||||
onText: onText_,
|
||||
onFinalMessage: onFinalMessage_,
|
||||
onError: onError_,
|
||||
abortRef: abortRef_,
|
||||
logging: { loggingName },
|
||||
settingsOfProvider,
|
||||
providerName,
|
||||
modelName,
|
||||
}: LLMMMessageParams,
|
||||
|
||||
metricsService: IMetricsService
|
||||
) => {
|
||||
|
||||
// trim message content (Anthropic and other providers give an error if there is trailing whitespace)
|
||||
messages = messages.map(m => ({ ...m, content: m.content.trim() }))
|
||||
|
||||
// only captures number of messages and message "shape", no actual code, instructions, prompts, etc
|
||||
const captureChatEvent = (eventId: string, extras?: object) => {
|
||||
metricsService.capture(eventId, {
|
||||
providerName,
|
||||
numMessages: messages?.length,
|
||||
messagesShape: messages?.map(msg => ({ role: msg.role, length: msg.content.length })),
|
||||
version: '2024-11-14',
|
||||
...extras,
|
||||
})
|
||||
}
|
||||
const submit_time = new Date()
|
||||
|
||||
let _fullTextSoFar = ''
|
||||
let _aborter: (() => void) | null = null
|
||||
let _setAborter = (fn: () => void) => { _aborter = fn }
|
||||
let _didAbort = false
|
||||
|
||||
const onText: OnText = ({ newText, fullText }) => {
|
||||
if (_didAbort) return
|
||||
onText_({ newText, fullText })
|
||||
_fullTextSoFar = fullText
|
||||
}
|
||||
|
||||
const onFinalMessage: OnFinalMessage = ({ fullText }) => {
|
||||
if (_didAbort) return
|
||||
captureChatEvent(`${loggingName} - Received Full Message`, { messageLength: fullText.length, duration: new Date().getMilliseconds() - submit_time.getMilliseconds() })
|
||||
onFinalMessage_({ fullText })
|
||||
}
|
||||
|
||||
const onError: OnError = ({ message: error, fullError }) => {
|
||||
if (_didAbort) return
|
||||
console.log("ERROR!!!!!", error)
|
||||
console.error('sendLLMMessage onError:', error)
|
||||
captureChatEvent(`${loggingName} - Error`, { error })
|
||||
onError_({ message: error, fullError })
|
||||
}
|
||||
|
||||
const onAbort = () => {
|
||||
captureChatEvent(`${loggingName} - Abort`, { messageLengthSoFar: _fullTextSoFar.length })
|
||||
try { _aborter?.() } // aborter sometimes automatically throws an error
|
||||
catch (e) { }
|
||||
_didAbort = true
|
||||
}
|
||||
abortRef_.current = onAbort
|
||||
|
||||
captureChatEvent(`${loggingName} - Sending Message`, { messageLength: messages[messages.length - 1]?.content.length })
|
||||
|
||||
try {
|
||||
switch (providerName) {
|
||||
case 'anthropic':
|
||||
sendAnthropicMsg({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName });
|
||||
break;
|
||||
case 'openAI':
|
||||
case 'openRouter':
|
||||
case 'openAICompatible':
|
||||
sendOpenAIMsg({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName });
|
||||
break;
|
||||
case 'gemini':
|
||||
sendGeminiMsg({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName });
|
||||
break;
|
||||
case 'ollama':
|
||||
sendOllamaMsg({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName });
|
||||
break;
|
||||
case 'groq':
|
||||
sendGroqMsg({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName });
|
||||
break;
|
||||
default:
|
||||
onError({ message: `Error: Void provider was "${providerName}", which is not recognized.`, fullError: null })
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
catch (error) {
|
||||
if (error instanceof Error) { onError({ message: error + '', fullError: error }) }
|
||||
else { onError({ message: `Unexpected Error in sendLLMMessage: ${error}`, fullError: error }); }
|
||||
// ; (_aborter as any)?.()
|
||||
// _didAbort = true
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Glass Devtools, Inc. All rights reserved.
|
||||
* Void Editor additions licensed under the AGPL 3.0 License.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
// registered in app.ts
|
||||
// code convention is to make a service responsible for this stuff, and not a channel, but having fewer files is simpler...
|
||||
|
||||
import { IServerChannel } from '../../../base/parts/ipc/common/ipc.js';
|
||||
import { Emitter, Event } from '../../../base/common/event.js';
|
||||
import { EventLLMMessageOnTextParams, EventLLMMessageOnErrorParams, EventLLMMessageOnFinalMessageParams, MainLLMMessageParams, AbortRef, LLMMMessageParams, MainLLMMessageAbortParams, MainOllamaListParams, OllamaListParams, EventOllamaListOnSuccessParams, EventOllamaListOnErrorParams } from '../common/llmMessageTypes.js';
|
||||
import { sendLLMMessage } from './llmMessage/sendLLMMessage.js'
|
||||
import { IMetricsService } from '../common/metricsService.js';
|
||||
import { ollamaList } from './llmMessage/ollama.js';
|
||||
|
||||
// NODE IMPLEMENTATION - calls actual sendLLMMessage() and returns listeners to it
|
||||
|
||||
export class LLMMessageChannel implements IServerChannel {
|
||||
// sendLLMMessage
|
||||
private readonly _onText_llm = new Emitter<EventLLMMessageOnTextParams>();
|
||||
private readonly _onFinalMessage_llm = new Emitter<EventLLMMessageOnFinalMessageParams>();
|
||||
private readonly _onError_llm = new Emitter<EventLLMMessageOnErrorParams>();
|
||||
|
||||
// abort
|
||||
private readonly _abortRefOfRequestId_llm: Record<string, AbortRef> = {}
|
||||
|
||||
// ollamaList
|
||||
private readonly _onSuccess_ollama = new Emitter<EventOllamaListOnSuccessParams>();
|
||||
private readonly _onError_ollama = new Emitter<EventOllamaListOnErrorParams>();
|
||||
|
||||
// stupidly, channels can't take in @IService
|
||||
constructor(
|
||||
private readonly metricsService: IMetricsService,
|
||||
) { }
|
||||
|
||||
// browser uses this to listen for changes
|
||||
listen(_: unknown, event: string): Event<any> {
|
||||
if (event === 'onText_llm') {
|
||||
return this._onText_llm.event;
|
||||
}
|
||||
else if (event === 'onFinalMessage_llm') {
|
||||
return this._onFinalMessage_llm.event;
|
||||
}
|
||||
else if (event === 'onError_llm') {
|
||||
return this._onError_llm.event;
|
||||
}
|
||||
else if (event === 'onSuccess_ollama') {
|
||||
return this._onSuccess_ollama.event;
|
||||
}
|
||||
else if (event === 'onError_ollama') {
|
||||
return this._onError_ollama.event;
|
||||
}
|
||||
else {
|
||||
throw new Error(`Event not found: ${event}`);
|
||||
}
|
||||
}
|
||||
|
||||
// browser uses this to call
|
||||
async call(_: unknown, command: string, params: any): Promise<any> {
|
||||
try {
|
||||
if (command === 'sendLLMMessage') {
|
||||
this._callSendLLMMessage(params)
|
||||
}
|
||||
else if (command === 'abort') {
|
||||
this._callAbort(params)
|
||||
}
|
||||
else if (command === 'ollamaList') {
|
||||
this._callOllamaList(params)
|
||||
}
|
||||
else {
|
||||
throw new Error(`Void sendLLM: command "${command}" not recognized.`)
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
console.log('llmMessageChannel: Call Error:', e)
|
||||
}
|
||||
}
|
||||
|
||||
// the only place sendLLMMessage is actually called
|
||||
private async _callSendLLMMessage(params: MainLLMMessageParams) {
|
||||
const { requestId } = params;
|
||||
|
||||
if (!(requestId in this._abortRefOfRequestId_llm))
|
||||
this._abortRefOfRequestId_llm[requestId] = { current: null }
|
||||
|
||||
const mainThreadParams: LLMMMessageParams = {
|
||||
...params,
|
||||
onText: ({ newText, fullText }) => { this._onText_llm.fire({ requestId, newText, fullText }); },
|
||||
onFinalMessage: ({ fullText }) => { this._onFinalMessage_llm.fire({ requestId, fullText }); },
|
||||
onError: ({ message: error, fullError }) => { console.log('sendLLM: firing err'); this._onError_llm.fire({ requestId, message: error, fullError }); },
|
||||
abortRef: this._abortRefOfRequestId_llm[requestId],
|
||||
}
|
||||
sendLLMMessage(mainThreadParams, this.metricsService);
|
||||
}
|
||||
|
||||
private _callAbort(params: MainLLMMessageAbortParams) {
|
||||
const { requestId } = params;
|
||||
if (!(requestId in this._abortRefOfRequestId_llm)) return
|
||||
this._abortRefOfRequestId_llm[requestId].current?.()
|
||||
delete this._abortRefOfRequestId_llm[requestId]
|
||||
}
|
||||
|
||||
private _callOllamaList(params: MainOllamaListParams) {
|
||||
const { requestId } = params;
|
||||
|
||||
const mainThreadParams: OllamaListParams = {
|
||||
...params,
|
||||
onSuccess: ({ models }) => { this._onSuccess_ollama.fire({ requestId, models }); },
|
||||
onError: ({ error }) => { this._onError_ollama.fire({ requestId, error }); },
|
||||
}
|
||||
ollamaList(mainThreadParams)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 { ITelemetryService } from '../../telemetry/common/telemetry.js';
|
||||
|
||||
import { IMetricsService } from '../common/metricsService.js';
|
||||
import { PostHog } from 'posthog-node'
|
||||
|
||||
|
||||
// posthog-js (old):
|
||||
// posthog.init('phc_UanIdujHiLp55BkUTjB1AuBXcasVkdqRwgnwRlWESH2', { api_host: 'https://us.i.posthog.com', })
|
||||
|
||||
// const buildEnv = 'development';
|
||||
// const buildNumber = '1.0.0';
|
||||
// const isMac = process.platform === 'darwin';
|
||||
|
||||
export class MetricsMainService extends Disposable implements IMetricsService {
|
||||
_serviceBrand: undefined;
|
||||
|
||||
readonly _distinctId: string
|
||||
readonly client: PostHog
|
||||
|
||||
constructor(
|
||||
@ITelemetryService private readonly _telemetryService: ITelemetryService
|
||||
) {
|
||||
super()
|
||||
this.client = new PostHog('phc_UanIdujHiLp55BkUTjB1AuBXcasVkdqRwgnwRlWESH2', { host: 'https://us.i.posthog.com', })
|
||||
|
||||
const { devDeviceId, firstSessionDate, machineId } = this._telemetryService
|
||||
this._distinctId = devDeviceId
|
||||
this.client.identify({ distinctId: devDeviceId, properties: { firstSessionDate, machineId } })
|
||||
|
||||
console.log('Void posthog metrics info:', JSON.stringify({ devDeviceId, firstSessionDate, machineId }))
|
||||
}
|
||||
|
||||
capture: IMetricsService['capture'] = (event, params) => {
|
||||
const capture = { distinctId: this._distinctId, event, properties: params } as const
|
||||
// console.log('full capture:', capture)
|
||||
this.client.capture(capture)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -741,8 +741,12 @@ export class CodeWindow extends BaseWindow implements ICodeWindow {
|
||||
|
||||
cb({ cancel: false, requestHeaders: Object.assign(details.requestHeaders, headers) });
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
private marketplaceHeadersPromise: Promise<object> | undefined;
|
||||
private getMarketplaceHeaders(): Promise<object> {
|
||||
if (!this.marketplaceHeadersPromise) {
|
||||
|
||||
@@ -90,6 +90,9 @@ import './mainThreadProfileContentHandlers.js';
|
||||
import './mainThreadAiRelatedInformation.js';
|
||||
import './mainThreadAiEmbeddingVector.js';
|
||||
|
||||
// Void added this:
|
||||
import './mainThreadInlineDiff.js';
|
||||
|
||||
export class ExtensionPoints implements IWorkbenchContribution {
|
||||
|
||||
static readonly ID = 'workbench.contrib.extensionPoints';
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
// Void created this file
|
||||
// it comes from mainThreadCodeInsets.ts
|
||||
|
||||
import { Disposable } from '../../../base/common/lifecycle.js';
|
||||
import { ICodeEditorService } from '../../../editor/browser/services/codeEditorService.js';
|
||||
import { MainContext, MainThreadInlineDiffShape } from '../common/extHost.protocol.js';
|
||||
import { IInlineDiffService } from '../../../editor/browser/services/inlineDiffService/inlineDiffService.js';
|
||||
import { ICodeEditor } from '../../../editor/browser/editorBrowser.js';
|
||||
import { IRange } from '../../../editor/common/core/range.js';
|
||||
import { extHostNamedCustomer, IExtHostContext } from '../../services/extensions/common/extHostCustomers.js';
|
||||
import { IUndoRedoElement, IUndoRedoService, UndoRedoElementType, UndoRedoGroup } from '../../../platform/undoRedo/common/undoRedo.js';
|
||||
import { IBulkEditService } from '../../../editor/browser/services/bulkEditService.js';
|
||||
import { WorkspaceEdit } from '../../../editor/common/languages.js';
|
||||
// import { IHistoryService } from '../../services/history/common/history.js';
|
||||
|
||||
|
||||
@extHostNamedCustomer(MainContext.MainThreadInlineDiff)
|
||||
export class MainThreadInlineDiff extends Disposable implements MainThreadInlineDiffShape {
|
||||
|
||||
// private readonly _proxy: ExtHostEditorInsetsShape;
|
||||
// private readonly _disposables = new DisposableStore();
|
||||
|
||||
constructor(
|
||||
context: IExtHostContext,
|
||||
@IInlineDiffService private readonly _inlineDiff: IInlineDiffService,
|
||||
@ICodeEditorService private readonly _editorService: ICodeEditorService,
|
||||
// @IHistoryService private readonly _historyService: IHistoryService, // history service is the history of pressing alt left/right
|
||||
@IUndoRedoService private readonly _undoRedoService: IUndoRedoService, // undoRedo service is the history of pressing ctrl+z
|
||||
@IBulkEditService private readonly _bulkEditService: IBulkEditService,
|
||||
|
||||
) {
|
||||
super();
|
||||
|
||||
// this._proxy = context.getProxy(ExtHostContext.ExtHostEditorInsets);
|
||||
// this._wcHistoryService.addEntry()
|
||||
}
|
||||
|
||||
_streamingState: { type: 'streaming'; editGroup: UndoRedoGroup } | { type: 'idle' } = { type: 'idle' }
|
||||
|
||||
startStreaming(editorId: string) {
|
||||
const editor = this._getEditor(editorId)
|
||||
if (!editor) return
|
||||
|
||||
const model = editor.getModel()
|
||||
if (!model) return
|
||||
|
||||
// all changes made when streaming should be a part of the group so we can undo them all together
|
||||
this._streamingState = {
|
||||
type: 'streaming',
|
||||
editGroup: new UndoRedoGroup()
|
||||
}
|
||||
|
||||
// TODO probably need to convert this to a stack
|
||||
const diffsSnapshotBefore = { placeholder: '' }
|
||||
const diffsSnapshotAfter = { placeholder: '' }
|
||||
|
||||
const elt: IUndoRedoElement = {
|
||||
type: UndoRedoElementType.Resource,
|
||||
resource: model.uri,
|
||||
label: 'Add Diffs',
|
||||
code: 'undoredo.inlineDiff',
|
||||
undo: () => {
|
||||
// reapply diffareas and diffs here
|
||||
console.log('reverting diffareas...', diffsSnapshotBefore.placeholder)
|
||||
},
|
||||
redo: () => {
|
||||
// reapply diffareas and diffs here
|
||||
// when done, need to record diffSnapshotAfter
|
||||
console.log('re-applying diffareas...', diffsSnapshotAfter.placeholder)
|
||||
}
|
||||
}
|
||||
|
||||
this._undoRedoService.pushElement(elt, this._streamingState.editGroup)
|
||||
|
||||
// ---------- START ----------
|
||||
editor.updateOptions({ readOnly: true })
|
||||
|
||||
|
||||
|
||||
// ---------- WHEN DONE ----------
|
||||
editor.updateOptions({ readOnly: false })
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
streamChange(editorId: string, edit: WorkspaceEdit) {
|
||||
const editor = this._getEditor(editorId)
|
||||
if (!editor) return
|
||||
|
||||
if (this._streamingState.type !== 'streaming') {
|
||||
console.error('Expected streamChange to be in state \'streaming\'.')
|
||||
return
|
||||
}
|
||||
|
||||
// count all changes towards the group
|
||||
this._bulkEditService.apply(edit, { undoRedoGroupId: this._streamingState.editGroup.id, })
|
||||
|
||||
|
||||
}
|
||||
|
||||
_getEditor = (editorId: string): ICodeEditor | undefined => {
|
||||
|
||||
let editor: ICodeEditor | undefined;
|
||||
editorId = editorId.substr(0, editorId.indexOf(',')); //todo@jrieken HACK
|
||||
|
||||
for (const candidate of this._editorService.listCodeEditors()) {
|
||||
if (candidate.getId() === editorId
|
||||
// && candidate.hasModel() && isEqual(candidate.getModel().uri, URI.revive(uri))
|
||||
) {
|
||||
editor = candidate;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return editor
|
||||
}
|
||||
|
||||
|
||||
$addDiff(editorId: string, originalText: string, range: IRange): void {
|
||||
|
||||
const editor = this._getEditor(editorId);
|
||||
if (!editor) return
|
||||
|
||||
this._inlineDiff.addDiff(editor, originalText, range)
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -109,6 +109,7 @@ import { ProxyIdentifier } from '../../services/extensions/common/proxyIdentifie
|
||||
import { ExcludeSettingOptions, TextSearchCompleteMessageType, TextSearchContextNew, TextSearchMatchNew } from '../../services/search/common/searchExtTypes.js';
|
||||
import type * as vscode from 'vscode';
|
||||
import { ExtHostCodeMapper } from './extHostCodeMapper.js';
|
||||
import { ExtHostInlineDiff } from './extHostInlineDiff.js';
|
||||
|
||||
export interface IExtensionRegistries {
|
||||
mine: ExtensionDescriptionRegistry;
|
||||
@@ -221,6 +222,9 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I
|
||||
const extHostSpeech = rpcProtocol.set(ExtHostContext.ExtHostSpeech, new ExtHostSpeech(rpcProtocol));
|
||||
const extHostEmbeddings = rpcProtocol.set(ExtHostContext.ExtHostEmbeddings, new ExtHostEmbeddings(rpcProtocol));
|
||||
|
||||
// Void added this:
|
||||
const extHostInlineDiff = rpcProtocol.set(ExtHostContext.ExtHostInlineDiff, new ExtHostInlineDiff(rpcProtocol.getProxy(MainContext.MainThreadInlineDiff), extHostEditors));
|
||||
|
||||
// Check that no named customers are missing
|
||||
const expected = Object.values<ProxyIdentifier<any>>(ExtHostContext);
|
||||
rpcProtocol.assertRegistered(expected);
|
||||
@@ -519,6 +523,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I
|
||||
|
||||
// namespace: languages
|
||||
const languages: typeof vscode.languages = {
|
||||
|
||||
createDiagnosticCollection(name?: string): vscode.DiagnosticCollection {
|
||||
return extHostDiagnostics.createDiagnosticCollection(extension.identifier, name);
|
||||
},
|
||||
@@ -553,6 +558,11 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I
|
||||
return extHostLanguageFeatures.registerCodeLensProvider(extension, checkSelector(selector), provider);
|
||||
},
|
||||
|
||||
// Void added addInlineDiff here:
|
||||
addInlineDiff(editor: vscode.TextEditor, originalText: string, modifiedRange: vscode.Range): void {
|
||||
extHostInlineDiff.addDiff(editor, originalText, modifiedRange)
|
||||
},
|
||||
|
||||
// Void added this (I think will need to add this back when add ctrl+K)
|
||||
// registerVoidCtrlKProvider(selector: vscode.DocumentSelector, provider: vscode.CodeLensProvider): vscode.Disposable {
|
||||
// return extHostLanguageFeatures.registerCodeLensProvider(extension, checkSelector(selector), provider);
|
||||
|
||||
@@ -2984,7 +2984,10 @@ export const MainContext = {
|
||||
MainThreadTesting: createProxyIdentifier<MainThreadTestingShape>('MainThreadTesting'),
|
||||
MainThreadLocalization: createProxyIdentifier<MainThreadLocalizationShape>('MainThreadLocalizationShape'),
|
||||
MainThreadAiRelatedInformation: createProxyIdentifier<MainThreadAiRelatedInformationShape>('MainThreadAiRelatedInformation'),
|
||||
MainThreadAiEmbeddingVector: createProxyIdentifier<MainThreadAiEmbeddingVectorShape>('MainThreadAiEmbeddingVector')
|
||||
MainThreadAiEmbeddingVector: createProxyIdentifier<MainThreadAiEmbeddingVectorShape>('MainThreadAiEmbeddingVector'),
|
||||
|
||||
// Void added this:
|
||||
MainThreadInlineDiff: createProxyIdentifier<MainThreadInlineDiffShape>('MainThreadInlineDiff'),
|
||||
};
|
||||
|
||||
export const ExtHostContext = {
|
||||
@@ -3055,5 +3058,17 @@ export const ExtHostContext = {
|
||||
ExtHostTimeline: createProxyIdentifier<ExtHostTimelineShape>('ExtHostTimeline'),
|
||||
ExtHostTesting: createProxyIdentifier<ExtHostTestingShape>('ExtHostTesting'),
|
||||
ExtHostTelemetry: createProxyIdentifier<ExtHostTelemetryShape>('ExtHostTelemetry'),
|
||||
ExtHostLocalization: createProxyIdentifier<ExtHostLocalizationShape>('ExtHostLocalization')
|
||||
ExtHostLocalization: createProxyIdentifier<ExtHostLocalizationShape>('ExtHostLocalization'),
|
||||
|
||||
// Void added this:
|
||||
ExtHostInlineDiff: createProxyIdentifier<ExtHostInlineDiffShape>('ExtHostInlineDiff'), // Void added this
|
||||
};
|
||||
|
||||
|
||||
// Void added these:
|
||||
export interface ExtHostInlineDiffShape {
|
||||
$onDidDispose(handle: number): void;
|
||||
}
|
||||
export interface MainThreadInlineDiffShape {
|
||||
$addDiff(editorId: string, originalText: string, range: IRange): void;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
// This file was created by Void
|
||||
// reference extHostCodeInsets.ts
|
||||
|
||||
import { Emitter } from '../../../base/common/event.js';
|
||||
import { DisposableStore } from '../../../base/common/lifecycle.js';
|
||||
import { ExtHostInlineDiffShape, MainThreadInlineDiffShape } from './extHost.protocol.js';
|
||||
import * as vscode from 'vscode'
|
||||
import { ExtHostTextEditor } from './extHostTextEditor.js';
|
||||
import { ExtHostEditors } from './extHostTextEditors.js';
|
||||
import { Range } from '../../../workbench/api/common/extHostTypeConverters.js'
|
||||
|
||||
export class ExtHostInlineDiff implements ExtHostInlineDiffShape {
|
||||
|
||||
private readonly _disposables = new DisposableStore();
|
||||
private _insets = new Map<number, { editor: vscode.TextEditor; inset: vscode.WebviewEditorInset; onDidReceiveMessage: Emitter<any> }>();
|
||||
|
||||
constructor(
|
||||
private readonly _proxy: MainThreadInlineDiffShape,
|
||||
private readonly _editors: ExtHostEditors,
|
||||
) { }
|
||||
|
||||
|
||||
dispose(): void {
|
||||
this._insets.forEach(value => value.inset.dispose());
|
||||
this._disposables.dispose();
|
||||
}
|
||||
|
||||
|
||||
addDiff(editor: vscode.TextEditor, originalText: string, modifiedRange: vscode.Range) {
|
||||
|
||||
let apiEditor: ExtHostTextEditor | undefined;
|
||||
for (const candidate of this._editors.getVisibleTextEditors(true)) {
|
||||
if (candidate.value === editor) {
|
||||
apiEditor = <ExtHostTextEditor>candidate;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!apiEditor) {
|
||||
throw new Error('not a visible editor');
|
||||
|
||||
}
|
||||
// can't send over the editor, so just send over its id and reconstruct it. This is stupid but it's what VSCode's editorinset does - Andrew
|
||||
const id = apiEditor.id;
|
||||
// let uri = apiEditor.value.document.uri;
|
||||
|
||||
// convert to IRange
|
||||
const range = Range.from(modifiedRange)
|
||||
|
||||
this._proxy.$addDiff(id, originalText, range)
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
// main thread calls this when disposes diff with this particular handle
|
||||
$onDidDispose(handle: number): void {
|
||||
const value = this._insets.get(handle);
|
||||
if (value) {
|
||||
value.inset.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -239,7 +239,7 @@ MenuRegistry.appendMenuItems([{
|
||||
group: '3_workbench_layout_move',
|
||||
command: {
|
||||
id: ToggleSidebarPositionAction.ID,
|
||||
title: localize('move second sidebar left', "Move Secondary Side Bar Left")
|
||||
title: localize('move second sidebar left', "Move Void Side Bar Left")
|
||||
},
|
||||
when: ContextKeyExpr.and(ContextKeyExpr.notEquals('config.workbench.sideBar.location', 'right'), ContextKeyExpr.equals('viewLocation', ViewContainerLocationToString(ViewContainerLocation.AuxiliaryBar))),
|
||||
order: 1
|
||||
@@ -250,7 +250,7 @@ MenuRegistry.appendMenuItems([{
|
||||
group: '3_workbench_layout_move',
|
||||
command: {
|
||||
id: ToggleSidebarPositionAction.ID,
|
||||
title: localize('move second sidebar right', "Move Secondary Side Bar Right")
|
||||
title: localize('move second sidebar right', "Move Void Side Bar Right")
|
||||
},
|
||||
when: ContextKeyExpr.and(ContextKeyExpr.equals('config.workbench.sideBar.location', 'right'), ContextKeyExpr.equals('viewLocation', ViewContainerLocationToString(ViewContainerLocation.AuxiliaryBar))),
|
||||
order: 1
|
||||
@@ -949,7 +949,7 @@ registerAction2(class extends Action2 {
|
||||
if (!hasAddedView) {
|
||||
results.push({
|
||||
type: 'separator',
|
||||
label: localize('secondarySideBarContainer', "Secondary Side Bar / {0}", containerModel.title)
|
||||
label: localize('secondarySideBarContainer', "Void Side Bar / {0}", containerModel.title)
|
||||
});
|
||||
hasAddedView = true;
|
||||
}
|
||||
@@ -1056,7 +1056,7 @@ class MoveFocusedViewAction extends Action2 {
|
||||
if (!(isViewSolo && currentLocation === ViewContainerLocation.AuxiliaryBar)) {
|
||||
items.push({
|
||||
id: '_.auxiliarybar.newcontainer',
|
||||
label: localize('moveFocusedView.newContainerInSidePanel', "New Secondary Side Bar Entry")
|
||||
label: localize('moveFocusedView.newContainerInSidePanel', "New Void Side Bar Entry")
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1104,7 +1104,7 @@ class MoveFocusedViewAction extends Action2 {
|
||||
|
||||
items.push({
|
||||
type: 'separator',
|
||||
label: localize('secondarySideBar', "Secondary Side Bar")
|
||||
label: localize('secondarySideBar', "Void Side Bar")
|
||||
});
|
||||
|
||||
const pinnedAuxPanels = paneCompositePartService.getPinnedPaneCompositeIds(ViewContainerLocation.AuxiliaryBar);
|
||||
@@ -1386,7 +1386,7 @@ if (!isMacintosh || !isNative) {
|
||||
ToggleVisibilityActions.push(...[
|
||||
CreateToggleLayoutItem(ToggleActivityBarVisibilityActionId, ContextKeyExpr.notEquals('config.workbench.activityBar.location', 'hidden'), localize('activityBar', "Activity Bar"), { whenA: ContextKeyExpr.equals('config.workbench.sideBar.location', 'left'), iconA: activityBarLeftIcon, iconB: activityBarRightIcon }),
|
||||
CreateToggleLayoutItem(ToggleSidebarVisibilityAction.ID, SideBarVisibleContext, localize('sideBar', "Primary Side Bar"), { whenA: ContextKeyExpr.equals('config.workbench.sideBar.location', 'left'), iconA: panelLeftIcon, iconB: panelRightIcon }),
|
||||
CreateToggleLayoutItem(ToggleAuxiliaryBarAction.ID, AuxiliaryBarVisibleContext, localize('secondarySideBar', "Secondary Side Bar"), { whenA: ContextKeyExpr.equals('config.workbench.sideBar.location', 'left'), iconA: panelRightIcon, iconB: panelLeftIcon }),
|
||||
CreateToggleLayoutItem(ToggleAuxiliaryBarAction.ID, AuxiliaryBarVisibleContext, localize('secondarySideBar', "Void Side Bar"), { whenA: ContextKeyExpr.equals('config.workbench.sideBar.location', 'left'), iconA: panelRightIcon, iconB: panelLeftIcon }),
|
||||
CreateToggleLayoutItem(TogglePanelAction.ID, PanelVisibleContext, localize('panel', "Panel"), panelIcon),
|
||||
CreateToggleLayoutItem(ToggleStatusbarVisibilityAction.ID, ContextKeyExpr.equals('config.workbench.statusBar.visible', true), localize('statusBar', "Status Bar"), statusBarIcon),
|
||||
]);
|
||||
|
||||
@@ -2434,7 +2434,7 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi
|
||||
comment: 'Information about the layout of the workbench during statup';
|
||||
activityBarVisible: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether or the not the activity bar is visible' };
|
||||
sideBarVisible: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether or the not the primary side bar is visible' };
|
||||
auxiliaryBarVisible: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether or the not the secondary side bar is visible' };
|
||||
auxiliaryBarVisible: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether or the not the Void side bar is visible' };
|
||||
panelVisible: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether or the not the panel is visible' };
|
||||
statusbarVisible: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether or the not the status bar is visible' };
|
||||
sideBarPosition: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether the primary side bar is on the left or right' };
|
||||
|
||||
@@ -26,7 +26,7 @@ const auxiliaryBarLeftOffIcon = registerIcon('auxiliarybar-left-off-layout-icon'
|
||||
export class ToggleAuxiliaryBarAction extends Action2 {
|
||||
|
||||
static readonly ID = 'workbench.action.toggleAuxiliaryBar';
|
||||
static readonly LABEL = localize2('toggleAuxiliaryBar', "Toggle Secondary Side Bar Visibility");
|
||||
static readonly LABEL = localize2('toggleAuxiliaryBar', "Toggle Void Side Bar Visibility");
|
||||
|
||||
constructor() {
|
||||
super({
|
||||
@@ -34,7 +34,7 @@ export class ToggleAuxiliaryBarAction extends Action2 {
|
||||
title: ToggleAuxiliaryBarAction.LABEL,
|
||||
toggled: {
|
||||
condition: AuxiliaryBarVisibleContext,
|
||||
title: localize('secondary sidebar', "Secondary Side Bar"),
|
||||
title: localize('secondary sidebar', "Void Side Bar"),
|
||||
mnemonicTitle: localize({ key: 'secondary sidebar mnemonic', comment: ['&& denotes a mnemonic'] }, "Secondary Si&&de Bar"),
|
||||
},
|
||||
|
||||
@@ -70,7 +70,7 @@ registerAction2(ToggleAuxiliaryBarAction);
|
||||
registerAction2(class FocusAuxiliaryBarAction extends Action2 {
|
||||
|
||||
static readonly ID = 'workbench.action.focusAuxiliaryBar';
|
||||
static readonly LABEL = localize2('focusAuxiliaryBar', "Focus into Secondary Side Bar");
|
||||
static readonly LABEL = localize2('focusAuxiliaryBar', "Focus into Void Side Bar");
|
||||
|
||||
constructor() {
|
||||
super({
|
||||
@@ -103,7 +103,7 @@ MenuRegistry.appendMenuItems([
|
||||
group: '0_workbench_toggles',
|
||||
command: {
|
||||
id: ToggleAuxiliaryBarAction.ID,
|
||||
title: localize('toggleSecondarySideBar', "Toggle Secondary Side Bar"),
|
||||
title: localize('toggleSecondarySideBar', "Toggle Void Side Bar"),
|
||||
toggled: { condition: AuxiliaryBarVisibleContext, icon: auxiliaryBarLeftIcon },
|
||||
icon: auxiliaryBarLeftOffIcon,
|
||||
},
|
||||
@@ -116,7 +116,7 @@ MenuRegistry.appendMenuItems([
|
||||
group: '0_workbench_toggles',
|
||||
command: {
|
||||
id: ToggleAuxiliaryBarAction.ID,
|
||||
title: localize('toggleSecondarySideBar', "Toggle Secondary Side Bar"),
|
||||
title: localize('toggleSecondarySideBar', "Toggle Void Side Bar"),
|
||||
toggled: { condition: AuxiliaryBarVisibleContext, icon: auxiliaryBarRightIcon },
|
||||
icon: auxiliaryBarRightOffIcon,
|
||||
},
|
||||
@@ -129,7 +129,7 @@ MenuRegistry.appendMenuItems([
|
||||
group: '3_workbench_layout_move',
|
||||
command: {
|
||||
id: ToggleAuxiliaryBarAction.ID,
|
||||
title: localize2('hideAuxiliaryBar', 'Hide Secondary Side Bar'),
|
||||
title: localize2('hideAuxiliaryBar', 'Hide Void Side Bar'),
|
||||
},
|
||||
when: ContextKeyExpr.and(AuxiliaryBarVisibleContext, ContextKeyExpr.equals('viewLocation', ViewContainerLocationToString(ViewContainerLocation.AuxiliaryBar))),
|
||||
order: 2
|
||||
|
||||
@@ -195,11 +195,13 @@ export class AuxiliaryBarPart extends AbstractPaneCompositePart {
|
||||
const positionActions: IAction[] = [];
|
||||
createAndFillInContextMenuActions(activityBarPositionMenu, { primary: [], secondary: positionActions });
|
||||
|
||||
|
||||
// appears when right click
|
||||
actions.push(...[
|
||||
new Separator(),
|
||||
new SubmenuAction('workbench.action.panel.position', localize('activity bar position', "Activity Bar Position"), positionActions),
|
||||
toAction({ id: ToggleSidebarPositionAction.ID, label: currentPositionRight ? localize('move second side bar left', "Move Secondary Side Bar Left") : localize('move second side bar right', "Move Secondary Side Bar Right"), run: () => this.commandService.executeCommand(ToggleSidebarPositionAction.ID) }),
|
||||
toAction({ id: ToggleAuxiliaryBarAction.ID, label: localize('hide second side bar', "Hide Secondary Side Bar"), run: () => this.commandService.executeCommand(ToggleAuxiliaryBarAction.ID) })
|
||||
toAction({ id: ToggleSidebarPositionAction.ID, label: currentPositionRight ? localize('move second side bar left', "Move Void Side Bar Left") : localize('move second side bar right', "Move Void Side Bar Right"), run: () => this.commandService.executeCommand(ToggleSidebarPositionAction.ID) }),
|
||||
toAction({ id: ToggleAuxiliaryBarAction.ID, label: localize('hide second side bar', "Hide Void Side Bar"), run: () => this.commandService.executeCommand(ToggleAuxiliaryBarAction.ID) })
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -342,7 +342,7 @@ registerAction2(class extends Action2 {
|
||||
constructor() {
|
||||
super({
|
||||
id: 'workbench.action.closeAuxiliaryBar',
|
||||
title: localize2('closeSecondarySideBar', 'Hide Secondary Side Bar'),
|
||||
title: localize2('closeSecondarySideBar', 'Hide Void Side Bar'),
|
||||
category: Categories.View,
|
||||
icon: closeIcon,
|
||||
menu: [{
|
||||
@@ -415,14 +415,16 @@ class MoveViewsBetweenPanelsAction extends Action2 {
|
||||
}
|
||||
}
|
||||
|
||||
// --- Move Panel Views To Secondary Side Bar
|
||||
// --- Move Panel Views To Void Side Bar
|
||||
|
||||
// these are just for the command pallette
|
||||
|
||||
class MovePanelToSidePanelAction extends MoveViewsBetweenPanelsAction {
|
||||
static readonly ID = 'workbench.action.movePanelToSidePanel';
|
||||
constructor() {
|
||||
super(ViewContainerLocation.Panel, ViewContainerLocation.AuxiliaryBar, {
|
||||
id: MovePanelToSidePanelAction.ID,
|
||||
title: localize2('movePanelToSecondarySideBar', "Move Panel Views To Secondary Side Bar"),
|
||||
title: localize2('movePanelToSecondarySideBar', "Move Panel Views To Void Side Bar"),
|
||||
category: Categories.View,
|
||||
f1: false
|
||||
});
|
||||
@@ -434,7 +436,7 @@ export class MovePanelToSecondarySideBarAction extends MoveViewsBetweenPanelsAct
|
||||
constructor() {
|
||||
super(ViewContainerLocation.Panel, ViewContainerLocation.AuxiliaryBar, {
|
||||
id: MovePanelToSecondarySideBarAction.ID,
|
||||
title: localize2('movePanelToSecondarySideBar', "Move Panel Views To Secondary Side Bar"),
|
||||
title: localize2('movePanelToSecondarySideBar', "Move Panel Views To Void Side Bar"),
|
||||
category: Categories.View,
|
||||
f1: true
|
||||
});
|
||||
@@ -452,7 +454,7 @@ class MoveSidePanelToPanelAction extends MoveViewsBetweenPanelsAction {
|
||||
constructor() {
|
||||
super(ViewContainerLocation.AuxiliaryBar, ViewContainerLocation.Panel, {
|
||||
id: MoveSidePanelToPanelAction.ID,
|
||||
title: localize2('moveSidePanelToPanel', "Move Secondary Side Bar Views To Panel"),
|
||||
title: localize2('moveSidePanelToPanel', "Move Side Bar Views To Panel"), // Void - this seemed to have a typo before
|
||||
category: Categories.View,
|
||||
f1: false
|
||||
});
|
||||
@@ -465,7 +467,7 @@ export class MoveSecondarySideBarToPanelAction extends MoveViewsBetweenPanelsAct
|
||||
constructor() {
|
||||
super(ViewContainerLocation.AuxiliaryBar, ViewContainerLocation.Panel, {
|
||||
id: MoveSecondarySideBarToPanelAction.ID,
|
||||
title: localize2('moveSidePanelToPanel', "Move Secondary Side Bar Views To Panel"),
|
||||
title: localize2('moveSidePanelToPanel', "Move Void Side Bar Views To Panel"),
|
||||
category: Categories.View,
|
||||
f1: true
|
||||
});
|
||||
|
||||
@@ -512,7 +512,7 @@ const registry = Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Con
|
||||
'type': 'string',
|
||||
'enum': ['left', 'right'],
|
||||
'default': 'left',
|
||||
'description': localize('sideBarLocation', "Controls the location of the primary side bar and activity bar. They can either show on the left or right of the workbench. The secondary side bar will show on the opposite side of the workbench.")
|
||||
'description': localize('sideBarLocation', "Controls the location of the primary side bar and activity bar. They can either show on the left or right of the workbench. The Void side bar will show on the opposite side of the workbench.")
|
||||
},
|
||||
'workbench.panel.showLabel': {
|
||||
'type': 'boolean',
|
||||
@@ -545,12 +545,12 @@ const registry = Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Con
|
||||
'type': 'string',
|
||||
'enum': ['default', 'top', 'bottom', 'hidden'],
|
||||
'default': 'default',
|
||||
'markdownDescription': localize({ comment: ['This is the description for a setting'], key: 'activityBarLocation' }, "Controls the location of the Activity Bar relative to the Primary and Secondary Side Bars."),
|
||||
'markdownDescription': localize({ comment: ['This is the description for a setting'], key: 'activityBarLocation' }, "Controls the location of the Activity Bar relative to the Primary and Void Side Bars."),
|
||||
'enumDescriptions': [
|
||||
localize('workbench.activityBar.location.default', "Show the Activity Bar on the side of the Primary Side Bar and on top of the Secondary Side Bar."),
|
||||
localize('workbench.activityBar.location.top', "Show the Activity Bar on top of the Primary and Secondary Side Bars."),
|
||||
localize('workbench.activityBar.location.bottom', "Show the Activity Bar at the bottom of the Primary and Secondary Side Bars."),
|
||||
localize('workbench.activityBar.location.hide', "Hide the Activity Bar in the Primary and Secondary Side Bars.")
|
||||
localize('workbench.activityBar.location.default', "Show the Activity Bar on the side of the Primary Side Bar and on top of the Void Side Bar."),
|
||||
localize('workbench.activityBar.location.top', "Show the Activity Bar on top of the Primary and Void Side Bars."),
|
||||
localize('workbench.activityBar.location.bottom', "Show the Activity Bar at the bottom of the Primary and Void Side Bars."),
|
||||
localize('workbench.activityBar.location.hide', "Hide the Activity Bar in the Primary and Void Side Bars.")
|
||||
],
|
||||
},
|
||||
'workbench.activityBar.iconClickBehavior': {
|
||||
@@ -598,7 +598,7 @@ const registry = Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Con
|
||||
'description': localize('workbench.hover.delay', "Controls the delay in milliseconds after which the hover is shown for workbench items (ex. some extension provided tree view items). Already visible items may require a refresh before reflecting this setting change."),
|
||||
// Testing has indicated that on Windows and Linux 500 ms matches the native hovers most closely.
|
||||
// On Mac, the delay is 1500.
|
||||
'default': isMacintosh ? 1500 : 500,
|
||||
'default': isMacintosh ? 300 : 300, // <-- Void edited this to 300 : 300 (was 1500 : 500)
|
||||
'minimum': 0
|
||||
},
|
||||
'workbench.reduceMotion': {
|
||||
|
||||
@@ -151,7 +151,7 @@ export class ViewQuickAccessProvider extends PickerQuickAccessProvider<IViewQuic
|
||||
// Viewlets / Panels
|
||||
addPaneComposites(ViewContainerLocation.Sidebar, localize('views', "Side Bar"));
|
||||
addPaneComposites(ViewContainerLocation.Panel, localize('panels', "Panel"));
|
||||
addPaneComposites(ViewContainerLocation.AuxiliaryBar, localize('secondary side bar', "Secondary Side Bar"));
|
||||
addPaneComposites(ViewContainerLocation.AuxiliaryBar, localize('secondary side bar', "Void Side Bar"));
|
||||
|
||||
const addPaneCompositeViews = (location: ViewContainerLocation) => {
|
||||
const paneComposites = this.paneCompositeService.getPaneComposites(location);
|
||||
|
||||
@@ -0,0 +1,770 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 { ILanguageFeaturesService } from '../../../../editor/common/services/languageFeatures.js';
|
||||
import { registerSingleton, InstantiationType } from '../../../../platform/instantiation/common/extensions.js';
|
||||
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
|
||||
import { ITextModel } from '../../../../editor/common/model.js';
|
||||
import { Position } from '../../../../editor/common/core/position.js';
|
||||
import { InlineCompletion, InlineCompletionContext } from '../../../../editor/common/languages.js';
|
||||
import { CancellationToken } from '../../../../base/common/cancellation.js';
|
||||
import { Range } from '../../../../editor/common/core/range.js';
|
||||
import { ILLMMessageService } from '../../../../platform/void/common/llmMessageService.js';
|
||||
import { IEditorService } from '../../../services/editor/common/editorService.js';
|
||||
import { isCodeEditor } from '../../../../editor/browser/editorBrowser.js';
|
||||
import { EditorResourceAccessor } from '../../../common/editor.js';
|
||||
import { IModelService } from '../../../../editor/common/services/model.js';
|
||||
|
||||
// The extension this was called from is here - https://github.com/voideditor/void/blob/autocomplete/extensions/void/src/extension/extension.ts
|
||||
|
||||
|
||||
/*
|
||||
A summary of autotab:
|
||||
|
||||
Postprocessing
|
||||
-one common problem for all models is outputting unbalanced parentheses
|
||||
we solve this by trimming all extra closing parentheses from the generated string
|
||||
in future, should make sure parentheses are always balanced
|
||||
|
||||
-another problem is completing the middle of a string, eg. "const [x, CURSOR] = useState()"
|
||||
we complete up to first matchup character
|
||||
but should instead complete the whole line / block (difficult because of parenthesis accuracy)
|
||||
|
||||
-too much info is bad. usually we want to show the user 1 line, and have a preloaded response afterwards
|
||||
this should happen automatically with caching system
|
||||
should break preloaded responses into \n\n chunks
|
||||
|
||||
Preprocessing
|
||||
- we don't generate if cursor is at end / beginning of a line (no spaces)
|
||||
- we generate 1 line if there is text to the right of cursor
|
||||
- we generate 1 line if variable declaration
|
||||
- (in many cases want to show 1 line but generate multiple)
|
||||
|
||||
State
|
||||
- cache based on prefix (and do some trimming first)
|
||||
- when press tab on one line, should have an immediate followup response
|
||||
to do this, show autocompletes before they're fully finished
|
||||
- [todo] remove each autotab when accepted
|
||||
!- [todo] provide type information
|
||||
|
||||
Details
|
||||
-generated results are trimmed up to 1 leading/trailing space
|
||||
-prefixes are cached up to 1 trailing newline
|
||||
-
|
||||
*/
|
||||
|
||||
class LRUCache<K, V> {
|
||||
public items: Map<K, V>;
|
||||
private keyOrder: K[];
|
||||
private maxSize: number;
|
||||
private disposeCallback?: (value: V, key?: K) => void;
|
||||
|
||||
constructor(maxSize: number, disposeCallback?: (value: V, key?: K) => void) {
|
||||
if (maxSize <= 0) throw new Error('Cache size must be greater than 0');
|
||||
|
||||
this.items = new Map();
|
||||
this.keyOrder = [];
|
||||
this.maxSize = maxSize;
|
||||
this.disposeCallback = disposeCallback;
|
||||
}
|
||||
|
||||
set(key: K, value: V): void {
|
||||
// If key exists, remove it from the order list
|
||||
if (this.items.has(key)) {
|
||||
this.keyOrder = this.keyOrder.filter(k => k !== key);
|
||||
}
|
||||
// If cache is full, remove least recently used item
|
||||
else if (this.items.size >= this.maxSize) {
|
||||
const key = this.keyOrder[0];
|
||||
const value = this.items.get(key);
|
||||
|
||||
// Call dispose callback if it exists
|
||||
if (this.disposeCallback && value !== undefined) {
|
||||
this.disposeCallback(value, key);
|
||||
}
|
||||
|
||||
this.items.delete(key);
|
||||
this.keyOrder.shift();
|
||||
}
|
||||
|
||||
// Add new item
|
||||
this.items.set(key, value);
|
||||
this.keyOrder.push(key);
|
||||
}
|
||||
|
||||
delete(key: K): boolean {
|
||||
const value = this.items.get(key);
|
||||
|
||||
if (value !== undefined) {
|
||||
// Call dispose callback if it exists
|
||||
if (this.disposeCallback) {
|
||||
this.disposeCallback(value, key);
|
||||
}
|
||||
|
||||
this.items.delete(key);
|
||||
this.keyOrder = this.keyOrder.filter(k => k !== key);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
// Call dispose callback for all items if it exists
|
||||
if (this.disposeCallback) {
|
||||
for (const [key, value] of this.items.entries()) {
|
||||
this.disposeCallback(value, key);
|
||||
}
|
||||
}
|
||||
|
||||
this.items.clear();
|
||||
this.keyOrder = [];
|
||||
}
|
||||
|
||||
get size(): number {
|
||||
return this.items.size;
|
||||
}
|
||||
|
||||
has(key: K): boolean {
|
||||
return this.items.has(key);
|
||||
}
|
||||
}
|
||||
|
||||
type AutocompletionStatus = 'pending' | 'finished' | 'error';
|
||||
type Autocompletion = {
|
||||
id: number,
|
||||
prefix: string,
|
||||
suffix: string,
|
||||
startTime: number,
|
||||
endTime: number | undefined,
|
||||
status: AutocompletionStatus,
|
||||
llmPromise: Promise<string> | undefined,
|
||||
insertText: string,
|
||||
requestId: string | null,
|
||||
}
|
||||
|
||||
const DEBOUNCE_TIME = 500
|
||||
const TIMEOUT_TIME = 60000
|
||||
const MAX_CACHE_SIZE = 20
|
||||
const MAX_PENDING_REQUESTS = 2
|
||||
|
||||
// postprocesses the result
|
||||
const postprocessResult = (result: string) => {
|
||||
|
||||
// trim all whitespace except for a single leading/trailing space
|
||||
// return result.trim()
|
||||
|
||||
const hasLeadingSpace = result.startsWith(' ');
|
||||
const hasTrailingSpace = result.endsWith(' ');
|
||||
return (hasLeadingSpace ? ' ' : '')
|
||||
+ result.trim()
|
||||
+ (hasTrailingSpace ? ' ' : '');
|
||||
|
||||
}
|
||||
|
||||
const extractCodeFromResult = (result: string) => {
|
||||
// Match either:
|
||||
// 1. ```language\n<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 => {
|
||||
const trimmedString = s.trimEnd();
|
||||
const trailingEnd = s.slice(trimmedString.length);
|
||||
|
||||
// keep only a single trailing newline
|
||||
if (trailingEnd.includes('\n')) {
|
||||
s = trimmedString + '\n';
|
||||
}
|
||||
|
||||
s = s.replace(/^\s+/gm, ''); // remove left tabs
|
||||
|
||||
return s;
|
||||
}
|
||||
|
||||
|
||||
|
||||
function getStringUpToUnbalancedParenthesis(s: string, prefix: string): string {
|
||||
|
||||
const pairs: Record<string, string> = { ')': '(', '}': '{', ']': '[' };
|
||||
|
||||
// process all bracets in prefix
|
||||
let stack: string[] = []
|
||||
const firstOpenIdx = prefix.search(/[[({]/);
|
||||
if (firstOpenIdx !== -1) {
|
||||
const brackets = prefix.slice(firstOpenIdx).split('').filter(c => '()[]{}'.includes(c));
|
||||
|
||||
for (const bracket of brackets) {
|
||||
if (bracket === '(' || bracket === '{' || bracket === '[') {
|
||||
stack.push(bracket);
|
||||
} else {
|
||||
if (stack.length > 0 && stack[stack.length - 1] === pairs[bracket]) {
|
||||
stack.pop();
|
||||
} else {
|
||||
stack.push(bracket);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// iterate through each character
|
||||
for (let i = 0; i < s.length; i++) {
|
||||
const char = s[i];
|
||||
|
||||
if (char === '(' || char === '{' || char === '[') { stack.push(char); }
|
||||
else if (char === ')' || char === '}' || char === ']') {
|
||||
if (stack.length === 0 || stack.pop() !== pairs[char]) { return s.substring(0, i); }
|
||||
}
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
|
||||
const parenthesisChars = `{}()[]<>\`'"`
|
||||
|
||||
// returns the text in the autocompletion to display, assuming the prefix is already matched
|
||||
const toInlineCompletions = ({ matchInfo, prefix, suffix, autocompletion, position, debug }: { matchInfo: matchInfo, prefix: string, suffix: string, autocompletion: Autocompletion, position: Position, debug?: boolean }): { insertText: string, range: Range }[] => {
|
||||
|
||||
|
||||
const suffixLines = suffix.split('\n')
|
||||
const prefixLines = prefix.split('\n')
|
||||
const suffixToTheRightOfCursor = suffixLines[0]
|
||||
const prefixToTheLeftOfCursor = prefixLines[prefixLines.length - 1]
|
||||
const generatedMiddle = autocompletion.insertText
|
||||
|
||||
let startIdx = matchInfo.startIdx
|
||||
let endIdx = generatedMiddle.length // exclusive bounds
|
||||
|
||||
// const naiveReturnValue = generatedMiddle.slice(startIdx)
|
||||
// console.log('naiveReturnValue: ', JSON.stringify(naiveReturnValue))
|
||||
// return [{ insertText: naiveReturnValue, }]
|
||||
|
||||
// do postprocessing for better ux
|
||||
// this is a bit hacky but may change a lot
|
||||
|
||||
// if there is space at the start of the completion and user has added it, remove it
|
||||
const charToLeftOfCursor = prefixToTheLeftOfCursor.slice(-1)[0] || ''
|
||||
const userHasAddedASpace = charToLeftOfCursor === ' ' || charToLeftOfCursor === '\t'
|
||||
const rawFirstNonspaceIdx = generatedMiddle.slice(startIdx).search(/[^\t ]/)
|
||||
if (rawFirstNonspaceIdx > -1 && userHasAddedASpace) {
|
||||
const firstNonspaceIdx = rawFirstNonspaceIdx + startIdx;
|
||||
// console.log('p0', startIdx, rawFirstNonspaceIdx)
|
||||
startIdx = Math.max(startIdx, firstNonspaceIdx)
|
||||
}
|
||||
|
||||
// if user is on a blank line and the generation starts with newline(s), remove them
|
||||
const numStartingNewlines = generatedMiddle.slice(startIdx).match(/^\n+/)?.[0].length || 0;
|
||||
if (
|
||||
!prefixToTheLeftOfCursor.trim()
|
||||
&& !suffixToTheRightOfCursor.trim()
|
||||
&& numStartingNewlines > 0
|
||||
) {
|
||||
// console.log('p1', numStartingNewlines)
|
||||
startIdx += numStartingNewlines
|
||||
}
|
||||
|
||||
// if the generated text matches with the suffix on the current line, stop
|
||||
if (suffixToTheRightOfCursor.trim()) { // completing in the middle of a line
|
||||
// complete until there is a match
|
||||
const rawMatchIndex = generatedMiddle.slice(startIdx).lastIndexOf(suffixToTheRightOfCursor.trim()[0])
|
||||
if (rawMatchIndex > -1) {
|
||||
// console.log('p2', rawMatchIndex, startIdx, suffixToTheRightOfCursor.trim()[0], 'AAA', generatedMiddle.slice(startIdx))
|
||||
const matchIdx = rawMatchIndex + startIdx;
|
||||
const matchChar = generatedMiddle[matchIdx]
|
||||
if (parenthesisChars.includes(matchChar)) {
|
||||
endIdx = Math.min(endIdx, matchIdx)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const restOfLineToGenerate = generatedMiddle.slice(startIdx).split('\n')[0] ?? ''
|
||||
// condition to complete as a single line completion
|
||||
if (
|
||||
prefixToTheLeftOfCursor.trim()
|
||||
&& !suffixToTheRightOfCursor.trim()
|
||||
&& restOfLineToGenerate.trim()
|
||||
) {
|
||||
|
||||
const rawNewlineIdx = generatedMiddle.slice(startIdx).indexOf('\n')
|
||||
if (rawNewlineIdx > -1) {
|
||||
// console.log('p3', startIdx, rawNewlineIdx)
|
||||
const newlineIdx = rawNewlineIdx + startIdx;
|
||||
endIdx = Math.min(endIdx, newlineIdx)
|
||||
}
|
||||
}
|
||||
|
||||
// // if a generated line matches with a suffix line, stop
|
||||
// if (suffixLines.length > 1) {
|
||||
// console.log('4')
|
||||
// const lines = []
|
||||
// for (const generatedLine of generatedLines) {
|
||||
// if (suffixLines.slice(0, 10).some(suffixLine =>
|
||||
// generatedLine.trim() !== '' && suffixLine.trim() !== ''
|
||||
// && generatedLine.trim().startsWith(suffixLine.trim())
|
||||
// )) break;
|
||||
// lines.push(generatedLine)
|
||||
// }
|
||||
// endIdx = lines.join('\n').length // this is hacky, remove or refactor in future
|
||||
// }
|
||||
|
||||
// console.log('pFinal', startIdx, endIdx)
|
||||
let completionStr = generatedMiddle.slice(startIdx, endIdx)
|
||||
|
||||
// filter out unbalanced parentheses
|
||||
completionStr = getStringUpToUnbalancedParenthesis(completionStr, prefix)
|
||||
// console.log('originalCompletionStr: ', JSON.stringify(generatedMiddle.slice(startIdx)))
|
||||
// console.log('finalCompletionStr: ', JSON.stringify(completionStr))
|
||||
|
||||
let rangeToReplace: Range = new Range(position.lineNumber, position.column, position.lineNumber, position.column)
|
||||
|
||||
return [{
|
||||
insertText: completionStr,
|
||||
range: rangeToReplace,
|
||||
}]
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// returns whether this autocompletion is in the cache
|
||||
// const doesPrefixMatchAutocompletion = ({ prefix, autocompletion }: { prefix: string, autocompletion: Autocompletion }): boolean => {
|
||||
|
||||
// const originalPrefix = autocompletion.prefix
|
||||
// const generatedMiddle = autocompletion.result
|
||||
// const originalPrefixTrimmed = trimPrefix(originalPrefix)
|
||||
// const currentPrefixTrimmed = trimPrefix(prefix)
|
||||
|
||||
// if (currentPrefixTrimmed.length < originalPrefixTrimmed.length) {
|
||||
// return false
|
||||
// }
|
||||
|
||||
// const isMatch = (originalPrefixTrimmed + generatedMiddle).startsWith(currentPrefixTrimmed)
|
||||
// return isMatch
|
||||
|
||||
// }
|
||||
|
||||
const getPrefixAndSuffix = (model: ITextModel, position: Position) => {
|
||||
|
||||
const fullText = model.getValue();
|
||||
|
||||
const cursorOffset = model.getOffsetAt(position)
|
||||
const prefix = fullText.substring(0, cursorOffset)
|
||||
const suffix = fullText.substring(cursorOffset)
|
||||
|
||||
return { prefix, suffix }
|
||||
|
||||
}
|
||||
|
||||
const getIndex = (str: string, line: number, char: number) => {
|
||||
return str.split('\n').slice(0, line).join('\n').length + (line > 0 ? 1 : 0) + char;
|
||||
}
|
||||
const getLastLine = (s: string): string => {
|
||||
const matches = s.match(/[^\n]*$/)
|
||||
return matches ? matches[0] : ''
|
||||
}
|
||||
|
||||
type matchInfo = {
|
||||
lineStart: number,
|
||||
character: number,
|
||||
startIdx: number,
|
||||
}
|
||||
// returns the startIdx of the match if there is a match, or undefined if there is no match
|
||||
// all results are wrt `autocompletion.result`
|
||||
const getPrefixAutocompletionMatch = ({ prefix, autocompletion }: { prefix: string, autocompletion: Autocompletion }): matchInfo | undefined => {
|
||||
|
||||
const trimmedCurrentPrefix = removeLeftTabsAndTrimEnd(prefix)
|
||||
const trimmedCompletionPrefix = removeLeftTabsAndTrimEnd(autocompletion.prefix)
|
||||
const trimmedCompletionMiddle = removeLeftTabsAndTrimEnd(autocompletion.insertText)
|
||||
|
||||
// console.log('@result: ', JSON.stringify(autocompletion.insertText))
|
||||
// console.log('@trimmedCurrentPrefix: ', JSON.stringify(trimmedCurrentPrefix))
|
||||
// console.log('@trimmedCompletionPrefix: ', JSON.stringify(trimmedCompletionPrefix))
|
||||
// console.log('@trimmedCompletionMiddle: ', JSON.stringify(trimmedCompletionMiddle))
|
||||
|
||||
if (trimmedCurrentPrefix.length < trimmedCompletionPrefix.length) { // user must write text beyond the original prefix at generation time
|
||||
console.log('@undefined1')
|
||||
return undefined
|
||||
}
|
||||
|
||||
if ( // check that completion starts with the prefix
|
||||
!(trimmedCompletionPrefix + trimmedCompletionMiddle)
|
||||
.startsWith(trimmedCurrentPrefix)
|
||||
) {
|
||||
console.log('@undefined2')
|
||||
return undefined
|
||||
}
|
||||
|
||||
// reverse map to find position wrt `autocompletion.result`
|
||||
const lineStart =
|
||||
trimmedCurrentPrefix.split('\n').length -
|
||||
trimmedCompletionPrefix.split('\n').length;
|
||||
|
||||
if (lineStart < 0) {
|
||||
console.log('@undefined3')
|
||||
|
||||
console.error('Error: No line found.');
|
||||
return undefined;
|
||||
}
|
||||
const currentPrefixLine = getLastLine(trimmedCurrentPrefix)
|
||||
const completionPrefixLine = lineStart === 0 ? getLastLine(trimmedCompletionPrefix) : ''
|
||||
const completionMiddleLine = autocompletion.insertText.split('\n')[lineStart]
|
||||
const fullCompletionLine = completionPrefixLine + completionMiddleLine
|
||||
|
||||
// console.log('currentPrefixLine', currentPrefixLine)
|
||||
// console.log('completionPrefixLine', completionPrefixLine)
|
||||
// console.log('completionMiddleLine', completionMiddleLine)
|
||||
|
||||
const charMatchIdx = fullCompletionLine.indexOf(currentPrefixLine)
|
||||
if (charMatchIdx < 0) {
|
||||
console.log('@undefined4', charMatchIdx)
|
||||
|
||||
console.error('Warning: Found character with negative index. This should never happen.')
|
||||
return undefined
|
||||
}
|
||||
|
||||
const character = (charMatchIdx +
|
||||
currentPrefixLine.length
|
||||
- completionPrefixLine.length
|
||||
)
|
||||
|
||||
const startIdx = getIndex(autocompletion.insertText, lineStart, character)
|
||||
|
||||
return {
|
||||
lineStart,
|
||||
character,
|
||||
startIdx,
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
const getCompletionOptions = ({ prefix, suffix }: { prefix: string, suffix: string }) => {
|
||||
|
||||
const prefixLines = prefix.split('\n')
|
||||
const suffixLines = suffix.split('\n')
|
||||
|
||||
const prefixToLeftOfCursor = prefixLines.slice(-1)[0] ?? ''
|
||||
const suffixToRightOfCursor = suffixLines[0] ?? ''
|
||||
|
||||
// default parameters
|
||||
let shouldGenerate = true
|
||||
let stopTokens: string[] = ['\n\n', '\r\n\r\n']
|
||||
|
||||
// specific cases
|
||||
if (suffixToRightOfCursor.trim() !== '') { // typing between something
|
||||
stopTokens = ['\n', '\r\n']
|
||||
}
|
||||
|
||||
// if (prefixToLeftOfCursor.trim() === '' && suffixToRightOfCursor.trim() === '') { // at an empty line
|
||||
// stopTokens = ['\n\n', '\r\n\r\n']
|
||||
// }
|
||||
|
||||
if (prefixToLeftOfCursor === '') { // at beginning or end of line
|
||||
shouldGenerate = false
|
||||
}
|
||||
|
||||
return { shouldGenerate, stopTokens }
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
export interface IAutocompleteService {
|
||||
readonly _serviceBrand: undefined;
|
||||
}
|
||||
|
||||
export const IAutocompleteService = createDecorator<IAutocompleteService>('AutocompleteService');
|
||||
|
||||
export class AutocompleteService extends Disposable implements IAutocompleteService {
|
||||
_serviceBrand: undefined;
|
||||
|
||||
private _autocompletionId: number = 0;
|
||||
private _autocompletionsOfDocument: { [docUriStr: string]: LRUCache<number, Autocompletion> } = {}
|
||||
|
||||
private _lastCompletionTime = 0
|
||||
private _lastPrefix: string = ''
|
||||
|
||||
// used internally by vscode
|
||||
// fires after every keystroke and returns the completion to show
|
||||
async _provideInlineCompletionItems(
|
||||
model: ITextModel,
|
||||
position: Position,
|
||||
context: InlineCompletionContext,
|
||||
token: CancellationToken,
|
||||
): Promise<InlineCompletion[]> {
|
||||
|
||||
const disabled = true
|
||||
const testMode = false
|
||||
|
||||
if (disabled) return [];
|
||||
|
||||
const docUriStr = model.uri.toString();
|
||||
|
||||
const { prefix, suffix } = getPrefixAndSuffix(model, position)
|
||||
// initialize cache and other variables
|
||||
// note that whenever an autocompletion is rejected, it is removed from cache
|
||||
if (!this._autocompletionsOfDocument[docUriStr]) {
|
||||
this._autocompletionsOfDocument[docUriStr] = new LRUCache<number, Autocompletion>(
|
||||
MAX_CACHE_SIZE,
|
||||
(autocompletion: Autocompletion) => {
|
||||
if (autocompletion.requestId)
|
||||
this._llmMessageService.abort(autocompletion.requestId)
|
||||
}
|
||||
)
|
||||
}
|
||||
this._lastPrefix = prefix
|
||||
|
||||
// print all pending autocompletions
|
||||
// let _numPending = 0
|
||||
// this._autocompletionsOfDocument[docUriStr].items.forEach((a: Autocompletion) => { if (a.status === 'pending') _numPending += 1 })
|
||||
// console.log('@numPending: ' + _numPending)
|
||||
|
||||
// get autocompletion from cache
|
||||
let cachedAutocompletion: Autocompletion | undefined = undefined
|
||||
let matchInfo: matchInfo | undefined = undefined
|
||||
for (const autocompletion of this._autocompletionsOfDocument[docUriStr].items.values()) {
|
||||
// if the user's change matches up with the generated text
|
||||
matchInfo = getPrefixAutocompletionMatch({ prefix, autocompletion })
|
||||
if (matchInfo !== undefined) {
|
||||
cachedAutocompletion = autocompletion
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// if there is a cached autocompletion, return it
|
||||
if (cachedAutocompletion && matchInfo) {
|
||||
|
||||
// console.log('id: ' + cachedAutocompletion.id)
|
||||
|
||||
if (cachedAutocompletion.status === 'finished') {
|
||||
// console.log('A1')
|
||||
|
||||
const inlineCompletions = toInlineCompletions({ matchInfo, autocompletion: cachedAutocompletion, prefix, suffix, position, debug: true })
|
||||
return inlineCompletions
|
||||
|
||||
} else if (cachedAutocompletion.status === 'pending') {
|
||||
// console.log('A2')
|
||||
|
||||
try {
|
||||
await cachedAutocompletion.llmPromise;
|
||||
const inlineCompletions = toInlineCompletions({ matchInfo, autocompletion: cachedAutocompletion, prefix, suffix, position })
|
||||
return inlineCompletions
|
||||
|
||||
} catch (e) {
|
||||
this._autocompletionsOfDocument[docUriStr].delete(cachedAutocompletion.id)
|
||||
console.error('Error creating autocompletion (1): ' + e)
|
||||
}
|
||||
|
||||
} else if (cachedAutocompletion.status === 'error') {
|
||||
// console.log('A3')
|
||||
}
|
||||
|
||||
return []
|
||||
}
|
||||
|
||||
// else if no more typing happens, then go forwards with the request
|
||||
// wait DEBOUNCE_TIME for the user to stop typing
|
||||
const thisTime = Date.now()
|
||||
this._lastCompletionTime = thisTime
|
||||
const didTypingHappenDuringDebounce = await new Promise((resolve, reject) =>
|
||||
setTimeout(() => {
|
||||
if (this._lastCompletionTime === thisTime) {
|
||||
resolve(false)
|
||||
} else {
|
||||
resolve(true)
|
||||
}
|
||||
}, DEBOUNCE_TIME)
|
||||
)
|
||||
|
||||
// if more typing happened, then do not go forwards with the request
|
||||
if (didTypingHappenDuringDebounce) {
|
||||
return []
|
||||
}
|
||||
|
||||
|
||||
// if there are too many pending requests, cancel the oldest one
|
||||
let numPending = 0
|
||||
let oldestPending: Autocompletion | undefined = undefined
|
||||
for (const autocompletion of this._autocompletionsOfDocument[docUriStr].items.values()) {
|
||||
if (autocompletion.status === 'pending') {
|
||||
numPending += 1
|
||||
if (oldestPending === undefined) {
|
||||
oldestPending = autocompletion
|
||||
}
|
||||
if (numPending >= MAX_PENDING_REQUESTS) {
|
||||
// cancel the oldest pending request and remove it from cache
|
||||
this._autocompletionsOfDocument[docUriStr].delete(oldestPending.id)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const { shouldGenerate, stopTokens: _ } = getCompletionOptions({ prefix, suffix }) // TODO mat
|
||||
|
||||
if (!shouldGenerate) return []
|
||||
|
||||
if (testMode && this._autocompletionId !== 0) { // TODO remove this
|
||||
return []
|
||||
}
|
||||
|
||||
// console.log('B')
|
||||
|
||||
// create a new autocompletion and add it to cache
|
||||
const newAutocompletion: Autocompletion = {
|
||||
id: this._autocompletionId++,
|
||||
prefix: prefix,
|
||||
suffix: suffix,
|
||||
startTime: Date.now(),
|
||||
endTime: undefined,
|
||||
status: 'pending',
|
||||
llmPromise: undefined,
|
||||
insertText: '',
|
||||
requestId: null,
|
||||
}
|
||||
|
||||
// set parameters of `newAutocompletion` appropriately
|
||||
newAutocompletion.llmPromise = new Promise((resolve, reject) => {
|
||||
|
||||
const requestId = this._llmMessageService.sendLLMMessage({
|
||||
logging: { loggingName: 'Autocomplete' },
|
||||
messages: [],
|
||||
onText: async ({ newText, fullText }) => {
|
||||
|
||||
newAutocompletion.insertText = fullText
|
||||
|
||||
// if generation doesn't match the prefix for the first few tokens generated, reject it
|
||||
if (!getPrefixAutocompletionMatch({ prefix: this._lastPrefix, autocompletion: newAutocompletion })) {
|
||||
reject('LLM response did not match user\'s text.')
|
||||
}
|
||||
},
|
||||
onFinalMessage: ({ fullText }) => {
|
||||
|
||||
// newAutocompletion.prefix = prefix
|
||||
// newAutocompletion.suffix = suffix
|
||||
// newAutocompletion.startTime = Date.now()
|
||||
newAutocompletion.endTime = Date.now()
|
||||
// newAutocompletion.abortRef = { current: () => { } }
|
||||
newAutocompletion.status = 'finished'
|
||||
// newAutocompletion.promise = undefined
|
||||
newAutocompletion.insertText = postprocessResult(extractCodeFromResult(fullText))
|
||||
|
||||
resolve(newAutocompletion.insertText)
|
||||
|
||||
},
|
||||
onError: ({ message }) => {
|
||||
newAutocompletion.endTime = Date.now()
|
||||
newAutocompletion.status = 'error'
|
||||
reject(message)
|
||||
},
|
||||
featureName: 'Autocomplete',
|
||||
range: { startLineNumber: position.lineNumber, startColumn: position.column, endLineNumber: position.lineNumber, endColumn: position.column },
|
||||
})
|
||||
newAutocompletion.requestId = requestId
|
||||
|
||||
// if the request hasnt resolved in TIMEOUT_TIME seconds, reject it
|
||||
setTimeout(() => {
|
||||
if (newAutocompletion.status === 'pending') {
|
||||
reject('Timeout receiving message to LLM.')
|
||||
}
|
||||
}, TIMEOUT_TIME)
|
||||
|
||||
})
|
||||
|
||||
|
||||
|
||||
// add autocompletion to cache
|
||||
this._autocompletionsOfDocument[docUriStr].set(newAutocompletion.id, newAutocompletion)
|
||||
|
||||
// show autocompletion
|
||||
try {
|
||||
await newAutocompletion.llmPromise
|
||||
// console.log('id: ' + newAutocompletion.id)
|
||||
|
||||
const matchInfo: matchInfo = { startIdx: 0, lineStart: 0, character: 0 }
|
||||
const inlineCompletions = toInlineCompletions({ matchInfo, autocompletion: newAutocompletion, prefix, suffix, position })
|
||||
return inlineCompletions
|
||||
|
||||
} catch (e) {
|
||||
this._autocompletionsOfDocument[docUriStr].delete(newAutocompletion.id)
|
||||
console.error('Error creating autocompletion (2): ' + e)
|
||||
return []
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
constructor(
|
||||
@ILanguageFeaturesService private _langFeatureService: ILanguageFeaturesService,
|
||||
@ILLMMessageService private readonly _llmMessageService: ILLMMessageService,
|
||||
@IEditorService private readonly _editorService: IEditorService,
|
||||
@IModelService private readonly _modelService: IModelService,
|
||||
) {
|
||||
super()
|
||||
|
||||
this._langFeatureService.inlineCompletionsProvider.register('*', {
|
||||
provideInlineCompletions: async (model, position, context, token) => {
|
||||
const items = await this._provideInlineCompletionItems(model, position, context, token)
|
||||
|
||||
// console.log('item: ', items?.[0]?.insertText)
|
||||
return { items: items, }
|
||||
},
|
||||
freeInlineCompletions: (completions) => {
|
||||
|
||||
// get the `docUriStr` and the `position` of the cursor
|
||||
const activePane = this._editorService.activeEditorPane;
|
||||
if (!activePane) return;
|
||||
const control = activePane.getControl();
|
||||
if (!control || !isCodeEditor(control)) return;
|
||||
const position = control.getPosition();
|
||||
if (!position) return;
|
||||
const resource = EditorResourceAccessor.getCanonicalUri(this._editorService.activeEditor);
|
||||
if (!resource) return;
|
||||
const model = this._modelService.getModel(resource)
|
||||
if (!model) return;
|
||||
const docUriStr = resource.toString();
|
||||
|
||||
const { prefix, } = getPrefixAndSuffix(model, position)
|
||||
|
||||
if (!this._autocompletionsOfDocument[docUriStr]) return;
|
||||
|
||||
// go through cached items and remove matching ones
|
||||
// autocompletion.prefix + autocompletion.insertedText ~== insertedText
|
||||
completions.items.forEach(item => {
|
||||
this._autocompletionsOfDocument[docUriStr].items.forEach((autocompletion: Autocompletion) => {
|
||||
if (removeLeftTabsAndTrimEnd(prefix)
|
||||
=== removeLeftTabsAndTrimEnd(autocompletion.prefix + autocompletion.insertText)
|
||||
) {
|
||||
this._autocompletionsOfDocument[docUriStr].delete(autocompletion.id);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
registerSingleton(IAutocompleteService, AutocompleteService, InstantiationType.Eager);
|
||||
@@ -0,0 +1,276 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Glass Devtools, Inc. All rights reserved.
|
||||
* Void Editor additions licensed under the AGPL 3.0 License.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { diffLines } from '../react/out/diff/index.js'
|
||||
|
||||
export type ComputedDiff = {
|
||||
type: 'edit';
|
||||
originalCode: string;
|
||||
originalStartLine: number;
|
||||
originalEndLine: number;
|
||||
code: string;
|
||||
startLine: number; // 1-indexed
|
||||
endLine: number;
|
||||
} | {
|
||||
type: 'insertion';
|
||||
// originalCode: string;
|
||||
originalStartLine: number; // insertion starts on column 0 of this
|
||||
// originalEndLine: number;
|
||||
code: string;
|
||||
startLine: number;
|
||||
endLine: number;
|
||||
} | {
|
||||
type: 'deletion';
|
||||
originalCode: string;
|
||||
originalStartLine: number;
|
||||
originalEndLine: number;
|
||||
// code: string;
|
||||
startLine: number; // deletion starts on column 0 of this
|
||||
// endLine: number;
|
||||
}
|
||||
|
||||
export function findDiffs(oldStr: string, newStr: string) {
|
||||
|
||||
// this makes it so the end of the file always ends with a \n (if you don't have this, then diffing E vs E\n gives an "edit". With it, you end up diffing E\n vs E\n\n which now properly gives an insertion)
|
||||
newStr += '\n';
|
||||
oldStr += '\n';
|
||||
|
||||
// an ordered list of every original line, line added to the new file, and line removed from the old file (order is unambiguous, think about it)
|
||||
const lineByLineChanges = diffLines(oldStr, newStr);
|
||||
lineByLineChanges.push({ value: '', added: false, removed: false }) // add a dummy so we flush any streaks we haven't yet at the very end (!line.added && !line.removed)
|
||||
|
||||
let oldFileLineNum: number = 1;
|
||||
let newFileLineNum: number = 1;
|
||||
|
||||
let streakStartInNewFile: number | undefined = undefined
|
||||
let streakStartInOldFile: number | undefined = undefined
|
||||
|
||||
const oldStrLines = ('\n' + oldStr).split('\n') // add newline so indexing starts at 1
|
||||
const newStrLines = ('\n' + newStr).split('\n')
|
||||
|
||||
const replacements: ComputedDiff[] = []
|
||||
for (const line of lineByLineChanges) {
|
||||
|
||||
// no change on this line
|
||||
if (!line.added && !line.removed) {
|
||||
|
||||
// do nothing
|
||||
|
||||
// if we were on a streak of +s and -s, end it
|
||||
if (streakStartInNewFile !== undefined) {
|
||||
let type: 'edit' | 'insertion' | 'deletion' = 'edit'
|
||||
|
||||
const startLine = streakStartInNewFile
|
||||
const endLine = newFileLineNum - 1 // don't include current line, the edit was up to this line but not including it
|
||||
|
||||
const originalStartLine = streakStartInOldFile!
|
||||
const originalEndLine = oldFileLineNum - 1 // don't include current line, the edit was up to this line but not including it
|
||||
|
||||
const newContent = newStrLines.slice(startLine, endLine + 1).join('\n')
|
||||
const originalContent = oldStrLines.slice(originalStartLine, originalEndLine + 1).join('\n')
|
||||
|
||||
// if the range is empty, mark it as a deletion / insertion (both won't be true at once)
|
||||
// DELETION
|
||||
if (endLine === startLine - 1) {
|
||||
type = 'deletion'
|
||||
// endLine = startLine
|
||||
}
|
||||
|
||||
// INSERTION
|
||||
else if (originalEndLine === originalStartLine - 1) {
|
||||
type = 'insertion'
|
||||
// originalEndLine = originalStartLine
|
||||
}
|
||||
|
||||
const replacement: ComputedDiff = {
|
||||
type,
|
||||
startLine, endLine,
|
||||
// startCol, endCol,
|
||||
originalStartLine, originalEndLine,
|
||||
// code: newContent,
|
||||
// originalRange: new Range(originalStartLine, originalStartCol, originalEndLine, originalEndCol),
|
||||
originalCode: originalContent,
|
||||
code: newContent,
|
||||
}
|
||||
|
||||
replacements.push(replacement)
|
||||
|
||||
streakStartInNewFile = undefined
|
||||
streakStartInOldFile = undefined
|
||||
}
|
||||
oldFileLineNum += line.count ?? 0;
|
||||
newFileLineNum += line.count ?? 0;
|
||||
}
|
||||
|
||||
// line was removed from old file
|
||||
else if (line.removed) {
|
||||
// if we weren't on a streak, start one on this current line num
|
||||
if (streakStartInNewFile === undefined) {
|
||||
streakStartInNewFile = newFileLineNum
|
||||
streakStartInOldFile = oldFileLineNum
|
||||
}
|
||||
oldFileLineNum += line.count ?? 0 // we processed the line so add 1 (or "count")
|
||||
}
|
||||
|
||||
// line was added to new file
|
||||
else if (line.added) {
|
||||
// if we weren't on a streak, start one on this current line num
|
||||
if (streakStartInNewFile === undefined) {
|
||||
streakStartInNewFile = newFileLineNum
|
||||
streakStartInOldFile = oldFileLineNum
|
||||
}
|
||||
newFileLineNum += line.count ?? 0; // we processed the line so add 1 (or "count")
|
||||
}
|
||||
} // end for
|
||||
|
||||
// console.log('DIFF', { oldStr, newStr, replacements })
|
||||
return replacements
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// // uncomment this to test
|
||||
// let name_ = ''
|
||||
// let testsFailed = 0
|
||||
// const assertEqual = (a: { [s: string]: any }, b: { [s: string]: any }) => {
|
||||
// let keys = new Set([...Object.keys(a), ...Object.keys(b)])
|
||||
// for (let k of keys) {
|
||||
// if (a[k] !== b[k]) {
|
||||
// console.error('Void Test Error:', name_, '\n', `${k}=`, `${JSON.stringify(a[k])}, ${JSON.stringify(b[k])}`)
|
||||
// // console.error(JSON.stringify(a, null, 4))
|
||||
// // console.error(JSON.stringify(b, null, 4))
|
||||
// testsFailed += 1
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// const test = (name: string, fn: () => void) => {
|
||||
// name_ = name
|
||||
// fn()
|
||||
// }
|
||||
|
||||
// const originalCode = `\
|
||||
// A
|
||||
// B
|
||||
// C
|
||||
// D
|
||||
// E`
|
||||
|
||||
// const insertedCode = `\
|
||||
// A
|
||||
// B
|
||||
// C
|
||||
// F
|
||||
// D
|
||||
// E`
|
||||
|
||||
// const modifiedCode = `\
|
||||
// A
|
||||
// B
|
||||
// C
|
||||
// F
|
||||
// E`
|
||||
|
||||
// const modifiedCode2 = `\
|
||||
// A
|
||||
// B
|
||||
// C
|
||||
// D
|
||||
// E
|
||||
// `
|
||||
|
||||
|
||||
// test('Diffs Insertion', () => {
|
||||
// const diffs = findDiffs(originalCode, insertedCode)
|
||||
|
||||
// const expected: BaseDiff = {
|
||||
// type: 'insertion',
|
||||
// originalCode: '',
|
||||
// originalStartLine: 4, // empty range where the insertion happened
|
||||
// originalEndLine: 4,
|
||||
|
||||
// startLine: 4,
|
||||
// startCol: 1,
|
||||
// endLine: 4,
|
||||
// endCol: Number.MAX_SAFE_INTEGER,
|
||||
// }
|
||||
// assertEqual(diffs[0], expected)
|
||||
// })
|
||||
|
||||
// test('Diffs Deletion', () => {
|
||||
// const diffs = findDiffs(insertedCode, originalCode)
|
||||
// assertEqual({ length: diffs.length }, { length: 1 })
|
||||
// const expected: BaseDiff = {
|
||||
// type: 'deletion',
|
||||
// originalCode: 'F',
|
||||
// originalStartLine: 4,
|
||||
// originalEndLine: 4,
|
||||
|
||||
// startLine: 4,
|
||||
// startCol: 1, // empty range where the deletion happened
|
||||
// endLine: 4,
|
||||
// endCol: 1,
|
||||
// }
|
||||
// assertEqual(diffs[0], expected)
|
||||
// })
|
||||
|
||||
// test('Diffs Modification', () => {
|
||||
// const diffs = findDiffs(originalCode, modifiedCode)
|
||||
// assertEqual({ length: diffs.length }, { length: 1 })
|
||||
// const expected: BaseDiff = {
|
||||
// type: 'edit',
|
||||
// originalCode: 'D',
|
||||
// originalStartLine: 4,
|
||||
// originalEndLine: 4,
|
||||
|
||||
// startLine: 4,
|
||||
// startCol: 1,
|
||||
// endLine: 4,
|
||||
// endCol: Number.MAX_SAFE_INTEGER,
|
||||
// }
|
||||
// assertEqual(diffs[0], expected)
|
||||
// })
|
||||
|
||||
// test('Diffs Modification 2', () => {
|
||||
// const diffs = findDiffs(originalCode, modifiedCode2)
|
||||
// assertEqual({ length: diffs.length }, { length: 1 })
|
||||
// const expected: BaseDiff = {
|
||||
// type: 'insertion',
|
||||
// originalCode: '',
|
||||
// originalStartLine: 6,
|
||||
// originalEndLine: 6,
|
||||
|
||||
// startLine: 6,
|
||||
// startCol: 1,
|
||||
// endLine: 6,
|
||||
// endCol: Number.MAX_SAFE_INTEGER,
|
||||
// }
|
||||
// assertEqual(diffs[0], expected)
|
||||
// })
|
||||
|
||||
|
||||
|
||||
// if (testsFailed === 0) {
|
||||
// console.log('✅ Void - All tests passed')
|
||||
// }
|
||||
// else {
|
||||
// console.log('❌ Void - At least one test failed')
|
||||
// }
|
||||
@@ -0,0 +1,20 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Glass Devtools, Inc. All rights reserved.
|
||||
* Void Editor additions licensed under the AGPL 3.0 License.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { isMacintosh } from '../../../../../base/common/platform.js';
|
||||
|
||||
// import { OperatingSystem, OS } from '../../../../base/common/platform.js';
|
||||
// OS === OperatingSystem.Macintosh
|
||||
export function getCmdKey(): string {
|
||||
if (isMacintosh) {
|
||||
return '⌘';
|
||||
} else {
|
||||
return 'Ctrl';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
import { ServicesAccessor } from '../../../../../editor/browser/editorExtensions.js';
|
||||
import { IModelService } from '../../../../../editor/common/services/model.js';
|
||||
import { IClipboardService } from '../../../../../platform/clipboard/common/clipboardService.js';
|
||||
import { IContextViewService, IContextMenuService } from '../../../../../platform/contextview/browser/contextView.js';
|
||||
import { IFileService } from '../../../../../platform/files/common/files.js';
|
||||
import { IHoverService } from '../../../../../platform/hover/browser/hover.js';
|
||||
import { IThemeService } from '../../../../../platform/theme/common/themeService.js';
|
||||
import { ILLMMessageService } from '../../../../../platform/void/common/llmMessageService.js';
|
||||
import { IRefreshModelService } from '../../../../../platform/void/common/refreshModelService.js';
|
||||
import { IVoidSettingsService } from '../../../../../platform/void/common/voidSettingsService.js';
|
||||
import { IInlineDiffsService } from '../inlineDiffsService.js';
|
||||
import { ISidebarStateService } from '../sidebarStateService.js';
|
||||
import { IThreadHistoryService } from '../threadHistoryService.js';
|
||||
|
||||
export type ReactServicesType = {
|
||||
sidebarStateService: ISidebarStateService;
|
||||
settingsStateService: IVoidSettingsService;
|
||||
threadsStateService: IThreadHistoryService;
|
||||
fileService: IFileService;
|
||||
modelService: IModelService;
|
||||
inlineDiffService: IInlineDiffsService;
|
||||
llmMessageService: ILLMMessageService;
|
||||
clipboardService: IClipboardService;
|
||||
refreshModelService: IRefreshModelService;
|
||||
|
||||
themeService: IThemeService,
|
||||
hoverService: IHoverService,
|
||||
|
||||
contextViewService: IContextViewService;
|
||||
contextMenuService: IContextMenuService;
|
||||
}
|
||||
|
||||
|
||||
export const getReactServices = (accessor: ServicesAccessor): ReactServicesType => {
|
||||
return {
|
||||
settingsStateService: accessor.get(IVoidSettingsService),
|
||||
sidebarStateService: accessor.get(ISidebarStateService),
|
||||
threadsStateService: accessor.get(IThreadHistoryService),
|
||||
fileService: accessor.get(IFileService),
|
||||
modelService: accessor.get(IModelService),
|
||||
inlineDiffService: accessor.get(IInlineDiffsService),
|
||||
llmMessageService: accessor.get(ILLMMessageService),
|
||||
clipboardService: accessor.get(IClipboardService),
|
||||
themeService: accessor.get(IThemeService),
|
||||
hoverService: accessor.get(IHoverService),
|
||||
refreshModelService: accessor.get(IRefreshModelService),
|
||||
contextViewService: accessor.get(IContextViewService),
|
||||
contextMenuService: accessor.get(IContextMenuService),
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,20 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Glass Devtools, Inc. All rights reserved.
|
||||
* Void Editor additions licensed under the AGPL 3.0 License.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
.monaco-editor .void-sweepIdxBG {
|
||||
background-color: var(--vscode-void-sweepIdxBG);
|
||||
}
|
||||
|
||||
.void-sweepBG {
|
||||
background-color: var(--vscode-void-sweepBG);
|
||||
}
|
||||
|
||||
.void-greenBG {
|
||||
background-color: var(--vscode-void-greenBG);
|
||||
}
|
||||
|
||||
.void-redBG {
|
||||
background-color: var(--vscode-void-redBG);
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Glass Devtools, Inc. All rights reserved.
|
||||
* Void Editor additions licensed under the AGPL 3.0 License.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { CodeSelection } from '../threadHistoryService.js';
|
||||
|
||||
export const stringifySelections = (selections: CodeSelection[]) => {
|
||||
|
||||
|
||||
|
||||
return selections.map(({ fileURI, content, selectionStr }) =>
|
||||
`\
|
||||
File: ${fileURI.fsPath}
|
||||
\`\`\`
|
||||
${content // this was the enite file which is foolish
|
||||
}
|
||||
\`\`\`${selectionStr === null ? '' : `
|
||||
Selection: ${selectionStr}`}
|
||||
`).join('\n')
|
||||
}
|
||||
|
||||
|
||||
export const userInstructionsStr = (instructions: string, selections: CodeSelection[] | null) => {
|
||||
let str = '';
|
||||
if (selections && selections.length > 0) {
|
||||
str += stringifySelections(selections);
|
||||
str += `Please edit the selected code following these instructions:\n`
|
||||
}
|
||||
str += `${instructions}`;
|
||||
return str;
|
||||
};
|
||||
+126
-9
@@ -1,5 +1,127 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Glass Devtools, Inc. All rights reserved.
|
||||
* Void Editor additions licensed under the AGPL 3.0 License.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
const generateDiffInstructions = `
|
||||
// // used for ctrl+l
|
||||
// const partialGenerationInstructions = ``
|
||||
|
||||
|
||||
// // used for ctrl+k, autocomplete
|
||||
// const fimInstructions = ``
|
||||
|
||||
|
||||
// CTRL+K prompt:
|
||||
// const promptContent = `Here is the user's original selection:
|
||||
// \`\`\`
|
||||
// <MID>${selection}</MID>
|
||||
// \`\`\`
|
||||
|
||||
// The user wants to apply the following instructions to the selection:
|
||||
// ${instructions}
|
||||
|
||||
// Please rewrite the selection following the user's instructions.
|
||||
|
||||
// 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
|
||||
|
||||
// Complete the following:
|
||||
// \`\`\`
|
||||
// <PRE>${prefix}</PRE>
|
||||
// <SUF>${suffix}</SUF>
|
||||
// <MID>`;
|
||||
|
||||
export const generateCtrlLInstructions = `\
|
||||
You are a coding assistant. You are given a list of relevant files \`files\`, a selection that the user is making \`selection\`, and instructions to follow \`instructions\`.
|
||||
|
||||
Please edit the selected file following the user's instructions (or, if appropriate, answer their question instead).
|
||||
|
||||
Instructions:
|
||||
1. Output the changes to make to the entire file.
|
||||
1. Do not re-write the entire file.
|
||||
3. Instead, you may use code elision to represent unchanged portions of code. For example, write "existing code..." in code comments.
|
||||
4. You must give enough context to apply the change in the correct location.
|
||||
|
||||
## EXAMPLE
|
||||
|
||||
FILES
|
||||
selected file \`math.ts\`:
|
||||
\`\`\`
|
||||
const addNumbers = (a, b) => a + b
|
||||
const subtractNumbers = (a, b) => a - b
|
||||
const divideNumbers = (a, b) => a / b
|
||||
\`\`\`
|
||||
|
||||
SELECTION
|
||||
\`\`\`
|
||||
const subtractNumbers = (a, b) => a - b
|
||||
\`\`\`
|
||||
|
||||
INSTRUCTIONS
|
||||
\`\`\`
|
||||
add a function that multiplies numbers below this
|
||||
\`\`\`
|
||||
|
||||
EXPECTED OUTPUT
|
||||
We can add the following code to the file:
|
||||
\`\`\`
|
||||
// existing code...
|
||||
const subtractNumbers = (a, b) => a - b;
|
||||
const multiplyNumbers = (a, b) => a * b;
|
||||
// existing code...
|
||||
\`\`\`
|
||||
|
||||
## EXAMPLE
|
||||
|
||||
FILES
|
||||
selected file \`fib.ts\`:
|
||||
\`\`\`
|
||||
|
||||
const dfs = (root) => {
|
||||
if (!root) return;
|
||||
console.log(root.val);
|
||||
dfs(root.left);
|
||||
dfs(root.right);
|
||||
}
|
||||
const fib = (n) => {
|
||||
if (n < 1) return 1
|
||||
return fib(n - 1) + fib(n - 2)
|
||||
}
|
||||
\`\`\`
|
||||
|
||||
SELECTION
|
||||
\`\`\`
|
||||
return fib(n - 1) + fib(n - 2)
|
||||
\`\`\`
|
||||
|
||||
INSTRUCTIONS
|
||||
\`\`\`
|
||||
memoize results
|
||||
\`\`\`
|
||||
|
||||
EXPECTED OUTPUT
|
||||
To implement memoization in your Fibonacci function, you can use a JavaScript object to store previously computed results. This will help avoid redundant calculations and improve performance. Here's how you can modify your function:
|
||||
\`\`\`
|
||||
// existing code...
|
||||
const fib = (n, memo = {}) => {
|
||||
if (n < 1) return 1;
|
||||
if (memo[n]) return memo[n]; // Check if result is already computed
|
||||
memo[n] = fib(n - 1, memo) + fib(n - 2, memo); // Store result in memo
|
||||
return memo[n];
|
||||
}
|
||||
\`\`\`
|
||||
Explanation:
|
||||
Memoization Object: A memo object is used to store the results of Fibonacci calculations for each n.
|
||||
Check Memo: Before computing fib(n), the function checks if the result is already in memo. If it is, it returns the stored result.
|
||||
Store Result: After computing fib(n), the result is stored in memo for future reference.
|
||||
|
||||
## END EXAMPLES
|
||||
`
|
||||
|
||||
export const generateDiffInstructions = `
|
||||
You are a coding assistant. You are given a list of relevant files \`files\`, a selection that the user is making \`selection\`, and instructions to follow \`instructions\`.
|
||||
|
||||
Please edit the selected file following the user's instructions (or, if appropriate, answer their question instead).
|
||||
@@ -160,7 +282,7 @@ We should change all the buttons like the one selected into a div component. Her
|
||||
`;
|
||||
|
||||
|
||||
const searchDiffChunkInstructions = `
|
||||
export const searchDiffChunkInstructions = `
|
||||
You are a coding assistant that applies a diff to a file. You are given a diff \`diff\`, a list of files \`files\` to apply the diff to, and a selection \`selection\` that you are currently considering in the file.
|
||||
|
||||
Determine whether you should modify ANY PART of the selection \`selection\` following the \`diff\`. Return \`true\` if you should modify any part of the selection, and \`false\` if you should not modify any part of it.
|
||||
@@ -269,10 +391,10 @@ OUTPUT
|
||||
`
|
||||
|
||||
|
||||
const writeFileWithDiffInstructions = `
|
||||
export const writeFileWithDiffInstructions = `
|
||||
You are a coding assistant that applies a diff to a file. You are given the original file \`original_file\`, a diff \`diff\`, and a new file that you are applying the diff to \`new_file\`.
|
||||
|
||||
Please finish writing the new file \`new_file\`, according to the diff \`diff\`.
|
||||
Please finish writing the new file \`new_file\`, according to the diff \`diff\`. You must completely re-write the whole file, using the diff.
|
||||
|
||||
Directions:
|
||||
1. Continue exactly where the new file \`new_file\` left off.
|
||||
@@ -399,8 +521,3 @@ export default Sidebar;\`\`\`
|
||||
|
||||
|
||||
|
||||
export {
|
||||
generateDiffInstructions,
|
||||
searchDiffChunkInstructions,
|
||||
writeFileWithDiffInstructions,
|
||||
};
|
||||
@@ -0,0 +1,2 @@
|
||||
out/
|
||||
src2/
|
||||
@@ -0,0 +1,10 @@
|
||||
|
||||
Run `node build.js` to compile the React into `out/`.
|
||||
|
||||
A couple things to remember:
|
||||
|
||||
- Make sure to add .js at the end of any external imports used in here, e.g. ../../../../../my_file.js. If you don't do this, you will get untraceable errors.
|
||||
|
||||
- src/ needs to be shallow (1 folder deep) so the detection of externals works properly (see tsup.config.js).
|
||||
|
||||
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Glass Devtools, Inc. All rights reserved.
|
||||
* Void Editor additions licensed under the AGPL 3.0 License.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { execSync } from 'child_process';
|
||||
|
||||
// clear temp dirs
|
||||
execSync('npx rimraf out/ && npx rimraf src2/')
|
||||
|
||||
// build and scope tailwind: https://www.npmjs.com/package/scope-tailwind
|
||||
execSync('npx scope-tailwind ./src -o src2/ -s void-scope -c styles.css -p "void-" ')
|
||||
|
||||
// tsup to build src2/ into out/
|
||||
execSync('npx tsup')
|
||||
|
||||
|
||||
console.log('✅ Done building! Kill your build script(s) (Ctrl+D in them), then press Cmd+Shift+B again.')
|
||||
@@ -0,0 +1,8 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Glass Devtools, Inc. All rights reserved.
|
||||
* Void Editor additions licensed under the AGPL 3.0 License.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { diffLines, Change } from 'diff';
|
||||
|
||||
export { diffLines, Change }
|
||||
+9
-5
@@ -1,9 +1,14 @@
|
||||
import React, { ReactNode, useCallback, useEffect, useState } from "react"
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Glass Devtools, Inc. All rights reserved.
|
||||
* Void Editor additions licensed under the AGPL 3.0 License.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import React, { ReactNode } from "react"
|
||||
import SyntaxHighlighter from "react-syntax-highlighter";
|
||||
import { atomOneDarkReasonable } from "react-syntax-highlighter/dist/esm/styles/hljs";
|
||||
|
||||
|
||||
const BlockCode = ({ text, buttonsOnHover, language }: { text: string, buttonsOnHover?: ReactNode, language?: string }) => {
|
||||
export const BlockCode = ({ text, buttonsOnHover, language }: { text: string, buttonsOnHover?: ReactNode, language?: string }) => {
|
||||
|
||||
const customStyle = {
|
||||
...atomOneDarkReasonable,
|
||||
@@ -16,9 +21,9 @@ const BlockCode = ({ text, buttonsOnHover, language }: { text: string, buttonsOn
|
||||
return (<>
|
||||
<div className={`relative group w-full bg-vscode-sidebar-bg overflow-hidden isolate`}>
|
||||
|
||||
{!toolbar ? null : (
|
||||
{buttonsOnHover === null ? null : (
|
||||
<div className="absolute top-0 right-0 opacity-0 group-hover:opacity-100 duration-200">
|
||||
<div className="flex space-x-2 p-2">{buttonsOnHover === null ? null : buttonsOnHover}</div>
|
||||
<div className="flex space-x-2 p-2">{buttonsOnHover}</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -39,4 +44,3 @@ const BlockCode = ({ text, buttonsOnHover, language }: { text: string, buttonsOn
|
||||
)
|
||||
}
|
||||
|
||||
export default BlockCode
|
||||
+24
-20
@@ -1,19 +1,28 @@
|
||||
import React, { JSX, useCallback, useEffect, useState } from "react"
|
||||
import { marked, MarkedToken, Token, TokensList } from "marked"
|
||||
import BlockCode from "./BlockCode"
|
||||
import { getVSCodeAPI } from "../getVscodeApi"
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Glass Devtools, Inc. All rights reserved.
|
||||
* Void Editor additions licensed under the AGPL 3.0 License.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import React, { JSX, useCallback, useEffect, useState } from 'react'
|
||||
import { marked, MarkedToken, Token } from 'marked'
|
||||
import { BlockCode } from './BlockCode.js'
|
||||
import { useService } from '../util/services.js'
|
||||
|
||||
|
||||
enum CopyButtonState {
|
||||
Copy = "Copy",
|
||||
Copied = "Copied!",
|
||||
Error = "Could not copy",
|
||||
Copy = 'Copy',
|
||||
Copied = 'Copied!',
|
||||
Error = 'Could not copy',
|
||||
}
|
||||
|
||||
const COPY_FEEDBACK_TIMEOUT = 1000 // amount of time to say 'Copied!'
|
||||
|
||||
const CodeButtonsOnHover = ({ diffRepr: text }: { diffRepr: string }) => {
|
||||
const [copyButtonState, setCopyButtonState] = useState(CopyButtonState.Copy)
|
||||
const inlineDiffService = useService('inlineDiffService')
|
||||
|
||||
const clipboardService = useService('clipboardService')
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
if (copyButtonState !== CopyButtonState.Copy) {
|
||||
@@ -24,15 +33,10 @@ const CodeButtonsOnHover = ({ diffRepr: text }: { diffRepr: string }) => {
|
||||
}, [copyButtonState])
|
||||
|
||||
const onCopy = useCallback(() => {
|
||||
navigator.clipboard.writeText(text).then(
|
||||
() => {
|
||||
setCopyButtonState(CopyButtonState.Copied)
|
||||
},
|
||||
() => {
|
||||
setCopyButtonState(CopyButtonState.Error)
|
||||
}
|
||||
)
|
||||
}, [text])
|
||||
clipboardService.writeText(text)
|
||||
.then(() => { setCopyButtonState(CopyButtonState.Copied) })
|
||||
.catch(() => { setCopyButtonState(CopyButtonState.Error) })
|
||||
}, [text, clipboardService])
|
||||
|
||||
return <>
|
||||
<button
|
||||
@@ -44,7 +48,8 @@ const CodeButtonsOnHover = ({ diffRepr: text }: { diffRepr: string }) => {
|
||||
<button
|
||||
className="btn btn-secondary btn-sm border border-vscode-input-border rounded"
|
||||
onClick={async () => {
|
||||
getVSCodeAPI().postMessage({ type: "applyChanges", diffRepr: text })
|
||||
|
||||
inlineDiffService.startStreaming({ featureName: 'Ctrl+L' }, text)
|
||||
}}
|
||||
>
|
||||
Apply
|
||||
@@ -125,7 +130,7 @@ const RenderToken = ({ token, nested = false }: { token: Token | string, nested?
|
||||
{item.task && (
|
||||
<input type="checkbox" checked={item.checked} readOnly />
|
||||
)}
|
||||
<MarkdownRender string={item.text} nested={true} />
|
||||
<ChatMarkdownRender string={item.text} nested={true} />
|
||||
</li>
|
||||
))}
|
||||
</ListTag>
|
||||
@@ -209,7 +214,7 @@ const RenderToken = ({ token, nested = false }: { token: Token | string, nested?
|
||||
)
|
||||
}
|
||||
|
||||
const MarkdownRender = ({ string, nested = false }: { string: string, nested?: boolean }) => {
|
||||
export const ChatMarkdownRender = ({ string, nested = false }: { string: string, nested?: boolean }) => {
|
||||
const tokens = marked.lexer(string); // https://marked.js.org/using_pro#renderer
|
||||
return (
|
||||
<>
|
||||
@@ -220,4 +225,3 @@ const MarkdownRender = ({ string, nested = false }: { string: string, nested?: b
|
||||
)
|
||||
}
|
||||
|
||||
export default MarkdownRender
|
||||
@@ -0,0 +1,66 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Glass Devtools, Inc. All rights reserved.
|
||||
* Void Editor additions licensed under the AGPL 3.0 License.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import React, { Component, ErrorInfo, ReactNode } from 'react';
|
||||
import { ErrorDisplay } from './ErrorDisplay.js';
|
||||
|
||||
interface Props {
|
||||
children: ReactNode;
|
||||
fallback?: ReactNode;
|
||||
onDismiss?: () => void;
|
||||
}
|
||||
|
||||
interface State {
|
||||
hasError: boolean;
|
||||
error: Error | null;
|
||||
errorInfo: ErrorInfo | null;
|
||||
}
|
||||
|
||||
class ErrorBoundary extends Component<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
hasError: false,
|
||||
error: null,
|
||||
errorInfo: null
|
||||
};
|
||||
}
|
||||
|
||||
static getDerivedStateFromError(error: Error): Partial<State> {
|
||||
return {
|
||||
hasError: true,
|
||||
error
|
||||
};
|
||||
}
|
||||
|
||||
componentDidCatch(error: Error, errorInfo: ErrorInfo): void {
|
||||
this.setState({
|
||||
error,
|
||||
errorInfo
|
||||
});
|
||||
}
|
||||
|
||||
render(): ReactNode {
|
||||
if (this.state.hasError && this.state.error) {
|
||||
// If a custom fallback is provided, use it
|
||||
if (this.props.fallback) {
|
||||
return this.props.fallback;
|
||||
}
|
||||
|
||||
// Use ErrorDisplay component as the default error UI
|
||||
return (
|
||||
<ErrorDisplay
|
||||
message={this.state.error + ''}
|
||||
fullError={this.state.error}
|
||||
onDismiss={this.props.onDismiss || null}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return this.props.children;
|
||||
}
|
||||
}
|
||||
|
||||
export default ErrorBoundary;
|
||||
@@ -0,0 +1,87 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Glass Devtools, Inc. All rights reserved.
|
||||
* Void Editor additions licensed under the AGPL 3.0 License.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import { AlertCircle, ChevronDown, ChevronUp, X } from 'lucide-react';
|
||||
|
||||
|
||||
export const ErrorDisplay = ({
|
||||
message,
|
||||
fullError,
|
||||
onDismiss,
|
||||
showDismiss,
|
||||
}: {
|
||||
message: string,
|
||||
fullError: Error | null,
|
||||
onDismiss: (() => void) | null,
|
||||
showDismiss?: boolean,
|
||||
}) => {
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
|
||||
let details: string | null = null;
|
||||
|
||||
if (fullError === null) {
|
||||
details = null
|
||||
}
|
||||
else if (typeof fullError === 'object') {
|
||||
details = JSON.stringify(fullError, null, 2)
|
||||
}
|
||||
else if (typeof fullError === 'string') {
|
||||
details = null
|
||||
}
|
||||
|
||||
|
||||
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">
|
||||
{/* eg Error */}
|
||||
Error
|
||||
</h3>
|
||||
<p className="text-red-700 mt-1">
|
||||
{/* eg Something went wrong */}
|
||||
{message}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
{details && (
|
||||
<button className="text-red-600 hover:text-red-800 p-1 rounded"
|
||||
onClick={() => setIsExpanded(!isExpanded)}
|
||||
>
|
||||
{isExpanded ? (
|
||||
<ChevronUp 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"
|
||||
onClick={onDismiss}
|
||||
>
|
||||
<X className="h-5 w-5" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Expandable Details */}
|
||||
{isExpanded && details && (
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user