Compare commits
210 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8a6daa5766 | |||
| 9ec4f3da01 | |||
| 86c9a17f8d | |||
| d21f2c4d62 | |||
| 733256feab | |||
| f9ce9de316 | |||
| 9f27b17f11 | |||
| 8ff4f1c6e1 | |||
| f6f6259f23 | |||
| c7e9175fd7 | |||
| bdc3c9cb7e | |||
| 790a2b6a4f | |||
| e6c779d91a | |||
| aa95ceafd2 | |||
| 9ef1aa2c60 | |||
| e4107e1257 | |||
| 1d5fef43c1 | |||
| 51280d4e77 | |||
| d5ccc6f777 | |||
| c58af2a3af | |||
| d857d54847 | |||
| a7cbed395b | |||
| f6479a0668 | |||
| a50b5c6407 | |||
| 53d19d819c | |||
| 1bb243bc34 | |||
| faaf3a738a | |||
| e8bbe4bf5a | |||
| 469bba178a | |||
| 879595dbe6 | |||
| f7d2e95d37 | |||
| 9dbc7f5caa | |||
| 1cc35b1d79 | |||
| 2473f210bc | |||
| 604578bb19 | |||
| 1d73aad2ed | |||
| 8508c2dd8e | |||
| a8c5c696be | |||
| 077d460b29 | |||
| f3d9893588 | |||
| c004e1af03 | |||
| 0d227f2bd6 | |||
| 0b96901aee | |||
| cd77542a9e | |||
| 0c2660e283 | |||
| 9a591ba29f | |||
| 4c633ee8ff | |||
| 67e9759d35 | |||
| 93dac201b3 | |||
| 946ec70136 | |||
| 161682cf75 | |||
| 6df0093786 | |||
| a527af93b7 | |||
| 775cb2e4ac | |||
| aacbef3e33 | |||
| f5b2dcfe0a | |||
| 9d25fab5cb | |||
| 250b90634f | |||
| e56716c792 | |||
| 0bf5278168 | |||
| f2c61d2419 | |||
| bdd224990d | |||
| 0ca2ad4930 | |||
| 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 | |||
| 9fc3e413a9 | |||
| 289da6bd6d | |||
| 1feb7ec485 | |||
| 5f97a4f8a6 | |||
| 752ebfff3b | |||
| 01d99645a6 | |||
| 00814d51f1 | |||
| e8ff4d8328 | |||
| 8da76e72df | |||
| 652f64cd14 | |||
| b52f0d8dd7 | |||
| f8917e7548 | |||
| 932e9fe9c6 | |||
| 3fd4357503 | |||
| 9b878a9fc0 | |||
| 268d03fad5 | |||
| 3aece81891 | |||
| 6b268a44bb | |||
| 70ce23862b | |||
| 1c32d3a1c9 | |||
| 2814c63765 | |||
| cc8c4d33ef | |||
| ada31ccf90 | |||
| 4a629be2e2 | |||
| 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 | |||
| 6d12737fda | |||
| d3962acd30 | |||
| 18557c8f6e | |||
| 29b037726e | |||
| eae1af331f | |||
| 2e92da500f | |||
| 59dc193f97 | |||
| bd778d259b | |||
| 605cf236a5 | |||
| 8bc3a0d277 | |||
| d80c88bc38 | |||
| dd34a5be57 | |||
| f4be80b724 | |||
| b40c58b78a | |||
| 64468b6f4d | |||
| 8f83b063f1 | |||
| 9eb897b655 | |||
| a738ca0b2c | |||
| f56772a07a | |||
| 38662d2824 | |||
| a51873c06a |
@@ -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.
|
||||
|
||||
|
||||
@@ -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";
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
+58
-64
@@ -1,51 +1,48 @@
|
||||
# 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))
|
||||
- Suggest New Features ([Discord](https://discord.gg/RSNjgaugJs))
|
||||
- Build New Features ([Project](https://github.com/orgs/voideditor/projects/2/views/3))
|
||||
- Submit Issues/Docs/Bugs ([Issues](https://github.com/voideditor/void/issues))
|
||||
|
||||
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).
|
||||
|
||||
For some useful links we've compiled see [`VOID_USEFUL_LINKS.md`](https://github.com/voideditor/void/blob/main/VOID_USEFUL_LINKS.md).
|
||||
## Building the full IDE
|
||||
|
||||
## 1. Building the Extension
|
||||
Please follow the steps below to build the IDE. If you have any questions, feel free to [submit an issue](https://github.com/voideditor/void/issues/new) with any build errors, or refer to VSCode's full [How to Contribute](https://github.com/microsoft/vscode/wiki/How-to-Contribute) page.
|
||||
|
||||
Here's how you can start contributing to the Void extension. This is where you should get started if you're new.
|
||||
|
||||
1. Clone the repository:
|
||||
### a. Build Prerequisites - Mac
|
||||
|
||||
```
|
||||
git clone https://github.com/voideditor/void
|
||||
```
|
||||
If you're using a Mac, make sure you have Python and XCode installed (you probably do 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, make sure you've installed NodeJS and run `npm install -g node-gyp`. Then:
|
||||
- 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).
|
||||
|
||||
## 2. Building the full IDE
|
||||
### Build instructions
|
||||
|
||||
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.
|
||||
|
||||
### a. Building on a Mac
|
||||
|
||||
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).
|
||||
To build Void, first follow the prerequisite steps above for your operating system and open `void/` inside VSCode. Then:
|
||||
|
||||
1. Install all dependencies.
|
||||
|
||||
@@ -53,9 +50,15 @@ To build on a Mac, open `void/` in VSCode. Make sure you've built the extension
|
||||
npm install
|
||||
```
|
||||
|
||||
2. Run `npm run watch`.
|
||||
2. Run `cd ./src/vs/workbench/contrib/void/browser/react/` and then `node ./build.js` to build Void's external dependencies (our React components, etc).
|
||||
|
||||
This can take ~5 min. It's done when you see something like:
|
||||
3. Press <kbd>Ctrl+Shift+B</kbd>, or if you prefer using the terminal run `npm run watch`.
|
||||
|
||||
This can take ~5 min.
|
||||
|
||||
If you ran <kbd>Ctrl+Shift+B</kbd>, the build is done when you see two check marks.
|
||||
|
||||
If you ran `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
|
||||
@@ -66,48 +69,36 @@ This can take ~5 min. It's done when you see something like:
|
||||
|
||||
<!-- 3. Press <kbd>Ctrl+Shift+B</kbd> to start the build process. -->
|
||||
|
||||
3. In a new terminal, run `./scripts/code.sh`.
|
||||
4. In a new terminal, run `./scripts/code.sh` (Mac/Linux) or `./scripts/code.bat` (Windows). This 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.
|
||||
|
||||
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".
|
||||
Now that you're set up, feel free to check out our [Issues](https://github.com/voideditor/void/issues) page.
|
||||
|
||||
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 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 see `[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` or do a clean install of your npm dependencies.
|
||||
|
||||
|
||||
```
|
||||
npm config set msvs_version 2022
|
||||
```
|
||||
-->
|
||||
## Bundling
|
||||
|
||||
To bundle the IDE into an executable, 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)`
|
||||
|
||||
|
||||
## Roadmap
|
||||
|
||||
Here are the most important topics on our Roadmap. More ⭐'s = more important.
|
||||
|
||||
These sometimes get outdated - please refer to our Issues page for the latest issues.
|
||||
Here are the most important topics on our Roadmap. More ⭐'s = more important. Please refer to our [Issues](https://github.com/voideditor/void/issues) page for the latest issues.
|
||||
|
||||
## ⭐⭐⭐ Make History work well.
|
||||
|
||||
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.
|
||||
|
||||
## ⭐⭐⭐ Build Cursor-style quick edits (ctrl+k).
|
||||
## ⭐⭐⭐ Build Cursor-style quick edits (Ctrl+K).
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
@@ -117,21 +108,19 @@ Eventually, we want to build a convenient API for creating AI tools. The API wil
|
||||
|
||||
## ⭐ One-stars.
|
||||
|
||||
⭐ Let the user accept / reject all Diffs in an entire file via the sidebar.
|
||||
⭐ 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 us a message in the #general channel of the [Discord](https://discord.gg/RSNjgaugJs) for any reason. Please check in especially if you want to make a lot of changes or build a large new feature.
|
||||
|
||||
# Submitting a Pull Request
|
||||
|
||||
Please submit a pull request once you've made a change. Here are a few guidelines:
|
||||
|
||||
- A PR should be about one _single_ feature change. The fewer items you change, the more likely the PR is to be accepted.
|
||||
## Submitting a Pull Request
|
||||
|
||||
- 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).
|
||||
Please submit a pull request once you've made a change. You don't need to submit an issue.
|
||||
|
||||
- Try to avoid refactoring and making feature changes in the same PR.
|
||||
Please don't use AI to write your PR 🙂.
|
||||
|
||||
# Relevant files
|
||||
|
||||
@@ -156,3 +145,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.
|
||||
|
||||
@@ -4,6 +4,8 @@ The Void team put together this list of links to get up and running with VSCode'
|
||||
|
||||
## Beginners / Getting started
|
||||
|
||||
<!-- - [Void's Guide](https://voideditor.com) to VSCode - we put together covers VSCode's source. -->
|
||||
|
||||
- [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.
|
||||
@@ -12,13 +14,12 @@ The Void team put together this list of links to get up and running with VSCode'
|
||||
|
||||
- [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.
|
||||
|
||||
|
||||
- [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 - sometimes useful to reference (especially if you're building an extension).
|
||||
|
||||
|
||||
## 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.*
|
||||
@@ -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.
|
||||
|
||||
@@ -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,115 +0,0 @@
|
||||
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,315 +0,0 @@
|
||||
import * as vscode from 'vscode';
|
||||
import { findDiffs } from './findDiffs';
|
||||
import { Diff, BaseDiffArea, BaseDiff, DiffArea } from './common/shared_types';
|
||||
|
||||
|
||||
|
||||
// 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:...
|
||||
})
|
||||
|
||||
|
||||
// responsible for displaying diffs and showing accept/reject buttons
|
||||
export class DisplayChangesProvider {
|
||||
|
||||
private _diffAreasOfDocument: { [docUriStr: string]: DiffArea[] } = {}
|
||||
private _diffsOfDocument: { [docUriStr: string]: Diff[] } = {}
|
||||
|
||||
private _diffareaidPool = 0
|
||||
private _diffidPool = 0
|
||||
private _weAreEditing: boolean = false
|
||||
|
||||
private _onDidChangeDiffsEvent: vscode.EventEmitter<void> = new vscode.EventEmitter<void>(); // signals a UI refresh on .fire() events
|
||||
|
||||
// declared by us, registered with vscode.languages.registerCodeLensProvider()
|
||||
constructor() {
|
||||
console.log('Creating DisplayChangesProvider')
|
||||
|
||||
// update diffs whenever the event fires
|
||||
this._onDidChangeDiffsEvent.event(() => {
|
||||
const editor = vscode.window.activeTextEditor
|
||||
if (!editor) return
|
||||
|
||||
let document = editor.document
|
||||
const docUriStr = document.uri.toString()
|
||||
return this._diffsOfDocument[docUriStr]?.flatMap(diff => diff.lenses) ?? []
|
||||
})
|
||||
|
||||
// this acts as a useEffect. Every time text changes, run this
|
||||
vscode.workspace.onDidChangeTextDocument((e) => {
|
||||
|
||||
const editor = vscode.window.activeTextEditor
|
||||
|
||||
if (!editor)
|
||||
return
|
||||
if (this._weAreEditing)
|
||||
return
|
||||
|
||||
const docUri = editor.document.uri
|
||||
const docUriStr = docUri.toString()
|
||||
const diffAreas = this._diffAreasOfDocument[docUriStr] || []
|
||||
|
||||
// loop through each change
|
||||
for (const change of e.contentChanges) {
|
||||
|
||||
// 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.range.end.line - change.range.start.line
|
||||
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.range.start.line >= diffArea.startLine && change.range.end.line <= diffArea.endLine) {
|
||||
diffArea.endLine += deltaNewlines
|
||||
}
|
||||
// check if the `diffArea` was fully deleted and remove it if so
|
||||
if (diffArea.startLine > diffArea.endLine) {
|
||||
//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.startLine > change.range.end.line) {
|
||||
diffArea.startLine += deltaNewlines
|
||||
diffArea.endLine += deltaNewlines
|
||||
}
|
||||
}
|
||||
|
||||
// TODO merge any diffAreas if they overlap with each other as a result from the shift
|
||||
|
||||
}
|
||||
|
||||
// refresh the diffAreas
|
||||
this.refreshDiffAreas(docUri)
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
// used by us only
|
||||
public addDiffArea(uri: vscode.Uri, diffArea: BaseDiffArea) {
|
||||
|
||||
const uriStr = uri.toString()
|
||||
|
||||
// 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 > diffArea.endLine || da.endLine < diffArea.startLine
|
||||
|
||||
if (!noOverlap) return false
|
||||
|
||||
return true
|
||||
})
|
||||
|
||||
// add `diffArea` to storage
|
||||
this._diffAreasOfDocument[uriStr].push({
|
||||
...diffArea,
|
||||
diffareaid: this._diffareaidPool
|
||||
})
|
||||
this._diffareaidPool += 1
|
||||
}
|
||||
|
||||
|
||||
// used by us only
|
||||
public refreshDiffAreas(docUri: vscode.Uri) {
|
||||
|
||||
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 docUriStr = docUri.toString()
|
||||
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 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(diffArea.originalCode, currentCode)
|
||||
|
||||
// add the diffs to `this._diffsOfDocument[docUriStr]`
|
||||
this.addDiffs(editor.document.uri, diffs, diffArea)
|
||||
|
||||
// // print diffs
|
||||
console.log('!CodeBefore:', JSON.stringify(diffArea.originalCode))
|
||||
console.log('!CodeAfter:', JSON.stringify(currentCode))
|
||||
console.log('DiffRepr: ', diffs.map(diff => diff.code).join('\n'))
|
||||
for (const diff of this._diffsOfDocument[docUriStr]) {
|
||||
console.log('------------')
|
||||
console.log('deletedCode:', JSON.stringify(diff.deletedCode))
|
||||
console.log('insertedCode:', JSON.stringify(diff.insertedCode))
|
||||
console.log('deletedRange:', diff.deletedRange.start.line, diff.deletedRange.end.line,)
|
||||
console.log('insertedRange:', diff.insertedRange.start.line, diff.insertedRange.end.line,)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// update green highlighting
|
||||
editor.setDecorations(
|
||||
greenDecoration,
|
||||
(this._diffsOfDocument[docUriStr]
|
||||
.filter(diff => diff.insertedRange !== undefined)
|
||||
.map(diff => diff.insertedRange)
|
||||
)
|
||||
);
|
||||
|
||||
// TODO update red highlighting
|
||||
// this._diffsOfDocument[docUriStr].map(diff => diff.deletedCode)
|
||||
|
||||
// update code lenses
|
||||
this._onDidChangeDiffsEvent.fire()
|
||||
|
||||
}
|
||||
|
||||
// used by us only
|
||||
public addDiffs(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.insertedRange, { title: 'Accept', command: 'void.acceptDiff', arguments: [{ diffid: this._diffidPool, diffareaid: diffArea.diffareaid }] }),
|
||||
new vscode.CodeLens(suggestedDiff.insertedRange, { 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
|
||||
|
||||
// get document uri
|
||||
const docUri = editor.document.uri
|
||||
const docUriStr = docUri.toString()
|
||||
|
||||
// get relevant diff
|
||||
// TODO speed up with hashmap
|
||||
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;
|
||||
}
|
||||
|
||||
// get relevant diffArea
|
||||
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 diffArea = this._diffAreasOfDocument[docUriStr][diffareaIdx]
|
||||
|
||||
// replace `originalCode[diff.deletedRange]` with diff.insertedCode
|
||||
// TODO add a history event to undo this change
|
||||
const originalLines = diffArea.originalCode.split('\n');
|
||||
const relativeStart = diff.deletedRange.start.line - diffArea.originalStartLine
|
||||
const relativeEnd = diff.deletedRange.end.line - diffArea.originalStartLine
|
||||
diffArea.originalCode = [
|
||||
...originalLines.slice(0, relativeStart), // lines before the deleted range
|
||||
...diff.insertedCode.split('\n'), // inserted lines
|
||||
...originalLines.slice(relativeEnd + 1) // lines after the deleted range
|
||||
].join('\n')
|
||||
|
||||
// if the diffArea has no changes, remove it
|
||||
const currentDiffAreaCode = editor.document.getText()
|
||||
.replace(/\r\n/g, '\n')
|
||||
.split('\n')
|
||||
.slice(diffArea.startLine, diffArea.endLine + 1)
|
||||
.join('\n')
|
||||
if (diffArea.originalCode === currentDiffAreaCode) { // if the currentDiffAreaCode === diffArea.originalCode, remove the diffArea
|
||||
const index = this._diffAreasOfDocument[docUriStr].findIndex(da => da.diffareaid === diffArea.diffareaid)
|
||||
this._diffAreasOfDocument[docUriStr].splice(index, 1)
|
||||
}
|
||||
|
||||
// refresh the diff area
|
||||
this.refreshDiffAreas(docUri)
|
||||
}
|
||||
|
||||
|
||||
// called on void.rejectDiff
|
||||
public async rejectDiff({ diffid, diffareaid }: { diffid: number, diffareaid: number }) {
|
||||
const editor = vscode.window.activeTextEditor
|
||||
if (!editor)
|
||||
return
|
||||
|
||||
// get document uri
|
||||
const docUri = editor.document.uri
|
||||
const docUriStr = docUri.toString()
|
||||
|
||||
// get relevant diff
|
||||
// TODO speed up with hashmap
|
||||
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;
|
||||
}
|
||||
|
||||
// get relevant diffArea
|
||||
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 diffArea = this._diffAreasOfDocument[docUriStr][diffareaIdx]
|
||||
|
||||
// replace `editorCode[diff.insertedRange]` with diff.deletedCode
|
||||
const workspaceEdit = new vscode.WorkspaceEdit();
|
||||
workspaceEdit.replace(docUri, diff.insertedRange, diff.deletedCode)
|
||||
this._weAreEditing = true
|
||||
await vscode.workspace.applyEdit(workspaceEdit)
|
||||
this._weAreEditing = false
|
||||
|
||||
// if the diffArea has no changes, remove it
|
||||
const currentDiffAreaCode = editor.document.getText()
|
||||
.replace(/\r\n/g, '\n')
|
||||
.split('\n')
|
||||
.slice(diffArea.startLine, diffArea.endLine + 1)
|
||||
.join('\n')
|
||||
if (diffArea.originalCode === currentDiffAreaCode) { // if the currentDiffAreaCode === diffArea.originalCode, remove the diffArea
|
||||
const index = this._diffAreasOfDocument[docUriStr].findIndex(da => da.diffareaid === diffArea.diffareaid)
|
||||
this._diffAreasOfDocument[docUriStr].splice(index, 1)
|
||||
}
|
||||
|
||||
// refresh the diff area
|
||||
this.refreshDiffAreas(docUri)
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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,261 +0,0 @@
|
||||
import * as vscode from 'vscode';
|
||||
import { OnFinalMessage, OnText, sendLLMMessage, SetAbort } from "./sendLLMMessage"
|
||||
import { VoidConfig } from '../sidebar/contextForConfig';
|
||||
import { findDiffs } from '../findDiffs';
|
||||
import { searchDiffChunkInstructions, writeFileWithDiffInstructions } from './systemPrompts';
|
||||
import { throttle } from 'lodash';
|
||||
|
||||
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
|
||||
}
|
||||
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 }) => {
|
||||
|
||||
console.log('DEBUG: ', debug)
|
||||
console.log('oldNext: ', oldCurrentLine)
|
||||
console.log('newNext: ', newCurrentLine)
|
||||
console.log('WRITE_TO_FILE1: ', fullCompletedStr.split('\n').slice(0, newCurrentLine + 1).join('\n'))
|
||||
console.log('WRITE_TO_FILE2: ', oldFileStr.split('\n').slice(oldCurrentLine + 1).join('\n'))
|
||||
|
||||
// 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 }
|
||||
)
|
||||
|
||||
|
||||
// `next` is the line after the completed text
|
||||
// `oldNext` is the same line but in the original file
|
||||
type CompetedReturn = { isFinished: true, next?: undefined, oldNext?: undefined, } | { isFinished?: undefined, next: number, oldNext: number, }
|
||||
const generateFileUsingDiffUntilMatchup = ({ fileUri, oldFileStr, completedStr, oldNext, next, diffStr, voidConfig, setAbort }: { fileUri: vscode.Uri, oldFileStr: string, completedStr: string, oldNext: number, next: number, diffStr: string, voidConfig: VoidConfig, setAbort: SetAbort }) => {
|
||||
|
||||
const NUM_MATCHUP_TOKENS = 20
|
||||
|
||||
const promptContent = `ORIGINAL_FILE
|
||||
\`\`\`
|
||||
${oldFileStr}
|
||||
\`\`\`
|
||||
|
||||
DIFF
|
||||
\`\`\`
|
||||
${diffStr}
|
||||
\`\`\`
|
||||
|
||||
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
|
||||
const promise = new Promise<CompetedReturn>((resolve, reject) => {
|
||||
|
||||
// get the abort method
|
||||
let _abort = () => { }
|
||||
let did_abort = false
|
||||
|
||||
|
||||
sendLLMMessage({
|
||||
messages: [{ role: 'system', content: writeFileWithDiffInstructions, }, { role: 'user', content: promptContent, }],
|
||||
onText: (tokenStr, deltaStr) => {
|
||||
|
||||
if (did_abort) return;
|
||||
|
||||
const fullCompletedStr = completedStr + deltaStr
|
||||
|
||||
// diff `originalFileStr` and `newFileStr`
|
||||
const diffs = findDiffs(oldFileStr, fullCompletedStr)
|
||||
const lastDiff = diffs[diffs.length - 1]
|
||||
const oldLineAfterLastDiff = lastDiff.originalEndLine + 1
|
||||
const newLineAfterLastDiff = lastDiff.endLine + 1
|
||||
|
||||
// check if we've generated a diff
|
||||
const didGenerateDiff = newLineAfterLastDiff > next
|
||||
|
||||
// get the line we are currently generating `newCurrentLine`; make sure it never goes past the last diff we've generated
|
||||
// - if `deltaStr` contains a diff, then _next = newLineAfterLastDiff - 1
|
||||
// - if it does not contain a diff, then _next = next + deltaStr.split('\n').length - 1
|
||||
const newCurrentLine = didGenerateDiff ? newLineAfterLastDiff - 1 : next + deltaStr.split('\n').length - 1
|
||||
const oldCurrentLine = didGenerateDiff ? oldLineAfterLastDiff - 1 : oldNext + (newCurrentLine - next)
|
||||
|
||||
// 1. Apply the changes and modify highlighting
|
||||
|
||||
applyCtrlLChangesToFile({ fileUri, newCurrentLine, oldCurrentLine, fullCompletedStr, oldFileStr })
|
||||
|
||||
// 2. Check for early stopping
|
||||
// the conditions for early stopping are:
|
||||
// - we have generated a diff
|
||||
// - there is matchup with the original file after the diff
|
||||
const isMatchupAfterDiff = fullCompletedStr.split('\n').slice(newLineAfterLastDiff).join('\n').length > NUM_MATCHUP_TOKENS
|
||||
if (didGenerateDiff && isMatchupAfterDiff) {
|
||||
|
||||
// resolve the promise
|
||||
resolve({ next: newCurrentLine + 1, oldNext: oldCurrentLine + 1, });
|
||||
|
||||
// abort the LLM call
|
||||
_abort()
|
||||
did_abort = true
|
||||
|
||||
} else {
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
},
|
||||
onFinalMessage: (deltaStr) => {
|
||||
|
||||
const newCompletedStr = completedStr + deltaStr
|
||||
|
||||
applyCtrlLChangesToFile({ fileUri, newCurrentLine: Number.MAX_SAFE_INTEGER, oldCurrentLine: Number.MAX_SAFE_INTEGER, fullCompletedStr: newCompletedStr, oldFileStr, debug: 'FINAL' })
|
||||
|
||||
resolve({ isFinished: true });
|
||||
},
|
||||
onError: (e) => {
|
||||
resolve({ isFinished: true });
|
||||
console.error('Error rewriting file with diff', e);
|
||||
},
|
||||
voidConfig,
|
||||
setAbort: (a) => { setAbort(a); _abort = a; },
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
|
||||
|
||||
return promise
|
||||
|
||||
}
|
||||
|
||||
|
||||
const shouldApplyDiffFn = ({ diffStr, fileStr, speculationStr, voidConfig, setAbort }: { diffStr: string, fileStr: string, speculationStr: string, voidConfig: VoidConfig, setAbort: SetAbort }) => {
|
||||
|
||||
const promptContent = `DIFF
|
||||
\`\`\`
|
||||
${diffStr}
|
||||
\`\`\`
|
||||
|
||||
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
|
||||
const promise = 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,
|
||||
setAbort,
|
||||
})
|
||||
})
|
||||
|
||||
// return the promise
|
||||
return promise
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
// 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 ({ fileUri, oldFileStr, diffStr, voidConfig, setAbort }: { fileUri: vscode.Uri, oldFileStr: string, diffStr: string, voidConfig: VoidConfig, setAbort: SetAbort }) => {
|
||||
|
||||
|
||||
// stateful variables
|
||||
let next = 0
|
||||
let oldNext = 0
|
||||
|
||||
while (next < oldFileStr.split('\n').length) {
|
||||
|
||||
console.log('next line: ', next)
|
||||
|
||||
// get the chunk
|
||||
const chunkStr = oldFileStr.split('\n').slice(next, next + LINES_PER_CHUNK).join('\n')
|
||||
|
||||
// ask LLM if we should apply the diff to the chunk
|
||||
const __start = new Date().getTime()
|
||||
|
||||
let shouldApplyDiff = await shouldApplyDiffFn({ fileStr: oldFileStr, speculationStr: chunkStr, diffStr, voidConfig, setAbort })
|
||||
|
||||
const __end = new Date().getTime()
|
||||
|
||||
if (!shouldApplyDiff) { // should not change the chunk
|
||||
console.log('KEEP CHUNK time: ', __end - __start)
|
||||
|
||||
next += LINES_PER_CHUNK
|
||||
oldNext += LINES_PER_CHUNK
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
// ask LLM to rewrite file with diff (if there is significant matchup with the original file, we stop rewriting)
|
||||
// make vscode read uri = 'asdasd'
|
||||
|
||||
const ___start = new Date().getTime()
|
||||
|
||||
|
||||
const completedStr = (await readFileContentOfUri(fileUri)).split('\n').slice(0, next).join('\n');
|
||||
const result = await generateFileUsingDiffUntilMatchup({ fileUri, oldFileStr, completedStr, oldNext, next, diffStr, voidConfig, setAbort, })
|
||||
|
||||
const ___end = new Date().getTime()
|
||||
|
||||
console.log('EDIT CHUNK time: ', ___end - ___start);
|
||||
|
||||
// if we are finished, stop the loop
|
||||
if (result.isFinished) {
|
||||
break;
|
||||
}
|
||||
|
||||
next = result.next
|
||||
oldNext = result.oldNext
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
export { applyDiffLazily }
|
||||
@@ -1,313 +0,0 @@
|
||||
import Anthropic from '@anthropic-ai/sdk';
|
||||
import OpenAI from 'openai';
|
||||
import { Ollama } from 'ollama/browser'
|
||||
import { VoidConfig } from '../sidebar/contextForConfig';
|
||||
|
||||
|
||||
|
||||
|
||||
export type OnText = (newText: string, fullText: string) => void
|
||||
|
||||
export type OnFinalMessage = (input: string) => void
|
||||
|
||||
export type SetAbort = (abort: () => void) => 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,
|
||||
setAbort: SetAbort,
|
||||
}) => void
|
||||
|
||||
type SendLLMMessageFnTypeExternal = (params: {
|
||||
messages: LLMMessage[],
|
||||
onText: OnText,
|
||||
onFinalMessage: (input: string) => void,
|
||||
onError: (error: string) => void,
|
||||
voidConfig: VoidConfig | null,
|
||||
setAbort: SetAbort,
|
||||
|
||||
})
|
||||
=> void
|
||||
|
||||
|
||||
|
||||
|
||||
// Anthropic
|
||||
const sendAnthropicMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinalMessage, onError, voidConfig, setAbort }) => {
|
||||
|
||||
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
|
||||
}
|
||||
setAbort(abort)
|
||||
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
// OpenAI, OpenRouter, OpenAICompatible
|
||||
const sendOpenAIMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinalMessage, onError, voidConfig, setAbort }) => {
|
||||
|
||||
let didAbort = false
|
||||
let fullText = ''
|
||||
|
||||
// if abort is called, onFinalMessage is NOT called, and no later onTexts are called either
|
||||
let abort: () => void = () => {
|
||||
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 => {
|
||||
abort = () => {
|
||||
// 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);
|
||||
}
|
||||
})
|
||||
|
||||
setAbort(abort)
|
||||
};
|
||||
|
||||
|
||||
// Ollama
|
||||
export const sendOllamaMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinalMessage, onError, voidConfig, setAbort }) => {
|
||||
|
||||
let didAbort = false
|
||||
let fullText = ""
|
||||
|
||||
// if abort is called, onFinalMessage is NOT called, and no later onTexts are called either
|
||||
let abort = () => {
|
||||
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 => {
|
||||
abort = () => {
|
||||
// ollama.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)
|
||||
})
|
||||
|
||||
setAbort(abort);
|
||||
};
|
||||
|
||||
|
||||
|
||||
// Greptile
|
||||
// https://docs.greptile.com/api-reference/query
|
||||
// https://docs.greptile.com/quickstart#sample-response-streamed
|
||||
|
||||
const sendGreptileMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinalMessage, onError, voidConfig, setAbort }) => {
|
||||
|
||||
let didAbort = false
|
||||
let fullText = ''
|
||||
|
||||
// if abort is called, onFinalMessage is NOT called, and no later onTexts are called either
|
||||
let abort: () => void = () => { 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)
|
||||
});
|
||||
|
||||
setAbort(abort)
|
||||
}
|
||||
|
||||
|
||||
|
||||
export const sendLLMMessage: SendLLMMessageFnTypeExternal = ({ messages, onText, onFinalMessage, onError, voidConfig, setAbort }) => {
|
||||
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, setAbort });
|
||||
case 'openAI':
|
||||
case 'openRouter':
|
||||
case 'openAICompatible':
|
||||
return sendOpenAIMsg({ messages, onText, onFinalMessage, onError, voidConfig, setAbort });
|
||||
case 'ollama':
|
||||
return sendOllamaMsg({ messages, onText, onFinalMessage, onError, voidConfig, setAbort });
|
||||
case 'greptile':
|
||||
return sendGreptileMsg({ messages, onText, onFinalMessage, onError, voidConfig, setAbort });
|
||||
default:
|
||||
onError(`Error: whichApi was ${voidConfig.default.whichApi}, which is not recognized!`)
|
||||
}
|
||||
}
|
||||
@@ -1,103 +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 BaseDiffArea = {
|
||||
// use `startLine` and `endLine` instead of `range` for mutibility
|
||||
// bounds are relative to the file, inclusive
|
||||
startLine: number;
|
||||
endLine: number;
|
||||
originalStartLine: number,
|
||||
originalEndLine: number,
|
||||
originalCode: string, // the original chunk of code (not necessarily the whole file)
|
||||
// `newCode: string,` is not included because it is the code in the actual file, `document.text()[startline: endLine + 1]`
|
||||
}
|
||||
|
||||
type DiffArea = BaseDiffArea & { diffareaid: number }
|
||||
|
||||
// the return type of diff creator
|
||||
type BaseDiff = {
|
||||
code: string; // representation of the diff in text
|
||||
deletedRange: vscode.Range; // relative to the original file, inclusive
|
||||
insertedRange: vscode.Range;
|
||||
deletedCode: string; // relative to the new file, inclusive
|
||||
insertedCode: 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', code: 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, BaseDiffArea,
|
||||
Diff, DiffArea,
|
||||
CodeSelection,
|
||||
File,
|
||||
MessageFromSidebar,
|
||||
MessageToSidebar,
|
||||
ChatThreads,
|
||||
ChatMessage,
|
||||
}
|
||||
@@ -1,225 +0,0 @@
|
||||
import * as vscode from 'vscode';
|
||||
import { ApprovalCodeLensProvider } from './ApprovalCodeLensProvider';
|
||||
import { 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 { DiffProvider } from './DiffProvider';
|
||||
// import { getVoidConfig } from './sidebar/contextForConfig';
|
||||
|
||||
// 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 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
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
// 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
|
||||
const displayChangesProvider = new DiffProvider();
|
||||
// context.subscriptions.push(vscode.languages.registerCodeLensProvider('*', displayChangesProvider));
|
||||
|
||||
// 4. Add approve/reject commands
|
||||
context.subscriptions.push(vscode.commands.registerCommand('void.acceptDiff', async (params) => {
|
||||
displayChangesProvider.acceptDiff(params)
|
||||
}));
|
||||
context.subscriptions.push(vscode.commands.registerCommand('void.rejectDiff', async (params) => {
|
||||
displayChangesProvider.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) => {
|
||||
|
||||
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 diffArea = {
|
||||
// startLine: 0, // in ctrl+L the start and end lines are the full document
|
||||
// endLine: editor.document.lineCount,
|
||||
// originalStartLine: 0,
|
||||
// originalEndLine: editor.document.lineCount,
|
||||
// originalCode: await readFileContentOfUri(editor.document.uri),
|
||||
// }
|
||||
// displayChangesProvider.addDiffArea(editor.document.uri, diffArea)
|
||||
|
||||
|
||||
// write new code `m.code` to the document
|
||||
// TODO update like this:
|
||||
// this._weAreEditing = true
|
||||
// await vscode.workspace.applyEdit(workspaceEdit)
|
||||
// await vscode.workspace.save(docUri)
|
||||
// this._weAreEditing = false
|
||||
const fileUri = editor.document.uri
|
||||
const fileStr = await readFileContentOfUri(fileUri)
|
||||
const voidConfig = getVoidConfig(context.globalState.get('partialVoidConfig') ?? {})
|
||||
|
||||
let abort = () => { } // TODO this is unused
|
||||
|
||||
// apply the change
|
||||
applyDiffLazily({ fileUri, oldFileStr: fileStr, diffStr: m.code, voidConfig, setAbort: (a) => { abort = a } })
|
||||
|
||||
// set the file equal to the change
|
||||
// await editor.edit(editBuilder => {
|
||||
// editBuilder.replace(new vscode.Range(diffArea.startLine, 0, diffArea.endLine, Number.MAX_SAFE_INTEGER), m.code);
|
||||
// });
|
||||
|
||||
// rediff the changes based on the diffAreas
|
||||
displayChangesProvider.refreshLenses(editor, editor.document.uri.toString())
|
||||
|
||||
}
|
||||
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
|
||||
// })
|
||||
// )
|
||||
|
||||
}
|
||||
@@ -0,0 +1,471 @@
|
||||
import * as vscode from 'vscode';
|
||||
import { AbortRef, LLMMessage, sendLLMMessage } from '../common/sendLLMMessage';
|
||||
import { getVoidConfigFromPartial, VoidConfig } from '../webviews/common/contextForConfig';
|
||||
import { LRUCache } from 'lru-cache';
|
||||
|
||||
|
||||
|
||||
// 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] treat windows \r\n separately from \n
|
||||
!- [todo] provide type information
|
||||
|
||||
Details
|
||||
-generated results are trimmed up to 1 leading/trailing space
|
||||
-prefixes are cached up to 1 trailing newline
|
||||
-
|
||||
*/
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
type AutocompletionStatus = 'pending' | 'finished' | 'error';
|
||||
type Autocompletion = {
|
||||
id: number,
|
||||
prefix: string,
|
||||
suffix: string,
|
||||
startTime: number,
|
||||
endTime: number | undefined,
|
||||
abortRef: AbortRef,
|
||||
status: AutocompletionStatus,
|
||||
llmPromise: Promise<string> | undefined,
|
||||
result: string,
|
||||
}
|
||||
|
||||
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) => {
|
||||
|
||||
console.log('result: ', JSON.stringify(result))
|
||||
|
||||
// trim all whitespace except for a single leading/trailing space
|
||||
const hasLeadingSpace = result.startsWith(' ');
|
||||
const hasTrailingSpace = result.endsWith(' ');
|
||||
return (hasLeadingSpace ? ' ' : '')
|
||||
+ result.trim()
|
||||
+ (hasTrailingSpace ? ' ' : '');
|
||||
|
||||
}
|
||||
|
||||
const extractCodeFromResult = (result: string) => {
|
||||
|
||||
// extract the code between triple backticks
|
||||
const parts = result.split(/```(?:\s*\w+)?\n?/);
|
||||
|
||||
// if there is no ``` then return the raw result
|
||||
if (parts.length === 1) {
|
||||
return result;
|
||||
}
|
||||
|
||||
// else return the code between the triple backticks
|
||||
return parts[1]
|
||||
|
||||
}
|
||||
|
||||
// trims the end of the prefix to improve cache hit rate
|
||||
const trimPrefix = (prefix: string) => {
|
||||
const trimmedPrefix = prefix.trimEnd()
|
||||
const trailingEnd = prefix.substring(trimmedPrefix.length)
|
||||
|
||||
// keep only a single trailing newline
|
||||
if (trailingEnd.includes('\n')) {
|
||||
return trimmedPrefix + '\n'
|
||||
}
|
||||
|
||||
// else ignore all spaces and return the trimmed prefix
|
||||
return trimmedPrefix
|
||||
}
|
||||
|
||||
function getStringUpToUnbalancedParenthesis(s: string, prefixToTheLeft: string): string {
|
||||
|
||||
const pairs: Record<string, string> = { ')': '(', '}': '{', ']': '[' };
|
||||
|
||||
// todo find first open bracket in prefix and get all brackets beyond it in prefix
|
||||
// get all bracets in prefix
|
||||
let stack: string[] = []
|
||||
const firstOpenIdx = prefixToTheLeft.search(/[[({]/);
|
||||
if (firstOpenIdx !== -1) stack = prefixToTheLeft.slice(firstOpenIdx).split('').filter(c => '()[]{}'.includes(c))
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
// finds the text in the autocompletion to display, assuming the prefix is already matched
|
||||
// example:
|
||||
// originalPrefix = abcd
|
||||
// generatedMiddle = efgh
|
||||
// originalSuffix = ijkl
|
||||
// the user has typed "ef" so prefix = abcdef
|
||||
// we want to return the rest of the generatedMiddle, which is "gh"
|
||||
const toInlineCompletion = ({ prefix, suffix, autocompletion, position }: { prefix: string, suffix: string, autocompletion: Autocompletion, position: vscode.Position }): vscode.InlineCompletionItem => {
|
||||
const originalPrefix = autocompletion.prefix
|
||||
const generatedMiddle = autocompletion.result
|
||||
|
||||
const trimmedOriginalPrefix = trimPrefix(originalPrefix)
|
||||
const trimmedCurrentPrefix = trimPrefix(prefix)
|
||||
|
||||
const suffixLines = suffix.split('\n')
|
||||
const prefixLines = trimmedCurrentPrefix.split('\n')
|
||||
const suffixToTheRightOfCursor = suffixLines[0].trim()
|
||||
const prefixToTheLeftOfCursor = prefixLines[prefixLines.length - 1].trim()
|
||||
|
||||
const generatedLines = generatedMiddle.split('\n')
|
||||
|
||||
// compute startIdx
|
||||
let startIdx = trimmedCurrentPrefix.length - trimmedOriginalPrefix.length
|
||||
if (startIdx < 0) {
|
||||
return new vscode.InlineCompletionItem('')
|
||||
}
|
||||
|
||||
// compute endIdx
|
||||
// hacks to get the suffix to render properly with lower quality models
|
||||
// if the generated text matches with the suffix on the current line, stop
|
||||
let endIdx: number | undefined = generatedMiddle.length // exclusive bounds
|
||||
|
||||
if (suffixToTheRightOfCursor !== '') { // completing in the middle of a line
|
||||
console.log('1')
|
||||
// complete until there is a match
|
||||
const matchIndex = generatedMiddle.lastIndexOf(suffixToTheRightOfCursor[0])
|
||||
if (matchIndex > 0) { endIdx = matchIndex }
|
||||
}
|
||||
|
||||
if (prefixToTheLeftOfCursor !== '') { // completing the end of a line
|
||||
console.log('2')
|
||||
// show a single line
|
||||
const newlineIdx = generatedMiddle.indexOf('\n')
|
||||
if (newlineIdx > -1) { endIdx = newlineIdx }
|
||||
}
|
||||
|
||||
// // if a generated line matches with a suffix line, stop
|
||||
// if (suffixLines.length > 1) {
|
||||
// console.log('3')
|
||||
// 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
|
||||
// }
|
||||
|
||||
let completionStr = generatedMiddle.slice(startIdx, endIdx)
|
||||
|
||||
// filter out unbalanced parentheses
|
||||
console.log('completionStrBeforeParens: ', JSON.stringify(completionStr))
|
||||
completionStr = getStringUpToUnbalancedParenthesis(completionStr, prefixLines.slice(-2).join('\n'))
|
||||
|
||||
console.log('originalCompletionStr: ', JSON.stringify(generatedMiddle.slice(startIdx)))
|
||||
console.log('finalCompletionStr: ', JSON.stringify(completionStr))
|
||||
|
||||
return new vscode.InlineCompletionItem(completionStr, new vscode.Range(position, position))
|
||||
|
||||
}
|
||||
|
||||
// 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 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 === '' || suffixToRightOfCursor === '') { // at beginning or end of line
|
||||
shouldGenerate = false
|
||||
}
|
||||
|
||||
console.log('shouldGenerate:', shouldGenerate, stopTokens)
|
||||
|
||||
return { shouldGenerate, stopTokens }
|
||||
|
||||
}
|
||||
|
||||
export class AutocompleteProvider implements vscode.InlineCompletionItemProvider {
|
||||
|
||||
private _extensionContext: vscode.ExtensionContext;
|
||||
|
||||
private _autocompletionId: number = 0;
|
||||
private _autocompletionsOfDocument: { [docUriStr: string]: LRUCache<number, Autocompletion> } = {}
|
||||
|
||||
private _lastCompletionTime = 0
|
||||
private _lastPrefix: string = ''
|
||||
|
||||
constructor(context: vscode.ExtensionContext) {
|
||||
this._extensionContext = context
|
||||
}
|
||||
|
||||
// used internally by vscode
|
||||
// fires after every keystroke and returns the completion to show
|
||||
async provideInlineCompletionItems(
|
||||
document: vscode.TextDocument,
|
||||
position: vscode.Position,
|
||||
context: vscode.InlineCompletionContext,
|
||||
token: vscode.CancellationToken,
|
||||
): Promise<vscode.InlineCompletionItem[]> {
|
||||
|
||||
const disabled = false
|
||||
if (disabled) { return []; }
|
||||
|
||||
const docUriStr = document.uri.toString()
|
||||
|
||||
|
||||
const fullText = document.getText();
|
||||
const cursorOffset = document.offsetAt(position);
|
||||
const prefix = fullText.substring(0, cursorOffset)
|
||||
const suffix = fullText.substring(cursorOffset)
|
||||
const voidConfig = getVoidConfigFromPartial(this._extensionContext.globalState.get('partialVoidConfig') ?? {})
|
||||
|
||||
// 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: MAX_CACHE_SIZE,
|
||||
dispose: (autocompletion) => {
|
||||
autocompletion.abortRef.current()
|
||||
}
|
||||
})
|
||||
}
|
||||
this._lastPrefix = prefix
|
||||
|
||||
// get all pending autocompletions
|
||||
let __c = 0
|
||||
this._autocompletionsOfDocument[docUriStr].forEach(a => { if (a.status === 'pending') __c += 1 })
|
||||
console.log('pending: ' + __c)
|
||||
|
||||
// get autocompletion from cache
|
||||
let cachedAutocompletion: Autocompletion | undefined = undefined
|
||||
for (const autocompletion of this._autocompletionsOfDocument[docUriStr].values()) {
|
||||
// if the user's change matches up with the generated text
|
||||
if (doesPrefixMatchAutocompletion({ prefix, autocompletion })) {
|
||||
cachedAutocompletion = autocompletion
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// if there is a cached autocompletion, return it
|
||||
if (cachedAutocompletion) {
|
||||
|
||||
if (cachedAutocompletion.status === 'finished') {
|
||||
console.log('A1')
|
||||
|
||||
const inlineCompletion = toInlineCompletion({ autocompletion: cachedAutocompletion, prefix, suffix, position })
|
||||
return [inlineCompletion]
|
||||
|
||||
} else if (cachedAutocompletion.status === 'pending') {
|
||||
console.log('A2')
|
||||
|
||||
try {
|
||||
await cachedAutocompletion.llmPromise;
|
||||
console.log('id: ' + cachedAutocompletion.id)
|
||||
const inlineCompletion = toInlineCompletion({ autocompletion: cachedAutocompletion, prefix, suffix, position })
|
||||
return [inlineCompletion]
|
||||
|
||||
} 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 []
|
||||
}
|
||||
|
||||
console.log('B')
|
||||
|
||||
// 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].values()) {
|
||||
if (autocompletion.status === 'pending') {
|
||||
numPending += 1
|
||||
if (oldestPending === undefined) {
|
||||
oldestPending = autocompletion
|
||||
}
|
||||
if (numPending >= MAX_PENDING_REQUESTS) {
|
||||
// cancel the oldest pending request and remove it from cache
|
||||
this._autocompletionsOfDocument[docUriStr].delete(oldestPending.id)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const { shouldGenerate, stopTokens } = getCompletionOptions({ prefix, suffix })
|
||||
|
||||
if (!shouldGenerate) return []
|
||||
|
||||
// create a new autocompletion and add it to cache
|
||||
const newAutocompletion: Autocompletion = {
|
||||
id: this._autocompletionId++,
|
||||
prefix: prefix,
|
||||
suffix: suffix,
|
||||
startTime: Date.now(),
|
||||
endTime: undefined,
|
||||
abortRef: { current: () => { } },
|
||||
status: 'pending',
|
||||
llmPromise: undefined,
|
||||
result: '',
|
||||
}
|
||||
|
||||
// set parameters of `newAutocompletion` appropriately
|
||||
newAutocompletion.llmPromise = new Promise((resolve, reject) => {
|
||||
|
||||
sendLLMMessage({
|
||||
mode: 'fim',
|
||||
fimInfo: { prefix, suffix },
|
||||
options: { stopTokens },
|
||||
onText: async (tokenStr, completionStr) => {
|
||||
|
||||
newAutocompletion.result = completionStr
|
||||
|
||||
// if generation doesn't match the prefix for the first few tokens generated, reject it
|
||||
if (!doesPrefixMatchAutocompletion({ prefix: this._lastPrefix, autocompletion: newAutocompletion })) {
|
||||
reject('LLM response did not match user\'s text.')
|
||||
}
|
||||
},
|
||||
onFinalMessage: (finalMessage) => {
|
||||
|
||||
// newAutocompletion.prefix = prefix
|
||||
// newAutocompletion.suffix = suffix
|
||||
// newAutocompletion.startTime = Date.now()
|
||||
newAutocompletion.endTime = Date.now()
|
||||
// newAutocompletion.abortRef = { current: () => { } }
|
||||
newAutocompletion.status = 'finished'
|
||||
// newAutocompletion.promise = undefined
|
||||
newAutocompletion.result = postprocessResult(extractCodeFromResult(finalMessage))
|
||||
|
||||
resolve(newAutocompletion.result)
|
||||
|
||||
},
|
||||
onError: (e) => {
|
||||
newAutocompletion.endTime = Date.now()
|
||||
newAutocompletion.status = 'error'
|
||||
reject(e)
|
||||
},
|
||||
voidConfig,
|
||||
abortRef: newAutocompletion.abortRef,
|
||||
})
|
||||
|
||||
// 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 inlineCompletion = toInlineCompletion({ autocompletion: newAutocompletion, prefix, suffix, position })
|
||||
return [inlineCompletion]
|
||||
|
||||
} catch (e) {
|
||||
this._autocompletionsOfDocument[docUriStr].delete(newAutocompletion.id)
|
||||
console.error('Error creating autocompletion (2): ' + e)
|
||||
return []
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -1,301 +0,0 @@
|
||||
|
||||
// import { diffLines, Change } from 'diff';
|
||||
import { diffLines, Change } from 'diff';
|
||||
|
||||
export type SuggestedEdit = {
|
||||
// start/end of current file
|
||||
startLine: number;
|
||||
startCol: number;
|
||||
endLine: number;
|
||||
endCol: number;
|
||||
|
||||
// start/end of original file
|
||||
originalStartLine: number,
|
||||
originalStartCol: number,
|
||||
originalEndLine: number,
|
||||
originalEndCol: number,
|
||||
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: SuggestedEdit[] = []
|
||||
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: SuggestedEdit = {
|
||||
type,
|
||||
startLine, startCol, endLine, endCol, newContent,
|
||||
originalStartLine, originalStartCol, originalEndLine, originalEndCol, originalContent
|
||||
} as SuggestedEdit
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
|
||||
// const diffLinesOld = (text1: string, text2: string) => {
|
||||
// var dmp = new diff_match_patch();
|
||||
// var a = dmp.diff_linesToChars_(text1, text2);
|
||||
// var lineText1 = a.chars1;
|
||||
// var lineText2 = a.chars2;
|
||||
// var lineArray = a.lineArray;
|
||||
// var diffs = dmp.diff_main(lineText1, lineText2, false);
|
||||
// dmp.diff_charsToLines_(diffs, lineArray);
|
||||
// // dmp.diff_cleanupSemantic(diffs);
|
||||
// return diffs;
|
||||
// }
|
||||
|
||||
|
||||
// // TODO use a better diff algorithm
|
||||
// export const findDiffsOld = (oldText: string, newText: string): BaseDiff[] => {
|
||||
|
||||
// const diffs = diffLinesOld(oldText, newText);
|
||||
|
||||
// const blocks: BaseDiff[] = [];
|
||||
// let reprBlock: string[] = [];
|
||||
// let deletedBlock: string[] = [];
|
||||
// let insertedBlock: string[] = [];
|
||||
// let insertedLine = 0;
|
||||
// let deletedLine = 0;
|
||||
// let insertedStart = 0;
|
||||
// let deletedStart = 0;
|
||||
|
||||
// diffs.forEach(([operation, text]) => {
|
||||
|
||||
// const lines = text.split('\n');
|
||||
|
||||
// switch (operation) {
|
||||
|
||||
// // insertion
|
||||
// case 1:
|
||||
// if (reprBlock.length === 0) { reprBlock.push('@@@@'); }
|
||||
// if (insertedBlock.length === 0) insertedStart = insertedLine;
|
||||
// insertedLine += lines.length - 1; // Update only the line count for new text
|
||||
// insertedBlock.push(text);
|
||||
// reprBlock.push(lines.map(line => `+ ${line}`).join('\n'));
|
||||
// break;
|
||||
|
||||
// // deletion
|
||||
// case -1:
|
||||
// if (reprBlock.length === 0) { reprBlock.push('@@@@'); }
|
||||
// if (deletedBlock.length === 0) deletedStart = deletedLine;
|
||||
// deletedLine += lines.length - 1; // Update only the line count for old text
|
||||
// deletedBlock.push(text);
|
||||
// reprBlock.push(lines.map(line => `- ${line}`).join('\n'));
|
||||
// break;
|
||||
|
||||
// // no change
|
||||
// case 0:
|
||||
// // If we have a pending block, add it to the blocks array
|
||||
// if (insertedBlock.length > 0 || deletedBlock.length > 0) {
|
||||
// blocks.push({
|
||||
// code: reprBlock.join(''),
|
||||
// deletedCode: deletedBlock.join(''),
|
||||
// insertedCode: insertedBlock.join(''),
|
||||
// deletedRange: new vscode.Range(deletedStart, 0, deletedLine, Number.MAX_SAFE_INTEGER),
|
||||
// insertedRange: new vscode.Range(insertedStart, 0, insertedLine, Number.MAX_SAFE_INTEGER),
|
||||
// });
|
||||
// }
|
||||
|
||||
// // Reset the block variables
|
||||
// reprBlock = [];
|
||||
// deletedBlock = [];
|
||||
// insertedBlock = [];
|
||||
|
||||
// // Update line counts for unchanged text
|
||||
// insertedLine += lines.length - 1;
|
||||
// deletedLine += lines.length - 1;
|
||||
|
||||
// break;
|
||||
// }
|
||||
// });
|
||||
|
||||
// // Add any remaining blocks after the loop ends
|
||||
// if (insertedBlock.length > 0 || deletedBlock.length > 0) {
|
||||
// blocks.push({
|
||||
// code: reprBlock.join(''),
|
||||
// deletedCode: deletedBlock.join(''),
|
||||
// insertedCode: insertedBlock.join(''),
|
||||
// deletedRange: new vscode.Range(deletedStart, 0, deletedLine, Number.MAX_SAFE_INTEGER),
|
||||
// insertedRange: new vscode.Range(insertedStart, 0, insertedLine, Number.MAX_SAFE_INTEGER),
|
||||
// });
|
||||
// }
|
||||
|
||||
// return blocks;
|
||||
// };
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// export const findDiffs = (oldText: string, newText: string): BaseDiff[] => {
|
||||
|
||||
// let diffs = diffLines(oldText, newText)
|
||||
// .map(diff => {
|
||||
// const operation = diff.added ? 1 : diff.removed ? -1 : 0;
|
||||
// const text = diff.value;
|
||||
// return [operation, text] as const;
|
||||
// })
|
||||
|
||||
|
||||
// const blocks: BaseDiff[] = [];
|
||||
// let reprBlock: string[] = [];
|
||||
// let deletedBlock: string[] = [];
|
||||
// let insertedBlock: string[] = [];
|
||||
// let newFileLine = 0;
|
||||
// let oldFileLine = 0;
|
||||
// let insertedStart = 0;
|
||||
// let deletedStart = 0;
|
||||
|
||||
// diffs.forEach(([operation, text]) => {
|
||||
|
||||
// const lines = text.split('\n');
|
||||
|
||||
// switch (operation) {
|
||||
|
||||
// // insertion
|
||||
// case 1:
|
||||
// if (reprBlock.length === 0) { reprBlock.push('@@@@'); }
|
||||
// if (insertedBlock.length === 0) insertedStart = newFileLine;
|
||||
// newFileLine += lines.length - 1; // update the line count for new text
|
||||
// insertedBlock.push(text);
|
||||
// reprBlock.push(lines.map(line => `+ ${line}`).join('\n'));
|
||||
// break;
|
||||
|
||||
// // deletion
|
||||
// case -1:
|
||||
// if (reprBlock.length === 0) { reprBlock.push('@@@@'); }
|
||||
// if (deletedBlock.length === 0) deletedStart = oldFileLine;
|
||||
// oldFileLine += lines.length - 1; // update the line count for old text
|
||||
// deletedBlock.push(text);
|
||||
// reprBlock.push(lines.map(line => `- ${line}`).join('\n'));
|
||||
// break;
|
||||
|
||||
// // no change
|
||||
// case 0:
|
||||
// // add pending block to the blocks array
|
||||
// if (insertedBlock.length > 0 || deletedBlock.length > 0) {
|
||||
// blocks.push({
|
||||
// code: reprBlock.join(''),
|
||||
// deletedCode: deletedBlock.join(''),
|
||||
// insertedCode: insertedBlock.join(''),
|
||||
// deletedRange: new vscode.Range(deletedStart, 0, oldFileLine, Number.MAX_SAFE_INTEGER),
|
||||
// insertedRange: new vscode.Range(insertedStart, 0, newFileLine, Number.MAX_SAFE_INTEGER),
|
||||
// });
|
||||
// }
|
||||
|
||||
// // update variables
|
||||
// reprBlock = [];
|
||||
// deletedBlock = [];
|
||||
// insertedBlock = [];
|
||||
// deletedStart += lines.length - 1;
|
||||
// insertedStart += lines.length - 1;
|
||||
// newFileLine += lines.length - 1;
|
||||
// oldFileLine += lines.length - 1;
|
||||
|
||||
// break;
|
||||
// }
|
||||
// });
|
||||
|
||||
// // Add any remaining blocks after the loop ends
|
||||
// if (insertedBlock.length > 0 || deletedBlock.length > 0) {
|
||||
// blocks.push({
|
||||
// code: reprBlock.join(''),
|
||||
// deletedCode: deletedBlock.join(''),
|
||||
// insertedCode: insertedBlock.join(''),
|
||||
// deletedRange: new vscode.Range(deletedStart, 0, oldFileLine, Number.MAX_SAFE_INTEGER),
|
||||
// insertedRange: new vscode.Range(insertedStart, 0, newFileLine, Number.MAX_SAFE_INTEGER),
|
||||
// });
|
||||
// }
|
||||
|
||||
// return blocks;
|
||||
// };
|
||||
|
||||
@@ -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,384 +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)
|
||||
},
|
||||
setAbort: (abort) => {
|
||||
abortFnRef.current = abort
|
||||
},
|
||||
voidConfig,
|
||||
})
|
||||
|
||||
|
||||
}
|
||||
|
||||
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,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,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
+2952
-422
File diff suppressed because it is too large
Load Diff
+20
-1
@@ -85,6 +85,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",
|
||||
@@ -100,6 +101,7 @@
|
||||
"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",
|
||||
@@ -115,16 +117,22 @@
|
||||
"yazl": "^2.4.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@anthropic-ai/sdk": "^0.32.1",
|
||||
"@google/generative-ai": "^0.21.0",
|
||||
"@playwright/test": "^1.46.1",
|
||||
"@swc/core": "1.3.62",
|
||||
"@types/cookie": "^0.3.3",
|
||||
"@types/debug": "^4.1.5",
|
||||
"@types/diff": "^6.0.0",
|
||||
"@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",
|
||||
@@ -156,6 +164,7 @@
|
||||
"cssnano": "^6.0.3",
|
||||
"debounce": "^1.0.0",
|
||||
"deemon": "^1.8.0",
|
||||
"diff": "^7.0.0",
|
||||
"electron": "30.5.1",
|
||||
"eslint": "8.36.0",
|
||||
"eslint-plugin-header": "3.1.1",
|
||||
@@ -190,6 +199,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",
|
||||
@@ -198,6 +208,8 @@
|
||||
"mocha-junit-reporter": "^2.2.1",
|
||||
"mocha-multi-reporters": "^1.5.1",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"ollama": "^0.5.9",
|
||||
"openai": "^4.71.1",
|
||||
"opn": "^6.0.0",
|
||||
"original-fs": "^1.2.0",
|
||||
"os-browserify": "^0.3.0",
|
||||
@@ -205,18 +217,25 @@
|
||||
"path-browserify": "^1.0.1",
|
||||
"postcss": "^8.4.33",
|
||||
"postcss-nesting": "^12.0.2",
|
||||
"posthog-js": "^1.184.2",
|
||||
"pump": "^1.0.1",
|
||||
"rcedit": "^1.1.0",
|
||||
"rimraf": "^2.2.8",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-syntax-highlighter": "^15.6.1",
|
||||
"rimraf": "^2.7.1",
|
||||
"scope-tailwind": "^1.0.1",
|
||||
"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",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"extends": "./tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
// "jsx": "react", // <-- Void added this
|
||||
"esModuleInterop": true,
|
||||
"removeComments": false,
|
||||
"preserveConstEnums": true,
|
||||
@@ -52,5 +53,9 @@
|
||||
"./vs/**/*.ts",
|
||||
"vscode-dts/vscode.proposed.*.d.ts",
|
||||
"vscode-dts/vscode.d.ts"
|
||||
|
||||
// Void added these:
|
||||
// "./vs/**/*.tsx",
|
||||
// "./vs/**/*.d.mts",
|
||||
]
|
||||
}
|
||||
|
||||
@@ -121,6 +121,8 @@ 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';
|
||||
|
||||
/**
|
||||
* 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 +150,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 +510,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 {
|
||||
@@ -1225,6 +1237,11 @@ export class CodeApplication extends Disposable {
|
||||
mainProcessElectronServer.registerChannel('logger', loggerChannel);
|
||||
sharedProcessClient.then(client => client.registerChannel('logger', loggerChannel));
|
||||
|
||||
// Void
|
||||
// const sendLLMMessageChannel = ProxyChannel.fromService(accessor.get(ISendLLMMessageService), disposables);
|
||||
const sendLLMMessageChannel = new LLMMessageChannel();
|
||||
mainProcessElectronServer.registerChannel('void-channel-sendLLMMessage', sendLLMMessageChannel);
|
||||
|
||||
// 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,105 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Glass Devtools, Inc. All rights reserved.
|
||||
* Void Editor additions licensed under the AGPLv3 License.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { ProxyOnTextPayload, ProxyOnErrorPayload, ProxyOnFinalMessagePayload, LLMMessageServiceParams, ProxyLLMMessageParams, ProxyLLMMessageAbortParams } from '../common/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 { IDisposable } from '../../../base/common/lifecycle.js';
|
||||
|
||||
|
||||
// BROWSER IMPLEMENTATION OF SENDLLMMESSAGE
|
||||
export const ISendLLMMessageService = createDecorator<ISendLLMMessageService>('sendLLMMessageService');
|
||||
|
||||
// defines an interface that node/ creates and browser/ uses
|
||||
export interface ISendLLMMessageService {
|
||||
readonly _serviceBrand: undefined;
|
||||
sendLLMMessage: (params: LLMMessageServiceParams) => string;
|
||||
abort: (requestId: string) => void;
|
||||
}
|
||||
|
||||
|
||||
export class SendLLMMessageService implements ISendLLMMessageService {
|
||||
|
||||
readonly _serviceBrand: undefined;
|
||||
private readonly channel: IChannel;
|
||||
|
||||
private readonly _disposablesOfRequestId: Record<string, IDisposable[]> = {}
|
||||
|
||||
constructor(
|
||||
@IMainProcessService mainProcessService: IMainProcessService // used as a renderer (only usable on client side)
|
||||
) {
|
||||
|
||||
this.channel = mainProcessService.getChannel('void-channel-sendLLMMessage')
|
||||
// const service = ProxyChannel.toService<LLMMessageChannel>(mainProcessService.getChannel('void-channel-sendLLMMessage')); // lets you call it like a service, not needed here
|
||||
}
|
||||
|
||||
_addDisposable(requestId: string, disposable: IDisposable) {
|
||||
if (!this._disposablesOfRequestId[requestId]) {
|
||||
this._disposablesOfRequestId[requestId] = []
|
||||
}
|
||||
this._disposablesOfRequestId[requestId].push(disposable)
|
||||
}
|
||||
|
||||
|
||||
|
||||
sendLLMMessage(params: LLMMessageServiceParams) {
|
||||
const requestId_ = generateUuid();
|
||||
const { onText, onFinalMessage, onError, ...proxyParams } = params;
|
||||
|
||||
// listen for listenerName='onText' | 'onFinalMessage' | 'onError', and call the original function on it
|
||||
|
||||
const onTextEvent: Event<ProxyOnTextPayload> = this.channel.listen('onText')
|
||||
this._addDisposable(requestId_,
|
||||
onTextEvent(e => {
|
||||
if (requestId_ !== e.requestId) return;
|
||||
onText(e)
|
||||
})
|
||||
)
|
||||
|
||||
const onFinalMessageEvent: Event<ProxyOnFinalMessagePayload> = this.channel.listen('onFinalMessage')
|
||||
this._addDisposable(requestId_,
|
||||
onFinalMessageEvent(e => {
|
||||
if (requestId_ !== e.requestId) return;
|
||||
onFinalMessage(e)
|
||||
this._dispose(requestId_)
|
||||
})
|
||||
)
|
||||
|
||||
const onErrorEvent: Event<ProxyOnErrorPayload> = this.channel.listen('onError')
|
||||
this._addDisposable(requestId_,
|
||||
onErrorEvent(e => {
|
||||
if (requestId_ !== e.requestId) return;
|
||||
console.log('event onError', JSON.stringify(e))
|
||||
onError(e)
|
||||
this._dispose(requestId_)
|
||||
})
|
||||
)
|
||||
|
||||
// params will be stripped of all its functions
|
||||
this.channel.call('sendLLMMessage', { ...proxyParams, requestId: requestId_ } satisfies ProxyLLMMessageParams);
|
||||
|
||||
return requestId_
|
||||
}
|
||||
|
||||
private _dispose(requestId: string) {
|
||||
if (!(requestId in this._disposablesOfRequestId)) return
|
||||
for (const disposable of this._disposablesOfRequestId[requestId]) {
|
||||
disposable.dispose()
|
||||
}
|
||||
delete this._disposablesOfRequestId[requestId]
|
||||
}
|
||||
|
||||
abort(requestId: string) {
|
||||
this.channel.call('abort', { requestId } satisfies ProxyLLMMessageAbortParams);
|
||||
this._dispose(requestId)
|
||||
}
|
||||
}
|
||||
|
||||
registerSingleton(ISendLLMMessageService, SendLLMMessageService, InstantiationType.Delayed);
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Glass Devtools, Inc. All rights reserved.
|
||||
* Void Editor additions licensed under the AGPLv3 License.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { VoidConfig } from '../../../workbench/contrib/void/browser/registerConfig.js';
|
||||
|
||||
// ---------- type definitions ----------
|
||||
|
||||
export type OnText = (p: { newText: string, fullText: string }) => void
|
||||
|
||||
export type OnFinalMessage = (p: { fullText: string }) => void
|
||||
|
||||
export type OnError = (p: { error: Error | string }) => void
|
||||
|
||||
export type AbortRef = { current: (() => void) | null }
|
||||
|
||||
export type LLMMessage = {
|
||||
role: 'system' | 'user' | 'assistant';
|
||||
content: string;
|
||||
}
|
||||
|
||||
export type LLMMessageServiceParams = {
|
||||
onText: OnText;
|
||||
onFinalMessage: OnFinalMessage;
|
||||
onError: OnError;
|
||||
|
||||
messages: LLMMessage[];
|
||||
voidConfig: VoidConfig | null;
|
||||
|
||||
logging: {
|
||||
loggingName: string,
|
||||
};
|
||||
}
|
||||
|
||||
export type SendLLMMMessageParams = {
|
||||
onText: OnText;
|
||||
onFinalMessage: OnFinalMessage;
|
||||
onError: OnError;
|
||||
|
||||
messages: LLMMessage[];
|
||||
voidConfig: VoidConfig | null;
|
||||
|
||||
logging: {
|
||||
loggingName: string,
|
||||
};
|
||||
abortRef: AbortRef;
|
||||
}
|
||||
|
||||
// can't send functions across a proxy, use listeners instead
|
||||
export const listenerNames = ['onText', 'onFinalMessage', 'onError'] as const
|
||||
export type ProxyLLMMessageParams = Omit<LLMMessageServiceParams, typeof listenerNames[number]> & { requestId: string }
|
||||
|
||||
export type ProxyOnTextPayload = Parameters<OnText>[0] & { requestId: string }
|
||||
export type ProxyOnFinalMessagePayload = Parameters<OnFinalMessage>[0] & { requestId: string }
|
||||
export type ProxyOnErrorPayload = Parameters<OnError>[0] & { requestId: string }
|
||||
|
||||
export type ProxyLLMMessageAbortParams = { requestId: string }
|
||||
@@ -0,0 +1,94 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Glass Devtools, Inc. All rights reserved.
|
||||
* Void Editor additions licensed under the AGPLv3 License.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
// this channel is registered in `app.ts`
|
||||
// code convention is to make a service responsible for this stuff, and not a channel, but this is simpler.
|
||||
// you could create one instance in electron-main/my-service.ts and one in browser/my-service.ts (and define the interface IMyService in common/my-service.ts), but we just use a channel here
|
||||
// registerSingleton(ISendLLMMessageService, SendLLMMessageService, InstantiationType.Delayed);
|
||||
|
||||
import { IServerChannel } from '../../../base/parts/ipc/common/ipc.js';
|
||||
import { Emitter, Event } from '../../../base/common/event.js';
|
||||
import { sendLLMMessage } from '../../../workbench/contrib/void/browser/react/out/util/sendLLMMessage.js';
|
||||
import { listenerNames, ProxyOnTextPayload, ProxyOnErrorPayload, ProxyOnFinalMessagePayload, ProxyLLMMessageParams, AbortRef, SendLLMMMessageParams, ProxyLLMMessageAbortParams } from '../common/llmMessageTypes.js';
|
||||
|
||||
// NODE IMPLEMENTATION OF SENDLLMMESSAGE - calls sendLLMMessage() and returns listeners
|
||||
|
||||
export class LLMMessageChannel implements IServerChannel {
|
||||
private readonly _onText = new Emitter<ProxyOnTextPayload>();
|
||||
readonly onText = this._onText.event;
|
||||
|
||||
private readonly _onFinalMessage = new Emitter<ProxyOnFinalMessagePayload>();
|
||||
readonly onFinalMessage = this._onFinalMessage.event;
|
||||
|
||||
private readonly _onError = new Emitter<ProxyOnErrorPayload>();
|
||||
readonly onError = this._onError.event;
|
||||
|
||||
|
||||
private readonly _abortRefOfRequestId: Record<string, AbortRef> = {}
|
||||
|
||||
|
||||
constructor() { }
|
||||
|
||||
// browser uses this to listen for changes
|
||||
listen(_: unknown, event: typeof listenerNames[number]): Event<any> {
|
||||
if (event === 'onText') {
|
||||
return this.onText;
|
||||
}
|
||||
else if (event === 'onFinalMessage') {
|
||||
return this.onFinalMessage;
|
||||
}
|
||||
else if (event === 'onError') {
|
||||
return this.onError;
|
||||
}
|
||||
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 {
|
||||
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 _callSendLLMMessage(params: ProxyLLMMessageParams) {
|
||||
const { requestId } = params;
|
||||
|
||||
if (!(requestId in this._abortRefOfRequestId))
|
||||
this._abortRefOfRequestId[requestId] = { current: null }
|
||||
|
||||
const mainThreadParams: SendLLMMMessageParams = {
|
||||
...params,
|
||||
onText: ({ newText, fullText }) => { this._onText.fire({ requestId, newText, fullText }); },
|
||||
onFinalMessage: ({ fullText }) => { this._onFinalMessage.fire({ requestId, fullText }); },
|
||||
onError: ({ error }) => { this._onError.fire({ requestId, error }); },
|
||||
abortRef: this._abortRefOfRequestId[requestId],
|
||||
}
|
||||
sendLLMMessage(mainThreadParams);
|
||||
}
|
||||
|
||||
private _callAbort(params: ProxyLLMMessageAbortParams) {
|
||||
const { requestId } = params;
|
||||
if (!(requestId in this._abortRefOfRequestId)) return
|
||||
this._abortRefOfRequestId[requestId].current?.()
|
||||
delete this._abortRefOfRequestId[requestId]
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -741,8 +741,24 @@ export class CodeWindow extends BaseWindow implements ICodeWindow {
|
||||
|
||||
cb({ cancel: false, requestHeaders: Object.assign(details.requestHeaders, headers) });
|
||||
});
|
||||
|
||||
|
||||
// // Void: send from https://
|
||||
// this._win.webContents.session.webRequest.onBeforeSendHeaders({ urls }, async (details, cb) => {
|
||||
// // const voidConfig = this.voidConfigStateService.state.voidConfig
|
||||
// // const whichApi = voidConfig.default['whichApi']
|
||||
// const endpoint = 'http://127.' //string | undefined = voidConfig[whichApi as VoidConfigField].endpoint
|
||||
|
||||
// if (endpoint && details.url.startsWith(endpoint)) {
|
||||
// details.requestHeaders['Origin'] = 'https://app.voideditor.com'
|
||||
// }
|
||||
// cb({ cancel: false, requestHeaders: details.requestHeaders });
|
||||
// });
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
void-imports/
|
||||
@@ -0,0 +1,276 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Glass Devtools, Inc. All rights reserved.
|
||||
* Void Editor additions licensed under the AGPLv3 License.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { diffLines } from './react/out/util/diffLines.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,18 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Glass Devtools, Inc. All rights reserved.
|
||||
* Void Editor additions licensed under the AGPLv3 License.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { OperatingSystem, OS } from '../../../../base/common/platform.js';
|
||||
|
||||
export function getCmdKey(): string {
|
||||
if (OS === OperatingSystem.Macintosh) {
|
||||
return '⌘';
|
||||
} else {
|
||||
return 'Ctrl';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
.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,25 @@
|
||||
|
||||
import { CodeSelection } from '../registerThreads.js';
|
||||
|
||||
export const filesStr = (selections: CodeSelection[]) => {
|
||||
|
||||
return selections.map(({ fileURI, content, selectionStr }) =>
|
||||
`\
|
||||
File: ${fileURI.fsPath}
|
||||
\`\`\`
|
||||
${content}
|
||||
\`\`\`${selectionStr === null ? '' : `
|
||||
Selection: ${selectionStr}`}
|
||||
`).join('\n')
|
||||
}
|
||||
|
||||
|
||||
export const userInstructionsStr = (instructions: string, selections: CodeSelection[] | null) => {
|
||||
let str = '';
|
||||
if (selections && selections.length > 0) {
|
||||
str += filesStr(selections);
|
||||
str += `Please edit the selected code following these instructions:\n`
|
||||
}
|
||||
str += `${instructions}`;
|
||||
return str;
|
||||
};
|
||||
+14
-9
@@ -1,5 +1,15 @@
|
||||
|
||||
const generateDiffInstructions = `
|
||||
|
||||
|
||||
// // used for ctrl+l
|
||||
// const partialGenerationInstructions = ``
|
||||
|
||||
|
||||
// // used for ctrl+k, autocomplete
|
||||
// const fimInstructions = ``
|
||||
|
||||
|
||||
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 +170,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 +279,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 +409,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).
|
||||
|
||||
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
import { execSync } from 'child_process';
|
||||
|
||||
// clear temp dirs
|
||||
execSync('npx rimraf out/ && npx rimraf src2/')
|
||||
|
||||
// build and scope tailwind
|
||||
execSync('npx scope-tailwind ./src -o src2/ -s void-scope -c styles.css -p "prefix-" ')
|
||||
|
||||
// tsup to build src2/ into out/
|
||||
execSync('npx tsup')
|
||||
|
||||
|
||||
console.log('✅ Done building! Press Cmd+Shift+B again.')
|
||||
+4
-5
@@ -1,9 +1,9 @@
|
||||
import React, { ReactNode, useCallback, useEffect, useState } from "react"
|
||||
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 +16,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 +39,3 @@ const BlockCode = ({ text, buttonsOnHover, language }: { text: string, buttonsOn
|
||||
)
|
||||
}
|
||||
|
||||
export default BlockCode
|
||||
+13
-12
@@ -1,19 +1,20 @@
|
||||
import React, { JSX, useCallback, useEffect, useState } from "react"
|
||||
import { marked, MarkedToken, Token, TokensList } from "marked"
|
||||
import BlockCode from "./BlockCode"
|
||||
import { getVSCodeAPI } from "../getVscodeApi"
|
||||
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 = ({ text }: { text: string }) => {
|
||||
const CodeButtonsOnHover = ({ diffRepr: text }: { diffRepr: string }) => {
|
||||
const [copyButtonState, setCopyButtonState] = useState(CopyButtonState.Copy)
|
||||
const inlineDiffService = useService('inlineDiffService')
|
||||
|
||||
useEffect(() => {
|
||||
if (copyButtonState !== CopyButtonState.Copy) {
|
||||
@@ -44,7 +45,8 @@ const CodeButtonsOnHover = ({ text }: { text: string }) => {
|
||||
<button
|
||||
className="btn btn-secondary btn-sm border border-vscode-input-border rounded"
|
||||
onClick={async () => {
|
||||
getVSCodeAPI().postMessage({ type: "applyChanges", code: text })
|
||||
|
||||
inlineDiffService.startStreaming('ctrl+l', text)
|
||||
}}
|
||||
>
|
||||
Apply
|
||||
@@ -66,7 +68,7 @@ const RenderToken = ({ token, nested = false }: { token: Token | string, nested?
|
||||
return <BlockCode
|
||||
text={t.text}
|
||||
language={t.lang}
|
||||
buttonsOnHover={<CodeButtonsOnHover text={t.text} />}
|
||||
buttonsOnHover={<CodeButtonsOnHover diffRepr={t.text} />}
|
||||
/>
|
||||
}
|
||||
|
||||
@@ -209,7 +211,7 @@ const RenderToken = ({ token, nested = false }: { token: Token | string, nested?
|
||||
)
|
||||
}
|
||||
|
||||
const MarkdownRender = ({ string, nested = false }: { string: string, nested?: boolean }) => {
|
||||
export const MarkdownRender = ({ string, nested = false }: { string: string, nested?: boolean }) => {
|
||||
const tokens = marked.lexer(string); // https://marked.js.org/using_pro#renderer
|
||||
return (
|
||||
<>
|
||||
@@ -220,4 +222,3 @@ const MarkdownRender = ({ string, nested = false }: { string: string, nested?: b
|
||||
)
|
||||
}
|
||||
|
||||
export default MarkdownRender
|
||||
@@ -0,0 +1,50 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Glass Devtools, Inc. All rights reserved.
|
||||
* Void Editor additions licensed under the AGPLv3 License.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { mountFnGenerator } from '../util/mountFnGenerator.js'
|
||||
|
||||
import { SidebarSettings } from './SidebarSettings.js';
|
||||
import { useSidebarState } from '../util/services.js';
|
||||
// import { SidebarThreadSelector } from './SidebarThreadSelector.js';
|
||||
// import { SidebarChat } from './SidebarChat.js';
|
||||
|
||||
import '../styles.css'
|
||||
import { SidebarThreadSelector } from './SidebarThreadSelector.js';
|
||||
import { SidebarChat } from './SidebarChat.js';
|
||||
|
||||
const Sidebar = () => {
|
||||
const sidebarState = useSidebarState()
|
||||
const { isHistoryOpen, currentTab: tab } = sidebarState
|
||||
|
||||
return <div className='@@void-scope'>
|
||||
<div className={`flex flex-col h-screen w-full`}>
|
||||
|
||||
{/* <span onClick={() => {
|
||||
const tabs = ['chat', 'settings', 'threadSelector']
|
||||
const index = tabs.indexOf(tab)
|
||||
sidebarStateService.setState({ currentTab: tabs[(index + 1) % tabs.length] as any })
|
||||
}}>clickme {tab}</span> */}
|
||||
|
||||
<div className={`mb-2 h-[30vh] ${isHistoryOpen ? '' : 'hidden'}`}>
|
||||
<SidebarThreadSelector />
|
||||
</div>
|
||||
|
||||
<div className={`${tab === 'chat' ? '' : 'hidden'}`}>
|
||||
<SidebarChat />
|
||||
</div>
|
||||
|
||||
<div className={`${tab === 'settings' ? '' : 'hidden'}`}>
|
||||
<SidebarSettings />
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
}
|
||||
|
||||
|
||||
const mountFn = mountFnGenerator(Sidebar)
|
||||
export default mountFn
|
||||
|
||||
@@ -0,0 +1,355 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Glass Devtools, Inc. All rights reserved.
|
||||
* Void Editor additions licensed under the AGPLv3 License.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
import React, { FormEvent, Fragment, useCallback, useEffect, useRef, useState } from 'react';
|
||||
|
||||
|
||||
import { useConfigState, useService, useThreadsState } from '../util/services.js';
|
||||
import { generateDiffInstructions } from '../../../prompt/systemPrompts.js';
|
||||
import { userInstructionsStr } from '../../../prompt/stringifyFiles.js';
|
||||
import { CodeSelection, CodeStagingSelection } from '../../../registerThreads.js';
|
||||
|
||||
import { BlockCode } from '../markdown/BlockCode.js';
|
||||
import { MarkdownRender } from '../markdown/MarkdownRender.js';
|
||||
import { IModelService } from '../../../../../../../editor/common/services/model.js';
|
||||
import { URI } from '../../../../../../../base/common/uri.js';
|
||||
import { EndOfLinePreference } from '../../../../../../../editor/common/model.js';
|
||||
import { IDisposable } from '../../../../../../../base/common/lifecycle.js';
|
||||
import { ErrorDisplay } from '../util/ErrorDisplay.js';
|
||||
import { LLMMessageServiceParams } from '../../../../../../../platform/void/common/llmMessageTypes.js';
|
||||
|
||||
// import { } from '@vscode/webview-ui-toolkit/react';
|
||||
|
||||
// read files from VSCode
|
||||
const VSReadFile = async (modelService: IModelService, uri: URI): Promise<string | null> => {
|
||||
const model = modelService.getModel(uri)
|
||||
if (!model) return null
|
||||
return model.getValue(EndOfLinePreference.LF)
|
||||
}
|
||||
|
||||
|
||||
|
||||
export type ChatMessage =
|
||||
| {
|
||||
role: 'user';
|
||||
content: string; // content sent to the llm
|
||||
displayContent: string; // content displayed to user
|
||||
selections: CodeSelection[] | null; // the user's selection
|
||||
}
|
||||
| {
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
|
||||
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 = (
|
||||
{ type, selections, setStaging }:
|
||||
| { type: 'past', selections: CodeSelection[] | null; setStaging?: undefined }
|
||||
| { type: 'staging', selections: CodeStagingSelection[] | null; setStaging: ((files: CodeStagingSelection[]) => void) }
|
||||
) => {
|
||||
return (
|
||||
!!selections && selections.length !== 0 && (
|
||||
<div className='flex flex-wrap -mx-1 -mb-1'>
|
||||
{selections.map((selection, i) => (
|
||||
<Fragment key={i}>
|
||||
|
||||
<button
|
||||
disabled={!setStaging}
|
||||
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={() => {
|
||||
if (type !== 'staging') return
|
||||
setStaging([...selections.slice(0, i), ...selections.slice(i + 1, Infinity)])
|
||||
}}
|
||||
>
|
||||
<span>{getBasename(selection.fileURI.fsPath)}</span>
|
||||
|
||||
{/* X button */}
|
||||
{type === 'staging' && <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>
|
||||
{/* selection text */}
|
||||
{type === 'staging' && selection.selectionStr && <BlockCode text={selection.selectionStr}
|
||||
buttonsOnHover={(<button
|
||||
onClick={() => {
|
||||
setStaging([...selections.slice(0, i), { ...selection, selectionStr: null }, ...selections.slice(i + 1, Infinity)])
|
||||
}}
|
||||
className="btn btn-secondary btn-sm border border-vscode-input-border rounded"
|
||||
>Remove</button>
|
||||
)} />}
|
||||
</Fragment>
|
||||
))}
|
||||
</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 type='past' selections={chatMessage.selections} />
|
||||
{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 = () => {
|
||||
|
||||
const chatInputRef = useRef<HTMLTextAreaElement | null>(null)
|
||||
|
||||
const modelService = useService('modelService')
|
||||
|
||||
// ----- HIGHER STATE -----
|
||||
// sidebar state
|
||||
const sidebarStateService = useService('sidebarStateService')
|
||||
useEffect(() => {
|
||||
const disposables: IDisposable[] = []
|
||||
disposables.push(
|
||||
sidebarStateService.onDidFocusChat(() => { chatInputRef.current?.focus() }),
|
||||
sidebarStateService.onDidBlurChat(() => { chatInputRef.current?.blur() })
|
||||
)
|
||||
return () => disposables.forEach(d => d.dispose())
|
||||
}, [sidebarStateService, chatInputRef])
|
||||
|
||||
// config state
|
||||
const configState = useConfigState()
|
||||
const { voidConfig } = configState
|
||||
|
||||
// threads state
|
||||
const threadsState = useThreadsState()
|
||||
const threadsStateService = useService('threadsStateService')
|
||||
|
||||
// ----- SIDEBAR CHAT state (local) -----
|
||||
// state of current message
|
||||
const [instructions, setInstructions] = useState('') // the user's instructions
|
||||
|
||||
// state of chat
|
||||
const [messageStream, setMessageStream] = useState('')
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const latestRequestIdRef = useRef<string | null>(null)
|
||||
|
||||
const [latestError, setLatestError] = useState<Error | string | null>(null)
|
||||
|
||||
const sendLLMMessageService = useService('sendLLMMessageService')
|
||||
|
||||
const isDisabled = !instructions
|
||||
|
||||
const formRef = useRef<HTMLFormElement | null>(null)
|
||||
const onSubmit = async (e: FormEvent<HTMLFormElement>) => {
|
||||
|
||||
e.preventDefault()
|
||||
if (isDisabled) return
|
||||
if (isLoading) return
|
||||
|
||||
|
||||
const currSelns = threadsStateService.state._currentStagingSelections
|
||||
const selections = !currSelns ? null : await Promise.all(
|
||||
currSelns.map(async (sel) => ({ ...sel, content: await VSReadFile(modelService, sel.fileURI) }))
|
||||
).then(
|
||||
(files) => files.filter(file => file.content !== null) as CodeSelection[]
|
||||
)
|
||||
|
||||
// add system message to chat history
|
||||
const systemPromptElt: ChatMessage = { role: 'system', content: generateDiffInstructions }
|
||||
threadsStateService.addMessageToCurrentThread(systemPromptElt)
|
||||
|
||||
const userContent = userInstructionsStr(instructions, selections)
|
||||
const newHistoryElt: ChatMessage = { role: 'user', content: userContent, displayContent: instructions, selections }
|
||||
threadsStateService.addMessageToCurrentThread(newHistoryElt)
|
||||
|
||||
const currentThread = threadsStateService.getCurrentThread(threadsStateService.state) // the the instant state right now, don't wait for the React state
|
||||
|
||||
|
||||
// send message to LLM
|
||||
|
||||
const object: LLMMessageServiceParams = {
|
||||
logging: { loggingName: 'Chat' },
|
||||
messages: [...(currentThread?.messages ?? []).map(m => ({ role: m.role, content: m.content })),],
|
||||
onText: ({ newText, fullText }) => setMessageStream(fullText),
|
||||
onFinalMessage: ({ fullText: content }) => {
|
||||
console.log('chat: running final message')
|
||||
|
||||
// add assistant's message to chat history, and clear selection
|
||||
const newHistoryElt: ChatMessage = { role: 'assistant', content, displayContent: content }
|
||||
threadsStateService.addMessageToCurrentThread(newHistoryElt)
|
||||
setMessageStream('')
|
||||
setIsLoading(false)
|
||||
},
|
||||
onError: ({ error }) => {
|
||||
console.log('chat: running 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, }
|
||||
threadsStateService.addMessageToCurrentThread(newHistoryElt)
|
||||
|
||||
setMessageStream('')
|
||||
setIsLoading(false)
|
||||
|
||||
setLatestError(error)
|
||||
},
|
||||
voidConfig,
|
||||
}
|
||||
|
||||
const latestRequestId = sendLLMMessageService.sendLLMMessage(object)
|
||||
latestRequestIdRef.current = latestRequestId
|
||||
|
||||
|
||||
setIsLoading(true)
|
||||
setInstructions('');
|
||||
formRef.current?.reset(); // reset the form's text when clear instructions or unexpected behavior happens
|
||||
threadsStateService.setStaging([]) // clear staging
|
||||
setLatestError(null)
|
||||
|
||||
}
|
||||
|
||||
const onAbort = () => {
|
||||
// abort the LLM
|
||||
if (latestRequestIdRef.current)
|
||||
sendLLMMessageService.abort(latestRequestIdRef.current)
|
||||
|
||||
// if messageStream was not empty, add it to the history
|
||||
const llmContent = messageStream || '(null)'
|
||||
const newHistoryElt: ChatMessage = { role: 'assistant', content: llmContent, displayContent: messageStream, }
|
||||
threadsStateService.addMessageToCurrentThread(newHistoryElt)
|
||||
|
||||
setMessageStream('')
|
||||
setIsLoading(false)
|
||||
|
||||
}
|
||||
|
||||
|
||||
const currentThread = threadsStateService.getCurrentThread(threadsState)
|
||||
|
||||
const selections = threadsState._currentStagingSelections
|
||||
|
||||
return <>
|
||||
<div className="overflow-x-hidden space-y-4">
|
||||
{/* previous messages */}
|
||||
{currentThread !== null && currentThread?.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">
|
||||
{/* selections */}
|
||||
{(selections && selections.length !== 0) && <div className="p-2 pb-0 space-y-2">
|
||||
<SelectedFiles type='staging' selections={selections} setStaging={threadsStateService.setStaging.bind(threadsStateService)} />
|
||||
</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 ? null :
|
||||
<ErrorDisplay
|
||||
error={latestError}
|
||||
onDismiss={() => { setLatestError(null) }}
|
||||
/>}
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
|
||||
|
||||
+29
-19
@@ -1,13 +1,22 @@
|
||||
import React, { useState } from "react";
|
||||
import { configFields, useVoidConfig, VoidConfigField } from "./contextForConfig";
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Glass Devtools, Inc. All rights reserved.
|
||||
* Void Editor additions licensed under the AGPLv3 License.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useConfigState, useService } from '../util/services.js';
|
||||
import { IVoidConfigStateService, nonDefaultConfigFields, PartialVoidConfig, VoidConfig, VoidConfigField, VoidConfigInfo, SetFieldFnType, ConfigState } from '../../../registerConfig.js';
|
||||
|
||||
|
||||
const SettingOfFieldAndParam = ({ field, param }: { field: VoidConfigField, param: string }) => {
|
||||
const { voidConfig, partialVoidConfig, voidConfigInfo, setConfigParam } = useVoidConfig()
|
||||
const { enumArr, defaultVal, description } = voidConfigInfo[field][param]
|
||||
const SettingOfFieldAndParam = ({ field, param, configState, configStateService }:
|
||||
{ field: VoidConfigField; param: string; configState: ConfigState; configStateService: IVoidConfigStateService }) => {
|
||||
|
||||
const { partialVoidConfig } = configState
|
||||
|
||||
|
||||
const { enumArr, defaultVal, description } = configStateService.voidConfigInfo[field][param]
|
||||
const val = partialVoidConfig[field]?.[param] ?? defaultVal // current value of this item
|
||||
|
||||
const updateState = (newValue: string) => { setConfigParam(field, param, newValue) }
|
||||
const updateState = (newValue: string) => { configStateService.setField(field, param, newValue) }
|
||||
|
||||
const resetButton = <button
|
||||
disabled={val === defaultVal}
|
||||
@@ -17,7 +26,7 @@ const SettingOfFieldAndParam = ({ field, param }: { field: VoidConfigField, para
|
||||
>
|
||||
<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>
|
||||
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>
|
||||
|
||||
@@ -25,7 +34,7 @@ const SettingOfFieldAndParam = ({ field, param }: { field: VoidConfigField, para
|
||||
// string
|
||||
(<input
|
||||
className='input p-1 w-full'
|
||||
type="text"
|
||||
type='text'
|
||||
value={val}
|
||||
onChange={(e) => updateState(e.target.value)}
|
||||
/>)
|
||||
@@ -53,23 +62,29 @@ const SettingOfFieldAndParam = ({ field, param }: { field: VoidConfigField, para
|
||||
</div>
|
||||
}
|
||||
|
||||
|
||||
export const SidebarSettings = () => {
|
||||
|
||||
const { voidConfig, voidConfigInfo } = useVoidConfig()
|
||||
const configState = useConfigState()
|
||||
const configStateService = useService('configStateService')
|
||||
|
||||
const { voidConfig } = configState
|
||||
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
|
||||
configState={configState}
|
||||
configStateService={configStateService}
|
||||
field='default'
|
||||
param='whichApi'
|
||||
/>
|
||||
<SettingOfFieldAndParam
|
||||
configState={configState}
|
||||
configStateService={configStateService}
|
||||
field='default'
|
||||
param='maxTokens'
|
||||
/>
|
||||
@@ -78,27 +93,22 @@ export const SidebarSettings = () => {
|
||||
<hr />
|
||||
|
||||
{/* render all fields, but hide the ones not visible for fast tab switching */}
|
||||
{configFields.map(field => {
|
||||
{nonDefaultConfigFields.map(field => {
|
||||
return <div
|
||||
key={field}
|
||||
className={`flex flex-col gap-y-2 ${field !== current_field ? 'hidden' : ''}`}
|
||||
>
|
||||
{Object.keys(voidConfigInfo[field]).map((param) => (
|
||||
{Object.keys(configStateService.voidConfigInfo[field]).map((param) => (
|
||||
<SettingOfFieldAndParam
|
||||
key={param}
|
||||
configState={configState}
|
||||
configStateService={configStateService}
|
||||
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>
|
||||
)
|
||||
}
|
||||
+16
-10
@@ -1,5 +1,9 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Glass Devtools, Inc. All rights reserved.
|
||||
* Void Editor additions licensed under the AGPLv3 License.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
import React from "react";
|
||||
import { ThreadsProvider, useThreads } from "./contextForThreads";
|
||||
import { useService, useThreadsState } from '../util/services.js';
|
||||
|
||||
|
||||
const truncate = (s: string) => {
|
||||
@@ -11,10 +15,12 @@ const truncate = (s: string) => {
|
||||
}
|
||||
|
||||
|
||||
export const SidebarThreadSelector = ({ onClose }: { onClose: () => void }) => {
|
||||
const { getAllThreads, getCurrentThread, switchToThread } = useThreads()
|
||||
export const SidebarThreadSelector = () => {
|
||||
const threadsState = useThreadsState()
|
||||
const threadsStateService = useService('threadsStateService')
|
||||
const sidebarStateService = useService('sidebarStateService')
|
||||
|
||||
const allThreads = getAllThreads()
|
||||
const { allThreads } = threadsState
|
||||
|
||||
// sorted by most recent to least recent
|
||||
const sortedThreadIds = Object.keys(allThreads ?? {}).sort((threadId1, threadId2) => allThreads![threadId1].lastModified > allThreads![threadId2].lastModified ? 1 : -1)
|
||||
@@ -24,7 +30,7 @@ export const SidebarThreadSelector = ({ onClose }: { onClose: () => void }) => {
|
||||
|
||||
{/* X button at top right */}
|
||||
<div className="text-right">
|
||||
<button className="btn btn-sm" onClick={onClose}>
|
||||
<button className="btn btn-sm" onClick={() => sidebarStateService.setState({ isHistoryOpen: false })}>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
@@ -48,7 +54,7 @@ export const SidebarThreadSelector = ({ onClose }: { onClose: () => void }) => {
|
||||
return <>Error: Threads not found.</>
|
||||
const pastThread = allThreads[threadId]
|
||||
|
||||
let btnStringArr = []
|
||||
let btnStringArr: string[] = []
|
||||
|
||||
let msg1 = truncate(allThreads[threadId].messages[0]?.displayContent ?? '(empty)')
|
||||
btnStringArr.push(msg1)
|
||||
@@ -57,15 +63,15 @@ export const SidebarThreadSelector = ({ onClose }: { onClose: () => void }) => {
|
||||
if (msg2)
|
||||
btnStringArr.push(msg2)
|
||||
|
||||
btnStringArr.push(allThreads[threadId].messages.length)
|
||||
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)}
|
||||
className={`btn btn-sm rounded-sm ${pastThread.id === threadsStateService.getCurrentThread(threadsState)?.id ? "btn-primary" : "btn-secondary"}`}
|
||||
onClick={() => threadsStateService.switchToThread(pastThread.id)}
|
||||
title={new Date(pastThread.createdAt).toLocaleString()}
|
||||
>
|
||||
{btnString}
|
||||
@@ -76,4 +82,4 @@ export const SidebarThreadSelector = ({ onClose }: { onClose: () => void }) => {
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
+3
-3
@@ -2,7 +2,8 @@
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
html {
|
||||
|
||||
/* html {
|
||||
font-size: var(--vscode-font-size);
|
||||
}
|
||||
|
||||
@@ -29,7 +30,6 @@ html {
|
||||
}
|
||||
}
|
||||
|
||||
/* add transparency when disabled */
|
||||
&:disabled {
|
||||
@apply opacity-75 cursor-not-allowed;
|
||||
}
|
||||
@@ -41,4 +41,4 @@ html {
|
||||
|
||||
.dropdown {
|
||||
@apply bg-vscode-dropdown-bg text-vscode-dropdown-foreground border-vscode-dropdown-border focus:outline-vscode-focus-border;
|
||||
}
|
||||
} */
|
||||
@@ -0,0 +1,161 @@
|
||||
import React, { useState } from 'react';
|
||||
import { AlertCircle, ChevronDown, ChevronUp, X } from 'lucide-react';
|
||||
|
||||
import { getCmdKey } from '../../../getCmdKey.js';
|
||||
|
||||
// const opaqueMessage = `\
|
||||
// Unfortunately, Void can't see the full error. However, you should be able to find more details by pressing ${getCmdKey()}+Shift+P, typing "Toggle Developer Tools", and looking at the console.\n
|
||||
// This error often means you have an incorrect API key. If you're self-hosting your own server, it might mean your CORS headers are off, and you should make sure your server's response has the header "Access-Control-Allow-Origins" set to "*", or at least allows "vscode-file://vscode-app".`
|
||||
// if ((error instanceof Error) && (error.cause + '').includes('TypeError: Failed to fetch')) {
|
||||
// e = error as any
|
||||
// e['Void Team'] = opaqueMessage
|
||||
// }
|
||||
|
||||
|
||||
type Details = {
|
||||
message: string,
|
||||
name: string,
|
||||
stack: string | null,
|
||||
cause: string | null,
|
||||
code: string | null,
|
||||
additional: Record<string, any>
|
||||
}
|
||||
|
||||
// Get detailed error information
|
||||
const getErrorDetails = (error: unknown) => {
|
||||
|
||||
let details: Details;
|
||||
|
||||
let e: Error & { [other: string]: undefined | any }
|
||||
|
||||
// If fetch() fails, it gives an opaque message. We add extra details to the error.
|
||||
if (error instanceof Error) {
|
||||
e = error
|
||||
}
|
||||
// sometimes error is an object but not an Error
|
||||
else if (typeof error === 'object') {
|
||||
e = new Error(`The server didn't give a very useful error message. More details below.`, { cause: JSON.stringify(error) })
|
||||
|
||||
}
|
||||
else {
|
||||
e = new Error(String(error))
|
||||
}
|
||||
// console.log('error display', JSON.stringify(e))
|
||||
|
||||
const message = e.message && e.error ?
|
||||
(e.message + ':\n' + e.error)
|
||||
: e.message || e.error || JSON.stringify(error)
|
||||
|
||||
details = {
|
||||
name: e.name || 'Error',
|
||||
message: message,
|
||||
stack: null, // e.stack is ignored because it's ugly and not very useful
|
||||
cause: e.cause ? String(e.cause) : null,
|
||||
code: e.code || null,
|
||||
additional: {}
|
||||
}
|
||||
|
||||
|
||||
// Collect any additional properties from the e
|
||||
for (let prop of Object.getOwnPropertyNames(e).filter((prop) => !Object.keys(details).includes(prop)))
|
||||
details.additional[prop] = (e as any)[prop]
|
||||
|
||||
return details;
|
||||
};
|
||||
|
||||
|
||||
|
||||
export const ErrorDisplay = ({
|
||||
error,
|
||||
onDismiss = null,
|
||||
showDismiss = true,
|
||||
className = ''
|
||||
}: {
|
||||
error: Error | object | string,
|
||||
onDismiss: (() => void) | null,
|
||||
showDismiss?: boolean,
|
||||
className?: string
|
||||
}) => {
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
|
||||
const details = getErrorDetails(error);
|
||||
const hasDetails = details.cause || Object.keys(details.additional).length > 0;
|
||||
|
||||
return (
|
||||
<div className={`rounded-lg border border-red-200 bg-red-50 p-4 ${className}`}>
|
||||
{/* Header */}
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex gap-3">
|
||||
<AlertCircle className="h-5 w-5 text-red-500 mt-0.5" />
|
||||
<div className="flex-1">
|
||||
<h3 className="font-semibold text-red-800">
|
||||
{details.name}
|
||||
</h3>
|
||||
<p className="text-red-700 mt-1">
|
||||
{details.message}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
{hasDetails && (
|
||||
<button
|
||||
onClick={() => setIsExpanded(!isExpanded)}
|
||||
className="text-red-600 hover:text-red-800 p-1 rounded"
|
||||
>
|
||||
{isExpanded ? (
|
||||
<ChevronUp className="h-5 w-5" />
|
||||
) : (
|
||||
<ChevronDown className="h-5 w-5" />
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
{showDismiss && onDismiss && (
|
||||
<button
|
||||
onClick={onDismiss}
|
||||
className="text-red-600 hover:text-red-800 p-1 rounded"
|
||||
>
|
||||
<X className="h-5 w-5" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Expandable Details */}
|
||||
{isExpanded && hasDetails && (
|
||||
<div className="mt-4 space-y-3 border-t border-red-200 pt-3">
|
||||
{details.code && (
|
||||
<div>
|
||||
<span className="font-semibold text-red-800">Error Code: </span>
|
||||
<span className="text-red-700">{details.code}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{details.cause && (
|
||||
<div>
|
||||
<span className="font-semibold text-red-800">Cause: </span>
|
||||
<span className="text-red-700">{details.cause}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{Object.keys(details.additional).length > 0 && (
|
||||
<div>
|
||||
<span className="font-semibold text-red-800">Additional Information:</span>
|
||||
<pre className="mt-1 text-sm text-red-700 overflow-x-auto whitespace-pre-wrap">
|
||||
{Object.keys(details.additional).map(key => `${key}:\n${details.additional[key]}`).join('\n')}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
{/* {details.stack && (
|
||||
<div>
|
||||
<span className="font-semibold text-red-800">Stack Trace:</span>
|
||||
<pre className="mt-1 text-sm text-red-700 overflow-x-auto whitespace-pre-wrap">
|
||||
{details.stack}
|
||||
</pre>
|
||||
</div>
|
||||
)} */}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,3 @@
|
||||
import { diffLines, Change } from 'diff';
|
||||
|
||||
export { diffLines, Change }
|
||||
@@ -0,0 +1,17 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import * as ReactDOM from 'react-dom/client'
|
||||
import { ReactServicesType, VoidSidebarState } from '../../../registerSidebar.js';
|
||||
import { _registerServices } from './services.js';
|
||||
|
||||
|
||||
export const mountFnGenerator = (Component: React.FC) => (rootElement: HTMLElement, services: ReactServicesType) => {
|
||||
if (typeof document === 'undefined') {
|
||||
console.error('index.tsx error: document was undefined')
|
||||
return
|
||||
}
|
||||
|
||||
_registerServices(services)
|
||||
|
||||
const root = ReactDOM.createRoot(rootElement)
|
||||
root.render(<Component />);
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
|
||||
import posthog from 'posthog-js';
|
||||
|
||||
export { posthog }
|
||||
@@ -0,0 +1,986 @@
|
||||
import Anthropic from '@anthropic-ai/sdk';
|
||||
import OpenAI from 'openai';
|
||||
import { Ollama } from 'ollama/browser'
|
||||
import { Content, GoogleGenerativeAI, GoogleGenerativeAIFetchError } from '@google/generative-ai';
|
||||
import { posthog } from 'posthog-js'
|
||||
import type { VoidConfig } from '../../../registerConfig.js';
|
||||
import type { LLMMessage, OnText, OnError, OnFinalMessage, SendLLMMMessageParams, } from '../../../../../../../platform/void/common/llmMessageTypes.js';
|
||||
import { LLMMessageServiceParams } from '../../../../../../../platform/void/common/llmMessageTypes.js';
|
||||
|
||||
type SendLLMMessageFnTypeInternal = (params: {
|
||||
messages: LLMMessage[];
|
||||
onText: OnText;
|
||||
onFinalMessage: OnFinalMessage;
|
||||
onError: OnError;
|
||||
voidConfig: VoidConfig;
|
||||
|
||||
_setAborter: (aborter: () => void) => void;
|
||||
}) => void
|
||||
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// Anthropic
|
||||
type LLMMessageAnthropic = {
|
||||
role: 'user' | 'assistant';
|
||||
content: string;
|
||||
}
|
||||
const sendAnthropicMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinalMessage, onError, voidConfig, _setAborter }) => {
|
||||
|
||||
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: parseMaxTokensStr(voidConfig.default.maxTokens)!, // this might be undefined, but it will just throw an error for the user
|
||||
});
|
||||
|
||||
|
||||
// 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({ error: 'Invalid API key.' })
|
||||
}
|
||||
else {
|
||||
onError({ error })
|
||||
}
|
||||
})
|
||||
|
||||
// TODO need to test this to make sure it works, it might throw an error
|
||||
_setAborter(() => stream.controller.abort())
|
||||
|
||||
};
|
||||
|
||||
// Gemini
|
||||
const sendGeminiMsg: SendLLMMessageFnTypeInternal = async ({ messages, onText, onFinalMessage, onError, voidConfig, _setAborter }) => {
|
||||
|
||||
let fullText = ''
|
||||
|
||||
const genAI = new GoogleGenerativeAI(voidConfig.gemini.apikey);
|
||||
const model = genAI.getGenerativeModel({ model: voidConfig.gemini.model });
|
||||
|
||||
// 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({ error: 'Invalid API key.' });
|
||||
}
|
||||
else {
|
||||
onError({ error });
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// OpenAI, OpenRouter, OpenAICompatible
|
||||
const sendOpenAIMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinalMessage, onError, voidConfig, _setAborter }) => {
|
||||
|
||||
let fullText = ''
|
||||
|
||||
let openai: OpenAI
|
||||
let options: OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming
|
||||
|
||||
const maxTokens = parseMaxTokensStr(voidConfig.default.maxTokens)
|
||||
|
||||
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: 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: 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: 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 => {
|
||||
_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({ error: 'Invalid API key.' });
|
||||
}
|
||||
else {
|
||||
onError({ error });
|
||||
}
|
||||
})
|
||||
|
||||
};
|
||||
|
||||
// Ollama
|
||||
export const sendOllamaMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinalMessage, onError, voidConfig, _setAborter }) => {
|
||||
|
||||
let fullText = ''
|
||||
|
||||
const ollama = new Ollama({ host: voidConfig.ollama.endpoint })
|
||||
|
||||
ollama.chat({
|
||||
model: voidConfig.ollama.model,
|
||||
messages: messages,
|
||||
stream: true,
|
||||
options: { num_predict: parseMaxTokensStr(voidConfig.default.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 => {
|
||||
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, _setAborter }) => {
|
||||
|
||||
let fullText = ''
|
||||
|
||||
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
|
||||
// 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 })
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
export const sendLLMMessage = ({
|
||||
messages,
|
||||
onText: onText_,
|
||||
onFinalMessage: onFinalMessage_,
|
||||
onError: onError_,
|
||||
abortRef: abortRef_,
|
||||
voidConfig,
|
||||
logging: { loggingName }
|
||||
}: SendLLMMMessageParams) => {
|
||||
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() }))
|
||||
|
||||
// only captures number of messages and message "shape", no actual code, instructions, prompts, etc
|
||||
const captureChatEvent = (eventId: string, extras?: object) => {
|
||||
posthog.capture(eventId, {
|
||||
whichApi: voidConfig.default['whichApi'],
|
||||
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 = ({ error }) => {
|
||||
console.error('sendLLMMessage onError:', error)
|
||||
if (_didAbort) return
|
||||
captureChatEvent(`${loggingName} - Error`, { error })
|
||||
onError_({ error })
|
||||
}
|
||||
|
||||
const onAbort = () => {
|
||||
captureChatEvent(`${loggingName} - Abort`, { messageLengthSoFar: _fullTextSoFar.length })
|
||||
_aborter?.()
|
||||
_didAbort = true
|
||||
}
|
||||
abortRef_.current = onAbort
|
||||
|
||||
captureChatEvent(`${loggingName} - Sending Message`, { messageLength: messages[messages.length - 1]?.content.length })
|
||||
|
||||
try {
|
||||
switch (voidConfig.default.whichApi) {
|
||||
case 'anthropic':
|
||||
sendAnthropicMsg({ messages, onText, onFinalMessage, onError, voidConfig, _setAborter, });
|
||||
break;
|
||||
case 'openAI':
|
||||
case 'openRouter':
|
||||
case 'openAICompatible':
|
||||
sendOpenAIMsg({ messages, onText, onFinalMessage, onError, voidConfig, _setAborter, });
|
||||
break;
|
||||
case 'gemini':
|
||||
sendGeminiMsg({ messages, onText, onFinalMessage, onError, voidConfig, _setAborter, });
|
||||
break;
|
||||
case 'ollama':
|
||||
sendOllamaMsg({ messages, onText, onFinalMessage, onError, voidConfig, _setAborter, });
|
||||
break;
|
||||
case 'greptile':
|
||||
sendGreptileMsg({ messages, onText, onFinalMessage, onError, voidConfig, _setAborter, });
|
||||
break;
|
||||
default:
|
||||
onError({ error: `Error: whichApi was ${voidConfig.default.whichApi}, which is not recognized!` })
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
catch (error) {
|
||||
if (error instanceof Error) { onError({ error }) }
|
||||
else { onError({ error: `Unexpected Error in sendLLMMessage: ${error}` }); }
|
||||
; (_aborter as any)?.()
|
||||
_didAbort = true
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// // 6. Autocomplete
|
||||
// const autocompleteProvider = new AutocompleteProvider(context);
|
||||
// context.subscriptions.push(vscode.languages.registerInlineCompletionItemProvider('*', autocompleteProvider));
|
||||
|
||||
// const voidConfig = getVoidConfigFromPartial(context.globalState.get('partialVoidConfig') ?? {})
|
||||
|
||||
// // setupAutocomplete({ voidConfig, abortRef })
|
||||
|
||||
// // 7. Language Server
|
||||
// console.log('run lsp')
|
||||
// let disposable = vscode.commands.registerCommand('typeInspector.inspect', runTreeSitter);
|
||||
// context.subscriptions.push(disposable);
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// import { configFields, VoidConfig } from "../webviews/common/contextForConfig"
|
||||
// import { FimInfo } from "./sendLLMMessage"
|
||||
|
||||
|
||||
// type GetFIMPrompt = ({ voidConfig, fimInfo }: { voidConfig: VoidConfig, fimInfo: FimInfo, }) => string
|
||||
|
||||
// export const getFIMSystem: GetFIMPrompt = ({ voidConfig, fimInfo }) => {
|
||||
|
||||
// switch (voidConfig.default.whichApi) {
|
||||
// case 'ollama':
|
||||
// return ''
|
||||
// case 'anthropic':
|
||||
// case 'openAI':
|
||||
// case 'gemini':
|
||||
// case 'greptile':
|
||||
// case 'openRouter':
|
||||
// case 'openAICompatible':
|
||||
// case 'azure':
|
||||
// default:
|
||||
// return `You are given the START and END to a piece of code. Please FILL IN THE MIDDLE between the START and END.
|
||||
|
||||
// Instruction summary:
|
||||
// 1. Return the MIDDLE of the code between the START and END.
|
||||
// 2. Do not give an explanation, description, or any other code besides the middle.
|
||||
// 3. Do not return duplicate code from either START or END.
|
||||
// 4. Make sure the MIDDLE piece of code has balanced brackets that match the START and END.
|
||||
// 5. The MIDDLE begins on the same line as START. Please include a newline character if you want to begin on the next line.
|
||||
// 6. Around 90% of the time, you should return just one or a few lines of code. You should keep your outputs short unless you are confident the user is trying to write boilderplate code.
|
||||
|
||||
// # EXAMPLE
|
||||
|
||||
// ## START:
|
||||
// \`\`\` python
|
||||
// def add(a,b):
|
||||
// return a + b
|
||||
// def subtract(a,b):
|
||||
// return a - b
|
||||
// \`\`\`
|
||||
// ## END:
|
||||
// \`\`\` python
|
||||
// def divide(a,b):
|
||||
// return a / b
|
||||
// \`\`\`
|
||||
// ## EXPECTED OUTPUT:
|
||||
// \`\`\` python
|
||||
|
||||
// def multiply(a,b):
|
||||
// return a * b
|
||||
// \`\`\`
|
||||
|
||||
// # EXAMPLE
|
||||
// ## START:
|
||||
// \`\`\` javascript
|
||||
// const x = 1
|
||||
|
||||
// const y
|
||||
// \`\`\`
|
||||
// ## END:
|
||||
// \`\`\` javascript
|
||||
|
||||
// const z = 3
|
||||
// \`\`\`
|
||||
// ## EXPECTED OUTPUT:
|
||||
// \`\`\` javascript
|
||||
// = 2
|
||||
// \`\`\`
|
||||
// `
|
||||
// }
|
||||
|
||||
|
||||
// }
|
||||
|
||||
|
||||
// export const getFIMPrompt: GetFIMPrompt = ({ voidConfig, fimInfo }) => {
|
||||
|
||||
// const { prefix: fullPrefix, suffix: fullSuffix } = fimInfo
|
||||
// const prefix = fullPrefix.split('\n').slice(-20).join('\n')
|
||||
// const suffix = fullSuffix.split('\n').slice(0, 20).join('\n')
|
||||
|
||||
|
||||
// console.log('prefix', JSON.stringify(prefix))
|
||||
// console.log('suffix', JSON.stringify(suffix))
|
||||
|
||||
// if (!prefix.trim() && !suffix.trim()) return ''
|
||||
|
||||
// // TODO may want to trim the prefix and suffix
|
||||
// switch (voidConfig.default.whichApi) {
|
||||
// case 'ollama':
|
||||
// if (voidConfig.ollama.model === 'codestral') {
|
||||
// return `[SUFFIX]${suffix}[PREFIX] ${prefix}`
|
||||
// } else if (voidConfig.ollama.model.includes('qwen')) {
|
||||
// return `<|fim_prefix|>${prefix}<|fim_suffix|>${suffix}<|fim_middle|>`
|
||||
// }
|
||||
// return ''
|
||||
// case 'anthropic':
|
||||
// case 'openAI':
|
||||
// case 'gemini':
|
||||
// case 'greptile':
|
||||
// case 'openRouter':
|
||||
// case 'openAICompatible':
|
||||
// case 'azure':
|
||||
// default:
|
||||
// return `## START:
|
||||
// \`\`\`
|
||||
// ${prefix}
|
||||
// \`\`\`
|
||||
// ## END:
|
||||
// \`\`\`
|
||||
// ${suffix}
|
||||
// \`\`\`
|
||||
// `
|
||||
// }
|
||||
// }
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// Mathew - sendLLMMessage
|
||||
|
||||
// import Anthropic from '@anthropic-ai/sdk';
|
||||
// import OpenAI from 'openai';
|
||||
// import { Ollama } from 'ollama/browser'
|
||||
// import { Content, GoogleGenerativeAI, GoogleGenerativeAIError, GoogleGenerativeAIFetchError } from '@google/generative-ai';
|
||||
// import { VoidConfig } from '../webviews/common/contextForConfig'
|
||||
// import { getFIMPrompt, getFIMSystem } from './getPrompt';
|
||||
|
||||
// export type AbortRef = { current: (() => void) }
|
||||
|
||||
// export type LLMMessageOnText = (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 LLMMessageOptions = { stopTokens?: string[] }
|
||||
|
||||
// type SendLLMMessageFnTypeInternal = (params: {
|
||||
// mode: 'chat' | 'fim',
|
||||
// messages: LLMMessage[],
|
||||
// options?: LLMMessageOptions,
|
||||
// onText: LLMMessageOnText,
|
||||
// onFinalMessage: OnFinalMessage,
|
||||
// onError: (error: string) => void,
|
||||
// abortRef: AbortRef,
|
||||
// voidConfig: VoidConfig,
|
||||
// }) => void
|
||||
|
||||
|
||||
// type SendLLMMessageFnTypeExternal = (params: (
|
||||
// | { mode?: 'chat', messages: LLMMessage[], fimInfo?: undefined, }
|
||||
// | { mode: 'fim', messages?: undefined, fimInfo: FimInfo, }
|
||||
// ) & {
|
||||
// options?: LLMMessageOptions,
|
||||
// onText: LLMMessageOnText,
|
||||
// onFinalMessage: OnFinalMessage,
|
||||
// onError: (error: string) => void,
|
||||
// abortRef: AbortRef,
|
||||
// voidConfig: VoidConfig | null, // these may be absent
|
||||
// }) => void
|
||||
|
||||
// export type FimInfo = {
|
||||
// prefix: string,
|
||||
// suffix: string,
|
||||
// }
|
||||
|
||||
// const parseMaxTokensStr = (maxTokensStr: string) => {
|
||||
// // parse the string but only if the full string is a valid number, eg parseInt('100abc') should return NaN
|
||||
// let int = isNaN(Number(maxTokensStr)) ? undefined : parseInt(maxTokensStr)
|
||||
// if (Number.isNaN(int))
|
||||
// return undefined
|
||||
// return int
|
||||
// }
|
||||
|
||||
// // 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: parseMaxTokensStr(voidConfig.default.maxTokens)!, // this might be undefined, but it will just throw an error for the user
|
||||
// });
|
||||
|
||||
// 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 }
|
||||
// };
|
||||
|
||||
// // Gemini
|
||||
// const sendGeminiMsg: SendLLMMessageFnTypeInternal = async ({ messages, onText, onFinalMessage, onError, voidConfig, abortRef }) => {
|
||||
|
||||
// let didAbort = false
|
||||
// let fullText = ''
|
||||
|
||||
// abortRef.current = () => {
|
||||
// didAbort = true
|
||||
// }
|
||||
|
||||
// const genAI = new GoogleGenerativeAI(voidConfig.gemini.apikey);
|
||||
// const model = genAI.getGenerativeModel({ model: voidConfig.gemini.model });
|
||||
|
||||
// // remove system messages that get sent to Gemini
|
||||
// // str of all system messages
|
||||
// let 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 => {
|
||||
// abortRef.current = () => {
|
||||
// // response.stream.return(fullText)
|
||||
// didAbort = true;
|
||||
// }
|
||||
// for await (const chunk of response.stream) {
|
||||
// if (didAbort) return;
|
||||
// const newText = chunk.text();
|
||||
// fullText += newText;
|
||||
// onText(newText, fullText);
|
||||
// }
|
||||
// onFinalMessage(fullText);
|
||||
// })
|
||||
// .catch((error) => {
|
||||
// if (error instanceof GoogleGenerativeAIFetchError) {
|
||||
// if (error.status === 400) {
|
||||
// onError('Invalid API key.');
|
||||
// }
|
||||
// else {
|
||||
// onError(`${error.name}:\n${error.message}`);
|
||||
// }
|
||||
// }
|
||||
// else {
|
||||
// onError(error);
|
||||
// }
|
||||
// })
|
||||
// }
|
||||
|
||||
// // 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
|
||||
|
||||
// let maxTokens = parseMaxTokensStr(voidConfig.default.maxTokens)
|
||||
|
||||
// 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: 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: 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: 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.name}:\n${error.message}`);
|
||||
// }
|
||||
// }
|
||||
// else {
|
||||
// onError(error);
|
||||
// }
|
||||
// })
|
||||
|
||||
// };
|
||||
|
||||
// // Ollama
|
||||
// export const sendOllamaMsg: SendLLMMessageFnTypeInternal = ({ options, mode, messages, onText, onFinalMessage, onError, voidConfig, abortRef }) => {
|
||||
|
||||
// let didAbort = false
|
||||
// let fullText = ""
|
||||
|
||||
// const ollama = new Ollama({ host: voidConfig.ollama.endpoint })
|
||||
|
||||
// abortRef.current = () => {
|
||||
// didAbort = true;
|
||||
// };
|
||||
|
||||
// type GenerateResponse = Awaited<ReturnType<(typeof ollama.generate)>>
|
||||
// type ChatResponse = Awaited<ReturnType<(typeof ollama.chat)>>
|
||||
|
||||
|
||||
// // First check if model exists
|
||||
// ollama.list()
|
||||
// .then(async models => {
|
||||
// const installedModels = models.models.map(m => m.name.replace(/:latest$/, ''))
|
||||
// const modelExists = installedModels.some(m => m.startsWith(voidConfig.ollama.model));
|
||||
// if (!modelExists) {
|
||||
// const errorMessage = `The model "${voidConfig.ollama.model}" is not available locally. Please run 'ollama pull ${voidConfig.ollama.model}' to download it first or
|
||||
// try selecting one from the Installed models: ${installedModels.join(', ')}`;
|
||||
// onText(errorMessage, errorMessage);
|
||||
// onFinalMessage(errorMessage);
|
||||
// return Promise.reject();
|
||||
// }
|
||||
|
||||
// if (mode === 'fim') {
|
||||
// // the fim prompt is the last message
|
||||
// let prompt = messages[messages.length - 1].content
|
||||
// return ollama.generate({
|
||||
// model: voidConfig.ollama.model,
|
||||
// prompt: prompt,
|
||||
// stream: true,
|
||||
// raw: true,
|
||||
// options: { stop: options?.stopTokens }
|
||||
// })
|
||||
// }
|
||||
|
||||
// return ollama.chat({
|
||||
// model: voidConfig.ollama.model,
|
||||
// messages: messages,
|
||||
// stream: true,
|
||||
// options: { num_predict: parseMaxTokensStr(voidConfig.default.maxTokens) }
|
||||
// });
|
||||
// })
|
||||
// .then(async stream => {
|
||||
// if (!stream) return;
|
||||
|
||||
// abortRef.current = () => {
|
||||
// didAbort = true
|
||||
// stream.abort()
|
||||
// }
|
||||
// for await (const chunk of stream) {
|
||||
// if (didAbort) return;
|
||||
|
||||
// const newText = (mode === 'fim'
|
||||
// ? (chunk as GenerateResponse).response
|
||||
// : (chunk as ChatResponse).message.content
|
||||
// )
|
||||
// fullText += newText;
|
||||
// onText(newText, fullText);
|
||||
// }
|
||||
// onFinalMessage(fullText);
|
||||
// })
|
||||
// .catch(error => {
|
||||
// // Check if the error is a connection error
|
||||
// if (error instanceof Error && error.message.includes('Failed to fetch')) {
|
||||
// const errorMessage = 'Ollama service is not running. Please start the Ollama service and try again.';
|
||||
// onText(errorMessage, errorMessage);
|
||||
// onFinalMessage(errorMessage);
|
||||
// } else if (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 = ({ options, mode, messages, fimInfo, onText, onFinalMessage, onError, voidConfig, abortRef }) => {
|
||||
// if (!voidConfig)
|
||||
// return onError('No config file found for LLM.');
|
||||
|
||||
// // handle defaults
|
||||
// if (!mode) mode = 'chat'
|
||||
// if (!messages) messages = []
|
||||
|
||||
// // build messages
|
||||
// if (mode === 'chat') {
|
||||
// // nothing needed
|
||||
// } else if (mode === 'fim') {
|
||||
// fimInfo = fimInfo!
|
||||
|
||||
// const system = getFIMSystem({ voidConfig, fimInfo })
|
||||
// const prompt = getFIMPrompt({ voidConfig, fimInfo })
|
||||
// messages = ([
|
||||
// { role: 'system', content: system },
|
||||
// { role: 'user', content: prompt }
|
||||
// ] as const)
|
||||
|
||||
// }
|
||||
|
||||
// // trim message content (Anthropic and other providers give an error if there is trailing whitespace)
|
||||
// messages = messages.map(m => ({ ...m, content: m.content.trim() }))
|
||||
// .filter(m => m.content !== '')
|
||||
|
||||
// if (messages.length === 0)
|
||||
// return onError('No messages provided to LLM.');
|
||||
|
||||
// switch (voidConfig.default.whichApi) {
|
||||
// case 'anthropic':
|
||||
// return sendAnthropicMsg({ options, mode, messages, onText, onFinalMessage, onError, voidConfig, abortRef });
|
||||
// case 'openAI':
|
||||
// case 'openRouter':
|
||||
// case 'openAICompatible':
|
||||
// return sendOpenAIMsg({ options, mode, messages, onText, onFinalMessage, onError, voidConfig, abortRef });
|
||||
// case 'gemini':
|
||||
// return sendGeminiMsg({ options, mode, messages, onText, onFinalMessage, onError, voidConfig, abortRef });
|
||||
// case 'ollama':
|
||||
// return sendOllamaMsg({ options, mode, messages, onText, onFinalMessage, onError, voidConfig, abortRef });
|
||||
// case 'greptile':
|
||||
// return sendGreptileMsg({ options, mode, messages, onText, onFinalMessage, onError, voidConfig, abortRef });
|
||||
// default:
|
||||
// onError(`Error: whichApi was ${voidConfig.default.whichApi}, which is not recognized!`)
|
||||
// }
|
||||
|
||||
// }
|
||||
@@ -0,0 +1,93 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { ConfigState } from '../../../registerConfig.js'
|
||||
import { VoidSidebarState, ReactServicesType } from '../../../registerSidebar.js'
|
||||
import { ThreadsState } from '../../../registerThreads.js'
|
||||
|
||||
|
||||
// normally to do this you'd use a useEffect that calls .onDidChangeState(), but useEffect mounts too late and misses initial state changes
|
||||
|
||||
let services: ReactServicesType
|
||||
|
||||
// even if React hasn't mounted yet, these variables are always updated to the latest state:
|
||||
let sidebarState: VoidSidebarState
|
||||
let configState: ConfigState
|
||||
let threadsState: ThreadsState
|
||||
|
||||
// React listens by adding a setState function to these:
|
||||
const sidebarStateListeners: Set<(s: VoidSidebarState) => void> = new Set()
|
||||
const configStateListeners: Set<(s: ConfigState) => void> = new Set()
|
||||
const threadsStateListeners: Set<(s: ThreadsState) => void> = new Set()
|
||||
|
||||
// must call this before you can use any of the hooks below
|
||||
// this should only be called ONCE! this is the only place you don't need to dispose onDidChange. If you use state.onDidChange anywhere else, make sure to dispose it!
|
||||
|
||||
let wasCalled = false
|
||||
|
||||
export const _registerServices = (services_: ReactServicesType) => {
|
||||
|
||||
if (wasCalled) console.error(`void _registerServices was called again! It should only be called once.`)
|
||||
wasCalled = true
|
||||
|
||||
services = services_
|
||||
const { sidebarStateService, configStateService, threadsStateService, } = services
|
||||
|
||||
sidebarState = sidebarStateService.state
|
||||
sidebarStateService.onDidChangeState(() => {
|
||||
sidebarState = sidebarStateService.state
|
||||
sidebarStateListeners.forEach(l => l(sidebarState))
|
||||
})
|
||||
|
||||
configState = configStateService.state
|
||||
configStateService.onDidChangeState(() => {
|
||||
configState = configStateService.state
|
||||
configStateListeners.forEach(l => l(configState))
|
||||
})
|
||||
|
||||
threadsState = threadsStateService.state
|
||||
threadsStateService.onDidChangeCurrentThread(() => {
|
||||
threadsState = threadsStateService.state
|
||||
threadsStateListeners.forEach(l => l(threadsState))
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
|
||||
// -- services --
|
||||
export const useService = <T extends keyof ReactServicesType,>(serviceName: T) => {
|
||||
if (services === null) {
|
||||
throw new Error('useAccessor must be used within an AccessorProvider')
|
||||
}
|
||||
return services[serviceName] as ReactServicesType[T]
|
||||
}
|
||||
|
||||
// -- state of services --
|
||||
|
||||
export const useSidebarState = () => {
|
||||
const [s, ss] = useState(sidebarState)
|
||||
useEffect(() => {
|
||||
ss(sidebarState)
|
||||
sidebarStateListeners.add(ss)
|
||||
return () => { sidebarStateListeners.delete(ss) }
|
||||
}, [ss])
|
||||
return s
|
||||
}
|
||||
|
||||
export const useConfigState = () => {
|
||||
const [s, ss] = useState(configState)
|
||||
useEffect(() => {
|
||||
ss(configState)
|
||||
configStateListeners.add(ss)
|
||||
return () => { configStateListeners.delete(ss) }
|
||||
}, [ss])
|
||||
return s
|
||||
}
|
||||
|
||||
export const useThreadsState = () => {
|
||||
const [s, ss] = useState(threadsState)
|
||||
useEffect(() => {
|
||||
ss(threadsState)
|
||||
threadsStateListeners.add(ss)
|
||||
return () => { threadsStateListeners.delete(ss) }
|
||||
}, [ss])
|
||||
return s
|
||||
}
|
||||
+5
-4
@@ -1,10 +1,9 @@
|
||||
/** @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}"],
|
||||
content: ['./src2/**/*.{jsx,tsx}'], // uses these files to decide how to transform the css file
|
||||
theme: {
|
||||
extend: {
|
||||
// inject user's vscode theme colors: https://code.visualstudio.com/api/extension-guides/webview#theming-webview-content
|
||||
colors: {
|
||||
vscode: {
|
||||
"sidebar-bg": "var(--vscode-sideBar-background)",
|
||||
@@ -28,4 +27,6 @@ module.exports = {
|
||||
},
|
||||
},
|
||||
plugins: [],
|
||||
};
|
||||
prefix: 'prefix-'
|
||||
}
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"strict": true,
|
||||
"exactOptionalPropertyTypes": false,
|
||||
|
||||
"jsx": "react-jsx",
|
||||
"moduleResolution": "NodeNext",
|
||||
"module": "NodeNext",
|
||||
"esModuleInterop": true,
|
||||
},
|
||||
"include": [
|
||||
// this is just for type checking, so src/ is the correct dir
|
||||
"./src/**/*.ts",
|
||||
"./src/**/*.tsx"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { defineConfig } from 'tsup'
|
||||
|
||||
export default defineConfig({
|
||||
entry: [
|
||||
'./src2/sidebar-tsx/Sidebar.tsx',
|
||||
'./src2/util/sendLLMMessage.tsx',
|
||||
'./src2/util/posthog.tsx',
|
||||
'./src2/util/diffLines.tsx',
|
||||
],
|
||||
outDir: './out',
|
||||
format: ['esm'],
|
||||
splitting: false,
|
||||
|
||||
// dts: true,
|
||||
// sourcemap: true,
|
||||
|
||||
clean: true,
|
||||
platform: 'browser', // 'node'
|
||||
target: 'esnext',
|
||||
injectStyle: true, // bundle css into the output file
|
||||
outExtension: () => ({ js: '.js' }),
|
||||
// default behavior is to take local files and make them internal (bundle them) and take imports like 'react' and leave them external (don't bundle them), we want the opposite in many ways
|
||||
noExternal: [ // noExternal means we should take these things and make them not external (bundle them into the output file) - anything that doesn't start with a "." needs to be force-flagged as not external
|
||||
/^(?!\.).*$/
|
||||
],
|
||||
external: [ // these imports should be kept external ../../../ are external (this is just an optimization so the output file doesn't re-implement functions)
|
||||
new RegExp('../../../*.js'
|
||||
.replaceAll('.', '\\.')
|
||||
.replaceAll('*', '.*'))
|
||||
],
|
||||
treeshake: true,
|
||||
esbuildOptions(options) {
|
||||
options.outbase = 'src2' // tries copying the folder hierarchy starting at src2
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,153 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Glass Devtools, Inc. All rights reserved.
|
||||
* Void Editor additions licensed under the AGPLv3 License.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { KeyCode, KeyMod } from '../../../../base/common/keyCodes.js';
|
||||
|
||||
|
||||
import { Action2, MenuId, registerAction2 } from '../../../../platform/actions/common/actions.js';
|
||||
import { ServicesAccessor } from '../../../../editor/browser/editorExtensions.js';
|
||||
|
||||
import { KeybindingWeight } from '../../../../platform/keybinding/common/keybindingsRegistry.js';
|
||||
import { ContextKeyExpr } from '../../../../platform/contextkey/common/contextkey.js';
|
||||
import { CodeStagingSelection, IThreadHistoryService } from './registerThreads.js';
|
||||
// import { IVoidConfigService } from './registerSettings.js';
|
||||
// import { IEditorService } from '../../../services/editor/common/editorService.js';
|
||||
|
||||
import { IEditorService } from '../../../services/editor/common/editorService.js';
|
||||
import { ICodeEditorService } from '../../../../editor/browser/services/codeEditorService.js';
|
||||
import { IRange } from '../../../../editor/common/core/range.js';
|
||||
import { ITextModel } from '../../../../editor/common/model.js';
|
||||
import { IVoidSidebarStateService, VOID_VIEW_ID } from './registerSidebar.js';
|
||||
// import { IClipboardService } from '../../../../platform/clipboard/common/clipboardService.js';
|
||||
|
||||
|
||||
// ---------- Register commands and keybindings ----------
|
||||
|
||||
|
||||
const roundRangeToLines = (range: IRange | null | undefined) => {
|
||||
if (!range)
|
||||
return null
|
||||
// IRange is 1-indexed
|
||||
const endLine = range.endColumn === 1 ? range.endLineNumber - 1 : range.endLineNumber // e.g. if the user triple clicks, it selects column=0, line=line -> column=0, line=line+1
|
||||
const newRange: IRange = {
|
||||
startLineNumber: range.startLineNumber,
|
||||
startColumn: 1,
|
||||
endLineNumber: endLine,
|
||||
endColumn: Number.MAX_SAFE_INTEGER
|
||||
}
|
||||
return newRange
|
||||
}
|
||||
|
||||
const getContentInRange = (model: ITextModel, range: IRange | null) => {
|
||||
if (!range)
|
||||
return null
|
||||
const content = model.getValueInRange(range)
|
||||
const trimmedContent = content
|
||||
.replace(/^\s*\n/g, '') // trim pure whitespace lines from start
|
||||
.replace(/\n\s*$/g, '') // trim pure whitespace lines from end
|
||||
return trimmedContent
|
||||
}
|
||||
|
||||
// Action: when press ctrl+L, show the sidebar chat and add to the selection
|
||||
registerAction2(class extends Action2 {
|
||||
constructor() {
|
||||
super({ id: 'void.ctrl+l', title: 'Show Sidebar', keybinding: { primary: KeyMod.CtrlCmd | KeyCode.KeyL, weight: KeybindingWeight.BuiltinExtension } });
|
||||
}
|
||||
async run(accessor: ServicesAccessor): Promise<void> {
|
||||
|
||||
const model = accessor.get(ICodeEditorService).getActiveCodeEditor()?.getModel()
|
||||
if (!model)
|
||||
return
|
||||
|
||||
|
||||
const stateService = accessor.get(IVoidSidebarStateService)
|
||||
stateService.setState({ isHistoryOpen: false, currentTab: 'chat' })
|
||||
stateService.fireFocusChat()
|
||||
|
||||
// add selection
|
||||
const threadHistoryService = accessor.get(IThreadHistoryService)
|
||||
const currentStaging = threadHistoryService.state._currentStagingSelections
|
||||
const currentStagingEltIdx = currentStaging?.findIndex(s => s.fileURI.fsPath === model.uri.fsPath)
|
||||
|
||||
// if there exists a selection with this URI, replace it
|
||||
const selectionRange = roundRangeToLines(
|
||||
accessor.get(IEditorService).activeTextEditorControl?.getSelection()
|
||||
)
|
||||
|
||||
if (selectionRange) {
|
||||
const selection: CodeStagingSelection = {
|
||||
selectionStr: getContentInRange(model, selectionRange),
|
||||
fileURI: model.uri
|
||||
}
|
||||
|
||||
if (currentStagingEltIdx !== undefined && currentStagingEltIdx !== -1) {
|
||||
threadHistoryService.setStaging([
|
||||
...currentStaging!.slice(0, currentStagingEltIdx),
|
||||
selection,
|
||||
...currentStaging!.slice(currentStagingEltIdx + 1, Infinity)
|
||||
])
|
||||
}
|
||||
else {
|
||||
threadHistoryService.setStaging([...(currentStaging ?? []), selection])
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// New chat menu button
|
||||
registerAction2(class extends Action2 {
|
||||
constructor() {
|
||||
super({
|
||||
id: 'void.newChatAction',
|
||||
title: 'View past chats',
|
||||
icon: { id: 'add' },
|
||||
menu: [{ id: MenuId.ViewTitle, group: 'navigation', when: ContextKeyExpr.equals('view', VOID_VIEW_ID), }]
|
||||
});
|
||||
}
|
||||
async run(accessor: ServicesAccessor): Promise<void> {
|
||||
const stateService = accessor.get(IVoidSidebarStateService)
|
||||
stateService.setState({ isHistoryOpen: false, currentTab: 'chat' })
|
||||
stateService.fireFocusChat()
|
||||
|
||||
const historyService = accessor.get(IThreadHistoryService)
|
||||
historyService.startNewThread()
|
||||
}
|
||||
})
|
||||
|
||||
// History menu button
|
||||
registerAction2(class extends Action2 {
|
||||
constructor() {
|
||||
super({
|
||||
id: 'void.historyAction',
|
||||
title: 'View past chats',
|
||||
icon: { id: 'history' },
|
||||
menu: [{ id: MenuId.ViewTitle, group: 'navigation', when: ContextKeyExpr.equals('view', VOID_VIEW_ID), }]
|
||||
});
|
||||
}
|
||||
async run(accessor: ServicesAccessor): Promise<void> {
|
||||
const stateService = accessor.get(IVoidSidebarStateService)
|
||||
stateService.setState({ isHistoryOpen: !stateService.state.isHistoryOpen, currentTab: 'chat' })
|
||||
stateService.fireBlurChat()
|
||||
}
|
||||
})
|
||||
|
||||
// Settings (API config) menu button
|
||||
registerAction2(class extends Action2 {
|
||||
constructor() {
|
||||
super({
|
||||
id: 'void.viewSettings',
|
||||
title: 'Void settings',
|
||||
icon: { id: 'settings-gear' },
|
||||
menu: [{ id: MenuId.ViewTitle, group: 'navigation', when: ContextKeyExpr.equals('view', VOID_VIEW_ID), }]
|
||||
});
|
||||
}
|
||||
async run(accessor: ServicesAccessor): Promise<void> {
|
||||
const stateService = accessor.get(IVoidSidebarStateService)
|
||||
stateService.setState({ isHistoryOpen: false, currentTab: stateService.state.currentTab === 'settings' ? 'chat' : 'settings' })
|
||||
stateService.fireBlurChat()
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,313 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Glass Devtools, Inc. All rights reserved.
|
||||
* Void Editor additions licensed under the AGPLv3 License.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Emitter, Event } from '../../../../base/common/event.js';
|
||||
import { Disposable } from '../../../../base/common/lifecycle.js';
|
||||
import { IEncryptionService } from '../../../../platform/encryption/common/encryptionService.js';
|
||||
import { registerSingleton, InstantiationType } from '../../../../platform/instantiation/common/extensions.js';
|
||||
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
|
||||
import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js';
|
||||
|
||||
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 nonDefaultConfigFields = [
|
||||
'anthropic',
|
||||
'openAI',
|
||||
'gemini',
|
||||
'greptile',
|
||||
'ollama',
|
||||
'openRouter',
|
||||
'openAICompatible',
|
||||
'azure',
|
||||
] as const
|
||||
|
||||
|
||||
|
||||
const voidConfigInfo: Record<
|
||||
typeof nonDefaultConfigFields[number] | 'default', {
|
||||
[prop: string]: {
|
||||
description: string;
|
||||
enumArr?: readonly string[] | undefined;
|
||||
defaultVal: string;
|
||||
};
|
||||
}
|
||||
> = {
|
||||
default: {
|
||||
whichApi: configEnum(
|
||||
'API Provider.',
|
||||
'anthropic',
|
||||
nonDefaultConfigFields,
|
||||
),
|
||||
|
||||
maxTokens: configEnum(
|
||||
'Max number of tokens to output.',
|
||||
'1024',
|
||||
[
|
||||
'default', // this will be parseInt'd into NaN and ignored by the API. Anything that's not a number has this behavior.
|
||||
'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'
|
||||
),
|
||||
model: configEnum(
|
||||
'Ollama model to use.',
|
||||
'codestral',
|
||||
['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'] 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.'
|
||||
// }
|
||||
// }
|
||||
// },
|
||||
},
|
||||
gemini: {
|
||||
apikey: configString('Google API key.', ''),
|
||||
model: configEnum(
|
||||
'Gemini model to use.',
|
||||
'gemini-1.5-flash',
|
||||
[
|
||||
'gemini-1.5-flash',
|
||||
'gemini-1.5-pro',
|
||||
'gemini-1.5-flash-8b',
|
||||
'gemini-1.0-pro'
|
||||
] as const
|
||||
),
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
// this is the type that comes with metadata like desc, default val, etc
|
||||
export 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']
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const getVoidConfig = (partialVoidConfig: PartialVoidConfig): VoidConfig => {
|
||||
const config = {} as PartialVoidConfig
|
||||
for (const field of [...nonDefaultConfigFields, 'default'] as const) {
|
||||
config[field] = {}
|
||||
for (const prop in voidConfigInfo[field]) {
|
||||
config[field][prop] = partialVoidConfig[field]?.[prop]?.trim() || voidConfigInfo[field][prop].defaultVal
|
||||
}
|
||||
}
|
||||
return config as VoidConfig
|
||||
}
|
||||
|
||||
|
||||
const VOID_CONFIG_KEY = 'void.partialVoidConfig'
|
||||
|
||||
export type SetFieldFnType = <K extends VoidConfigField>(field: K, param: keyof VoidConfigInfo[K], newVal: string) => Promise<void>;
|
||||
|
||||
export type ConfigState = {
|
||||
partialVoidConfig: PartialVoidConfig; // free parameter
|
||||
voidConfig: VoidConfig; // computed from partialVoidConfig
|
||||
}
|
||||
|
||||
export interface IVoidConfigStateService {
|
||||
readonly _serviceBrand: undefined;
|
||||
readonly state: ConfigState;
|
||||
readonly voidConfigInfo: VoidConfigInfo;
|
||||
onDidChangeState: Event<void>;
|
||||
setField: SetFieldFnType;
|
||||
}
|
||||
|
||||
export const IVoidConfigStateService = createDecorator<IVoidConfigStateService>('VoidConfigStateService');
|
||||
class VoidConfigStateService extends Disposable implements IVoidConfigStateService {
|
||||
_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: ConfigState;
|
||||
readonly voidConfigInfo: VoidConfigInfo = voidConfigInfo; // just putting this here for simplicity, it's static though
|
||||
|
||||
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, just treat partialVoidConfig like it's empty
|
||||
this.state = {
|
||||
partialVoidConfig: {},
|
||||
voidConfig: getVoidConfig({}),
|
||||
}
|
||||
|
||||
// read and update the actual state immediately
|
||||
this._readPartialVoidConfig().then(partialVoidConfig => {
|
||||
this._setState(partialVoidConfig)
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
private async _readPartialVoidConfig(): Promise<PartialVoidConfig> {
|
||||
const encryptedPartialConfig = this._storageService.get(VOID_CONFIG_KEY, StorageScope.APPLICATION)
|
||||
|
||||
if (!encryptedPartialConfig)
|
||||
return {}
|
||||
|
||||
const partialVoidConfigStr = await this._encryptionService.decrypt(encryptedPartialConfig)
|
||||
return JSON.parse(partialVoidConfigStr)
|
||||
}
|
||||
|
||||
|
||||
private async _storePartialVoidConfig(partialVoidConfig: PartialVoidConfig) {
|
||||
const encryptedPartialConfigStr = await this._encryptionService.encrypt(JSON.stringify(partialVoidConfig))
|
||||
this._storageService.store(VOID_CONFIG_KEY, encryptedPartialConfigStr, StorageScope.APPLICATION, StorageTarget.USER)
|
||||
}
|
||||
|
||||
|
||||
// Set field on PartialVoidConfig
|
||||
setField: SetFieldFnType = async <K extends VoidConfigField>(field: K, param: keyof VoidConfigInfo[K], newVal: string) => {
|
||||
const { partialVoidConfig } = this.state
|
||||
|
||||
const newPartialConfig: PartialVoidConfig = {
|
||||
...partialVoidConfig,
|
||||
[field]: {
|
||||
...partialVoidConfig[field],
|
||||
[param]: newVal
|
||||
}
|
||||
}
|
||||
await this._storePartialVoidConfig(newPartialConfig)
|
||||
this._setState(newPartialConfig)
|
||||
}
|
||||
|
||||
// internal function to update state, should be called every time state changes
|
||||
private async _setState(partialVoidConfig: PartialVoidConfig) {
|
||||
this.state = {
|
||||
partialVoidConfig: partialVoidConfig,
|
||||
voidConfig: getVoidConfig(partialVoidConfig),
|
||||
}
|
||||
this._onDidChangeState.fire()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
registerSingleton(IVoidConfigStateService, VoidConfigStateService, InstantiationType.Eager);
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,52 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Glass Devtools, Inc. All rights reserved.
|
||||
* Void Editor additions licensed under the AGPLv3 License.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
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 { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js';
|
||||
|
||||
import { posthog } from './react/out/util/posthog.js'
|
||||
|
||||
|
||||
|
||||
// const buildEnv = 'development';
|
||||
// const buildNumber = '1.0.0';
|
||||
// const isMac = process.platform === 'darwin';
|
||||
// // TODO use commandKey
|
||||
// const commandKey = isMac ? '⌘' : 'Ctrl';
|
||||
// const systemInfo = {
|
||||
// buildEnv,
|
||||
// buildNumber,
|
||||
// isMac,
|
||||
// }
|
||||
|
||||
|
||||
|
||||
interface IMetricsService {
|
||||
readonly _serviceBrand: undefined;
|
||||
}
|
||||
|
||||
const IMetricsService = createDecorator<IMetricsService>('metricsService');
|
||||
class MetricsService extends Disposable implements IMetricsService {
|
||||
_serviceBrand: undefined;
|
||||
|
||||
constructor(
|
||||
@ITelemetryService private readonly _telemetryService: ITelemetryService
|
||||
) {
|
||||
super()
|
||||
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
|
||||
})
|
||||
const deviceId = this._telemetryService.devDeviceId
|
||||
console.debug('deviceId', deviceId)
|
||||
posthog.identify(deviceId)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
registerSingleton(IMetricsService, MetricsService, InstantiationType.Eager);
|
||||
@@ -0,0 +1,246 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Glass Devtools, Inc. All rights reserved.
|
||||
* Void Editor additions licensed under the AGPLv3 License.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Registry } from '../../../../platform/registry/common/platform.js';
|
||||
import {
|
||||
Extensions as ViewContainerExtensions, IViewContainersRegistry,
|
||||
ViewContainerLocation, IViewsRegistry, Extensions as ViewExtensions,
|
||||
IViewDescriptorService,
|
||||
} from '../../../common/views.js';
|
||||
|
||||
import * as nls from '../../../../nls.js';
|
||||
import * as dom from '../../../../base/browser/dom.js';
|
||||
|
||||
import { Codicon } from '../../../../base/common/codicons.js';
|
||||
import { localize } from '../../../../nls.js';
|
||||
import { registerIcon } from '../../../../platform/theme/common/iconRegistry.js';
|
||||
import { ViewPaneContainer } from '../../../browser/parts/views/viewPaneContainer.js';
|
||||
|
||||
import { SyncDescriptor } from '../../../../platform/instantiation/common/descriptors.js';
|
||||
import { KeyCode, KeyMod } from '../../../../base/common/keyCodes.js';
|
||||
|
||||
|
||||
import { IViewPaneOptions, ViewPane } from '../../../browser/parts/views/viewPane.js';
|
||||
|
||||
import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';
|
||||
import { createDecorator, IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';
|
||||
import { Disposable } from '../../../../base/common/lifecycle.js';
|
||||
import { Emitter, Event } from '../../../../base/common/event.js';
|
||||
import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js';
|
||||
import { IViewsService } from '../../../services/views/common/viewsService.js';
|
||||
import { IThreadHistoryService } from './registerThreads.js';
|
||||
import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';
|
||||
import { IThemeService } from '../../../../platform/theme/common/themeService.js';
|
||||
import { IContextMenuService } from '../../../../platform/contextview/browser/contextView.js';
|
||||
import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js';
|
||||
import { IOpenerService } from '../../../../platform/opener/common/opener.js';
|
||||
import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js';
|
||||
import { IHoverService } from '../../../../platform/hover/browser/hover.js';
|
||||
// import { IVoidConfigService } from './registerSettings.js';
|
||||
// import { IEditorService } from '../../../services/editor/common/editorService.js';
|
||||
|
||||
import mountFn from './react/out/sidebar-tsx/Sidebar.js';
|
||||
|
||||
import { IVoidConfigStateService } from './registerConfig.js';
|
||||
import { IFileService } from '../../../../platform/files/common/files.js';
|
||||
import { IInlineDiffsService } from './registerInlineDiffs.js';
|
||||
import { IModelService } from '../../../../editor/common/services/model.js';
|
||||
import { ISendLLMMessageService } from '../../../../platform/void/browser/llmMessageService.js';
|
||||
|
||||
|
||||
// import { IClipboardService } from '../../../../platform/clipboard/common/clipboardService.js';
|
||||
|
||||
|
||||
// compare against search.contribution.ts and https://app.greptile.com/chat/w1nsmt3lauwzculipycpn?repo=github%3Amain%3Amicrosoft%2Fvscode
|
||||
// and debug.contribution.ts, scm.contribution.ts (source control)
|
||||
|
||||
export type VoidSidebarState = {
|
||||
isHistoryOpen: boolean;
|
||||
currentTab: 'chat' | 'settings';
|
||||
}
|
||||
|
||||
export type ReactServicesType = {
|
||||
sidebarStateService: IVoidSidebarStateService;
|
||||
configStateService: IVoidConfigStateService;
|
||||
threadsStateService: IThreadHistoryService;
|
||||
fileService: IFileService;
|
||||
modelService: IModelService;
|
||||
inlineDiffService: IInlineDiffsService;
|
||||
sendLLMMessageService: ISendLLMMessageService;
|
||||
}
|
||||
|
||||
// ---------- Define viewpane ----------
|
||||
|
||||
class VoidSidebarViewPane extends ViewPane {
|
||||
|
||||
constructor(
|
||||
options: IViewPaneOptions,
|
||||
@IInstantiationService instantiationService: IInstantiationService,
|
||||
@IViewDescriptorService viewDescriptorService: IViewDescriptorService,
|
||||
@IConfigurationService configurationService: IConfigurationService,
|
||||
@IContextKeyService contextKeyService: IContextKeyService,
|
||||
@IThemeService themeService: IThemeService,
|
||||
@IContextMenuService contextMenuService: IContextMenuService,
|
||||
@IKeybindingService keybindingService: IKeybindingService,
|
||||
@IOpenerService openerService: IOpenerService,
|
||||
@ITelemetryService telemetryService: ITelemetryService,
|
||||
@IHoverService hoverService: IHoverService,
|
||||
// Void:
|
||||
// @IVoidSidebarStateService private readonly _voidSidebarStateService: IVoidSidebarStateService,
|
||||
// @IThreadHistoryService private readonly _threadHistoryService: IThreadHistoryService,
|
||||
// TODO chat service
|
||||
) {
|
||||
super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, telemetryService, hoverService)
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
protected override renderBody(parent: HTMLElement): void {
|
||||
super.renderBody(parent);
|
||||
|
||||
const { root } = dom.h('div@root')
|
||||
dom.append(parent, root);
|
||||
|
||||
// gets set immediately
|
||||
this.instantiationService.invokeFunction(accessor => {
|
||||
const services: ReactServicesType = {
|
||||
configStateService: accessor.get(IVoidConfigStateService),
|
||||
sidebarStateService: accessor.get(IVoidSidebarStateService),
|
||||
threadsStateService: accessor.get(IThreadHistoryService),
|
||||
fileService: accessor.get(IFileService),
|
||||
modelService: accessor.get(IModelService),
|
||||
inlineDiffService: accessor.get(IInlineDiffsService),
|
||||
sendLLMMessageService: accessor.get(ISendLLMMessageService),
|
||||
}
|
||||
mountFn(root, services);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
// ---------- Register viewpane inside the void container ----------
|
||||
|
||||
const voidThemeIcon = Codicon.symbolObject;
|
||||
const voidViewIcon = registerIcon('void-view-icon', voidThemeIcon, localize('voidViewIcon', 'View icon of the Void chat view.'));
|
||||
|
||||
// called VIEWLET_ID in other places for some reason
|
||||
export const VOID_VIEW_CONTAINER_ID = 'workbench.view.void'
|
||||
export const VOID_VIEW_ID = VOID_VIEW_CONTAINER_ID // not sure if we can change this
|
||||
|
||||
// Register view container
|
||||
const viewContainerRegistry = Registry.as<IViewContainersRegistry>(ViewContainerExtensions.ViewContainersRegistry);
|
||||
const viewContainer = viewContainerRegistry.registerViewContainer({
|
||||
id: VOID_VIEW_CONTAINER_ID,
|
||||
title: nls.localize2('void', 'Void'), // this is used to say "Void" (Ctrl + L)
|
||||
ctorDescriptor: new SyncDescriptor(ViewPaneContainer, [VOID_VIEW_CONTAINER_ID, { mergeViewWithContainerWhenSingleView: true }]),
|
||||
hideIfEmpty: false,
|
||||
icon: voidViewIcon,
|
||||
order: 1,
|
||||
}, ViewContainerLocation.AuxiliaryBar, { doNotRegisterOpenCommand: true, });
|
||||
|
||||
|
||||
|
||||
// Register search default location to the container (sidebar)
|
||||
const viewsRegistry = Registry.as<IViewsRegistry>(ViewExtensions.ViewsRegistry);
|
||||
viewsRegistry.registerViews([{
|
||||
id: VOID_VIEW_ID,
|
||||
hideByDefault: false, // start open
|
||||
containerIcon: voidViewIcon,
|
||||
name: nls.localize2('void chat', "Chat"), // this says ... : CHAT
|
||||
ctorDescriptor: new SyncDescriptor(VoidSidebarViewPane),
|
||||
canToggleVisibility: false,
|
||||
canMoveView: true,
|
||||
openCommandActionDescriptor: {
|
||||
id: viewContainer.id,
|
||||
keybindings: {
|
||||
primary: KeyMod.CtrlCmd | KeyCode.KeyL,
|
||||
},
|
||||
order: 1
|
||||
},
|
||||
}], viewContainer);
|
||||
|
||||
|
||||
|
||||
// ---------- Register service that manages sidebar's state ----------
|
||||
|
||||
export interface IVoidSidebarStateService {
|
||||
readonly _serviceBrand: undefined;
|
||||
|
||||
readonly state: VoidSidebarState; // readonly to the user
|
||||
setState(newState: Partial<VoidSidebarState>): void;
|
||||
onDidChangeState: Event<void>;
|
||||
|
||||
onDidFocusChat: Event<void>;
|
||||
onDidBlurChat: Event<void>;
|
||||
fireFocusChat(): void;
|
||||
fireBlurChat(): void;
|
||||
|
||||
openView(): void;
|
||||
}
|
||||
|
||||
|
||||
export const IVoidSidebarStateService = createDecorator<IVoidSidebarStateService>('voidSidebarStateService');
|
||||
class VoidSidebarStateService extends Disposable implements IVoidSidebarStateService {
|
||||
_serviceBrand: undefined;
|
||||
|
||||
private readonly _onDidChangeState = new Emitter<void>();
|
||||
readonly onDidChangeState: Event<void> = this._onDidChangeState.event;
|
||||
|
||||
private readonly _onFocusChat = new Emitter<void>();
|
||||
readonly onDidFocusChat: Event<void> = this._onFocusChat.event;
|
||||
|
||||
private readonly _onBlurChat = new Emitter<void>();
|
||||
readonly onDidBlurChat: Event<void> = this._onBlurChat.event;
|
||||
|
||||
|
||||
// state
|
||||
state: VoidSidebarState
|
||||
|
||||
|
||||
setState(newState: Partial<VoidSidebarState>) {
|
||||
// make sure view is open if the tab changes
|
||||
if ('currentTab' in newState) {
|
||||
this.openView()
|
||||
}
|
||||
|
||||
this.state = { ...this.state, ...newState }
|
||||
this._onDidChangeState.fire()
|
||||
}
|
||||
|
||||
fireFocusChat() {
|
||||
this._onFocusChat.fire()
|
||||
}
|
||||
|
||||
fireBlurChat() {
|
||||
this._onBlurChat.fire()
|
||||
}
|
||||
|
||||
openView() {
|
||||
this._viewsService.openViewContainer(VOID_VIEW_CONTAINER_ID);
|
||||
this._viewsService.openView(VOID_VIEW_ID);
|
||||
}
|
||||
|
||||
constructor(
|
||||
@IViewsService private readonly _viewsService: IViewsService,
|
||||
// @IThreadHistoryService private readonly _threadHistoryService: IThreadHistoryService,
|
||||
) {
|
||||
super()
|
||||
// auto open the view on mount (if it bothers you this is here, this is technically just initializing the state of the view)
|
||||
this.openView()
|
||||
|
||||
// initial state
|
||||
this.state = {
|
||||
isHistoryOpen: false,
|
||||
currentTab: 'chat',
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
registerSingleton(IVoidSidebarStateService, VoidSidebarStateService, InstantiationType.Eager);
|
||||
@@ -0,0 +1,196 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Glass Devtools, Inc. All rights reserved.
|
||||
* Void Editor additions licensed under the AGPLv3 License.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
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 { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js';
|
||||
|
||||
import { URI } from '../../../../base/common/uri.js';
|
||||
import { Emitter, Event } from '../../../../base/common/event.js';
|
||||
|
||||
// if selectionStr is null, it means just send the whole file
|
||||
export type CodeSelection = {
|
||||
selectionStr: string | null;
|
||||
fileURI: URI;
|
||||
content: string;
|
||||
}
|
||||
|
||||
export type CodeStagingSelection = {
|
||||
selectionStr: string | null;
|
||||
fileURI: URI;
|
||||
}
|
||||
|
||||
export type ChatMessage =
|
||||
| {
|
||||
role: 'user';
|
||||
content: string; // content sent to the llm
|
||||
displayContent: string; // content displayed to user
|
||||
selections: CodeSelection[] | null; // the user's selection
|
||||
}
|
||||
| {
|
||||
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;
|
||||
}
|
||||
|
||||
// a 'thread' means a chat message history
|
||||
export type ChatThreads = {
|
||||
[id: string]: {
|
||||
id: string; // store the id here too
|
||||
createdAt: string; // ISO string
|
||||
lastModified: string; // ISO string
|
||||
messages: ChatMessage[];
|
||||
};
|
||||
}
|
||||
|
||||
export type ThreadsState = {
|
||||
allThreads: ChatThreads;
|
||||
_currentThreadId: string | null; // intended for internal use only
|
||||
_currentStagingSelections: CodeStagingSelection[] | null;
|
||||
}
|
||||
|
||||
|
||||
const newThreadObject = () => {
|
||||
const now = new Date().toISOString()
|
||||
return {
|
||||
id: new Date().getTime().toString(),
|
||||
createdAt: now,
|
||||
lastModified: now,
|
||||
messages: [],
|
||||
}
|
||||
}
|
||||
|
||||
const THREAD_STORAGE_KEY = 'void.threadsHistory'
|
||||
|
||||
export interface IThreadHistoryService {
|
||||
readonly _serviceBrand: undefined;
|
||||
|
||||
readonly state: ThreadsState;
|
||||
onDidChangeCurrentThread: Event<void>;
|
||||
|
||||
getCurrentThread(state: ThreadsState): ChatThreads[string] | null;
|
||||
startNewThread(): void;
|
||||
switchToThread(threadId: string): void;
|
||||
addMessageToCurrentThread(message: ChatMessage): void;
|
||||
|
||||
setStaging(stagingSelection: CodeStagingSelection[] | null): void;
|
||||
|
||||
}
|
||||
|
||||
export const IThreadHistoryService = createDecorator<IThreadHistoryService>('voidThreadHistoryService');
|
||||
class ThreadHistoryService extends Disposable implements IThreadHistoryService {
|
||||
_serviceBrand: undefined;
|
||||
|
||||
// this fires when the current thread changes at all (a switch of currentThread, or a message added to it, etc)
|
||||
private readonly _onDidChangeCurrentThread = new Emitter<void>();
|
||||
readonly onDidChangeCurrentThread: Event<void> = this._onDidChangeCurrentThread.event;
|
||||
|
||||
state: ThreadsState // allThreads is persisted, currentThread is not
|
||||
|
||||
constructor(
|
||||
@IStorageService private readonly _storageService: IStorageService,
|
||||
) {
|
||||
super()
|
||||
|
||||
this.state = {
|
||||
allThreads: this._readAllThreads(),
|
||||
_currentThreadId: null,
|
||||
_currentStagingSelections: null,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private _readAllThreads(): ChatThreads {
|
||||
const threads = this._storageService.get(THREAD_STORAGE_KEY, StorageScope.APPLICATION)
|
||||
return threads ? JSON.parse(threads) : {}
|
||||
}
|
||||
|
||||
private _storeAllThreads(threads: ChatThreads) {
|
||||
this._storageService.store(THREAD_STORAGE_KEY, JSON.stringify(threads), StorageScope.APPLICATION, StorageTarget.USER)
|
||||
}
|
||||
|
||||
// this should be the only place this.state = ... appears besides constructor
|
||||
private _setState(state: Partial<ThreadsState>, affectsCurrent: boolean) {
|
||||
this.state = {
|
||||
...this.state,
|
||||
...state
|
||||
}
|
||||
if (affectsCurrent)
|
||||
this._onDidChangeCurrentThread.fire()
|
||||
}
|
||||
|
||||
// must "prove" that you have access to the current state by providing it
|
||||
getCurrentThread(state: ThreadsState): ChatThreads[string] | null {
|
||||
return state._currentThreadId ? state.allThreads[state._currentThreadId] ?? null : null;
|
||||
}
|
||||
|
||||
switchToThread(threadId: string) {
|
||||
this._setState({ _currentThreadId: threadId }, true)
|
||||
}
|
||||
|
||||
|
||||
startNewThread() {
|
||||
// if a thread with 0 messages already exists, switch to it
|
||||
const { allThreads: currentThreads } = this.state
|
||||
for (const threadId in currentThreads) {
|
||||
if (currentThreads[threadId].messages.length === 0) {
|
||||
this.switchToThread(threadId)
|
||||
return
|
||||
}
|
||||
}
|
||||
// otherwise, start a new thread
|
||||
const newThread = newThreadObject()
|
||||
|
||||
// update state
|
||||
const newThreads = {
|
||||
...currentThreads,
|
||||
[newThread.id]: newThread
|
||||
}
|
||||
this._storeAllThreads(newThreads)
|
||||
this._setState({ allThreads: newThreads, _currentThreadId: newThread.id }, true)
|
||||
}
|
||||
|
||||
|
||||
addMessageToCurrentThread(message: ChatMessage) {
|
||||
console.log('adding ', message.role, 'to chat')
|
||||
const { allThreads, _currentThreadId } = this.state
|
||||
|
||||
// get the current thread, or create one
|
||||
let currentThread: ChatThreads[string]
|
||||
if (_currentThreadId && (_currentThreadId in allThreads)) {
|
||||
currentThread = allThreads[_currentThreadId]
|
||||
}
|
||||
else {
|
||||
currentThread = newThreadObject()
|
||||
this.state._currentThreadId = currentThread.id
|
||||
}
|
||||
|
||||
// update state and store it
|
||||
const newThreads = {
|
||||
...allThreads,
|
||||
[currentThread.id]: {
|
||||
...currentThread,
|
||||
lastModified: new Date().toISOString(),
|
||||
messages: [...currentThread.messages, message],
|
||||
}
|
||||
}
|
||||
this._storeAllThreads(newThreads)
|
||||
this._setState({ allThreads: newThreads }, true) // the current thread just changed (it had a message added to it)
|
||||
}
|
||||
|
||||
|
||||
setStaging(stagingSelection: CodeStagingSelection[] | null): void {
|
||||
this._setState({ _currentStagingSelections: stagingSelection }, true) // this is a hack for now
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
registerSingleton(IThreadHistoryService, ThreadHistoryService, InstantiationType.Eager);
|
||||
@@ -1,72 +1,25 @@
|
||||
import { Registry } from '../../../../platform/registry/common/platform.js';
|
||||
import {
|
||||
Extensions as ViewContainerExtensions, IViewContainersRegistry,
|
||||
ViewContainerLocation, IViewsRegistry, Extensions as ViewExtensions,
|
||||
IViewDescriptor
|
||||
} from '../../../../workbench/common/views.js';
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Glass Devtools, Inc. All rights reserved.
|
||||
* Void Editor additions licensed under the AGPLv3 License.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as nls from '../../../../nls.js';
|
||||
// register keybinds
|
||||
import './registerActions.js'
|
||||
|
||||
import { VoidViewPane } from '../../../../workbench/contrib/void/browser/voidViewPane.js';
|
||||
// register Settings
|
||||
import './registerConfig.js'
|
||||
|
||||
import { Codicon } from '../../../../base/common/codicons.js';
|
||||
import { localize } from '../../../../nls.js';
|
||||
import { registerIcon } from '../../../../platform/theme/common/iconRegistry.js';
|
||||
import { ViewPaneContainer } from '../../../../workbench/browser/parts/views/viewPaneContainer.js';
|
||||
// register inline diffs
|
||||
import './registerInlineDiffs.js'
|
||||
|
||||
import { SyncDescriptor } from '../../../../platform/instantiation/common/descriptors.js';
|
||||
import { KeyCode, KeyMod } from '../../../../base/common/keyCodes.js';
|
||||
// register Posthog metrics
|
||||
import './registerMetrics.js'
|
||||
|
||||
// register Sidebar chat
|
||||
import './registerSidebar.js'
|
||||
|
||||
const voidViewIcon = registerIcon('void-view-icon', Codicon.search, localize('voidViewIcon', 'View icon of the Void chat view.'));
|
||||
|
||||
|
||||
// compare against search.contribution.ts and https://app.greptile.com/chat/w1nsmt3lauwzculipycpn?repo=github%3Amain%3Amicrosoft%2Fvscode
|
||||
// and debug.contribution.ts, scm.contribution.ts (source control)
|
||||
|
||||
const VIEW_CONTAINER_ID = 'workbench.view.void' // called VIEWLET_ID in other places for some reason
|
||||
|
||||
// Register view container
|
||||
const viewContainerRegistry = Registry.as<IViewContainersRegistry>(ViewContainerExtensions.ViewContainersRegistry);
|
||||
const viewContainer = viewContainerRegistry.registerViewContainer({
|
||||
id: VIEW_CONTAINER_ID,
|
||||
title: nls.localize2('void', 'Void'), // this is used to say "Void" (Ctrl + L)
|
||||
ctorDescriptor: new SyncDescriptor(ViewPaneContainer, [VIEW_CONTAINER_ID, { mergeViewWithContainerWhenSingleView: true }]),
|
||||
hideIfEmpty: false,
|
||||
icon: voidViewIcon,
|
||||
order: 1,
|
||||
}, ViewContainerLocation.AuxiliaryBar, { doNotRegisterOpenCommand: true });
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// this is called a descriptor, but it's the actual View that gets used inside the view container
|
||||
const VIEW_ID = VIEW_CONTAINER_ID // not sure if we can change this
|
||||
const viewDescriptor: IViewDescriptor = {
|
||||
id: VIEW_ID,
|
||||
containerIcon: voidViewIcon,
|
||||
name: nls.localize2('void chat', "Chat"), // this says ... : CHAT
|
||||
ctorDescriptor: new SyncDescriptor(VoidViewPane),
|
||||
canToggleVisibility: false,
|
||||
canMoveView: true,
|
||||
openCommandActionDescriptor: {
|
||||
id: viewContainer.id,
|
||||
keybindings: {
|
||||
primary: KeyMod.CtrlCmd | KeyCode.KeyL, // we don't need to disable the original ctrl+L (probably because it brings panel into focus first)
|
||||
},
|
||||
order: 1
|
||||
// mnemonicTitle: nls.localize({ key: 'miViewSearch', comment: ['&& denotes a mnemonic'] }, "&&Search"),
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
// Register search default location to the container (sidebar)
|
||||
Registry.as<IViewsRegistry>(ViewExtensions.ViewsRegistry).registerViews([viewDescriptor], viewContainer);
|
||||
|
||||
|
||||
// TODO can add a configuration for the user to choose config options - see search.contribution.ts
|
||||
|
||||
// register Thread History
|
||||
import './registerThreads.js'
|
||||
|
||||
// register css
|
||||
import './media/void.css'
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
|
||||
|
||||
|
||||
import { ViewPane } from '../../../../workbench/browser/parts/views/viewPane.js';
|
||||
|
||||
// import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
// import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
|
||||
// import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
|
||||
// import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
// import { IViewPaneOptions, } from 'vs/workbench/browser/parts/views/viewPane';
|
||||
// import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
|
||||
// import { IOpenerService } from 'vs/platform/opener/common/opener';
|
||||
// import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
|
||||
// import { IThemeService } from 'vs/platform/theme/common/themeService';
|
||||
// import { IViewDescriptorService } from 'vs/workbench/common/views';
|
||||
// import { IHoverService } from 'vs/platform/hover/browser/hover';
|
||||
|
||||
|
||||
export class VoidViewPane extends ViewPane {
|
||||
|
||||
// constructor(
|
||||
// options: IViewPaneOptions,
|
||||
// @IInstantiationService instantiationService: IInstantiationService,
|
||||
// @IViewDescriptorService viewDescriptorService: IViewDescriptorService,
|
||||
// @IConfigurationService configurationService: IConfigurationService,
|
||||
// @IContextKeyService contextKeyService: IContextKeyService,
|
||||
// @IThemeService themeService: IThemeService,
|
||||
// @IContextMenuService contextMenuService: IContextMenuService,
|
||||
// @IKeybindingService keybindingService: IKeybindingService,
|
||||
// @IOpenerService openerService: IOpenerService,
|
||||
// @ITelemetryService telemetryService: ITelemetryService,
|
||||
// @IHoverService hoverService: IHoverService,
|
||||
// ) {
|
||||
// super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, telemetryService, hoverService);
|
||||
// }
|
||||
|
||||
}
|
||||
|
||||
// register a singleton service that mounts the ViewPane here
|
||||
@@ -328,6 +328,11 @@ export class DesktopMain extends Disposable {
|
||||
//
|
||||
// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
|
||||
// // Void
|
||||
// const sendLLMMessageService = new SendLLMMessageService();
|
||||
// serviceCollection.set(ISendLLMMessageService, sendLLMMessageService);
|
||||
|
||||
|
||||
|
||||
return { serviceCollection, logService, storageService, configurationService };
|
||||
}
|
||||
|
||||
@@ -13,6 +13,14 @@ import './browser/workbench.contribution.js';
|
||||
//#endregion
|
||||
|
||||
|
||||
|
||||
//#region --- Void
|
||||
// Void added this:
|
||||
import './contrib/void/browser/void.contribution.js';
|
||||
import '../platform/void/browser/llmMessageService.js';
|
||||
//#endregion
|
||||
|
||||
|
||||
//#region --- workbench actions
|
||||
|
||||
import './browser/actions/textInputActions.js';
|
||||
|
||||
@@ -31,6 +31,12 @@ import './electron-sandbox/parts/dialogs/dialog.contribution.js';
|
||||
|
||||
//#endregion
|
||||
|
||||
// //#region --- Void
|
||||
// // Void added this (modeling off of import '.*clipboardservice.js'):
|
||||
// import './services/void/electron-main/sendLLMMessage.js';
|
||||
// //#endregion
|
||||
|
||||
|
||||
|
||||
//#region --- workbench services
|
||||
|
||||
|
||||
@@ -32,6 +32,9 @@ import './browser/web.main.js';
|
||||
//#endregion
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
//#region --- workbench services
|
||||
|
||||
import './services/integrity/browser/integrityService.js';
|
||||
|
||||
Vendored
+3
-1
@@ -4,7 +4,6 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
declare module 'vscode' {
|
||||
|
||||
/**
|
||||
* The version of the editor.
|
||||
*/
|
||||
@@ -14074,6 +14073,9 @@ declare module 'vscode' {
|
||||
*/
|
||||
export namespace languages {
|
||||
|
||||
/** Void added this: */
|
||||
export function addInlineDiff(editor: TextEditor, originalText: string, modifiedRange: Range): void;
|
||||
|
||||
/**
|
||||
* Return the identifiers of all known languages.
|
||||
* @returns Promise resolving to an array of identifier strings.
|
||||
|
||||
@@ -77,6 +77,18 @@
|
||||
process.on(type, callback);
|
||||
}
|
||||
},
|
||||
|
||||
// Void : {
|
||||
// /**
|
||||
// * Send a message to the LLM.
|
||||
// * @param {any} data The data to send to the LLM.
|
||||
// * @returns {Promise<any>} The response from the LLM.
|
||||
// */
|
||||
// sendLLMMessage: async (data) => {
|
||||
// // Use ipcRenderer.invoke to send the message to the main process (see app.ts)
|
||||
// return await ipcRenderer.invoke('vscode:sendLLMMessage', data);
|
||||
// }
|
||||
// },
|
||||
};
|
||||
|
||||
if (process.contextIsolated) {
|
||||
|
||||
Reference in New Issue
Block a user