Compare commits
392 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a0fa104d23 | |||
| 7fd0b133e5 | |||
| 321876932a | |||
| eda8517c78 | |||
| 6ce37f7627 | |||
| 18b76885f5 | |||
| ba8644fbb6 | |||
| ee959ded08 | |||
| 499a47904e | |||
| 5ac03bb917 | |||
| 922f3ba2d7 | |||
| a189629341 | |||
| d2098f71eb | |||
| 77c59f2e80 | |||
| e6ecad3f65 | |||
| ee43ec99ea | |||
| e77039e9ad | |||
| 6aa1375067 | |||
| fe017948c6 | |||
| 9b3d4cbdba | |||
| a8cf0b361f | |||
| 680125baec | |||
| 0db4f7118d | |||
| 2ea20243a8 | |||
| 4d02381dab | |||
| 6990216fbe | |||
| f5d68d9348 | |||
| 165c5aac24 | |||
| 22f4a75c00 | |||
| 06513168c1 | |||
| a80883c10f | |||
| 20b0fd6af9 | |||
| 5171f7244f | |||
| 43708d25e7 | |||
| 2b54733242 | |||
| cd77d1a1c4 | |||
| 37af09add8 | |||
| ade1e70536 | |||
| 159e0b677f | |||
| 335ebe1e11 | |||
| 7be1ae5003 | |||
| 5d7f8fc91f | |||
| d6b3bdee35 | |||
| 8c1626f79b | |||
| 97ffe0b4cc | |||
| 9cf316f808 | |||
| aabacba2a9 | |||
| 39ccb1bbbd | |||
| 63560308b5 | |||
| 967aff2d19 | |||
| 50327a238e | |||
| 8eb5abe429 | |||
| 3c532478de | |||
| 5d5839736e | |||
| b8fd40e735 | |||
| a869ddb6f3 | |||
| e5a40e3d35 | |||
| 1081b8879f | |||
| 4efc5209ab | |||
| adcdeed094 | |||
| 86285e2393 | |||
| c1b6b2e01d | |||
| 2dcdd78316 | |||
| fa9a3db86f | |||
| 4445ff28ce | |||
| 9d6a4c4648 | |||
| e41f8790b5 | |||
| d363edb1a8 | |||
| 3cddc5d5cc | |||
| 2509290ab2 | |||
| aba49edfdd | |||
| 13b60f7ee3 | |||
| bd5cf464c2 | |||
| 44e7d10ece | |||
| 6bf79b27c0 | |||
| bb22aefb6a | |||
| 7002f320d6 | |||
| def8803919 | |||
| fdb21a68b8 | |||
| 455acc3490 | |||
| bc5b07b26f | |||
| 79ad57b112 | |||
| 9391dcb0b8 | |||
| b90716e38e | |||
| d8d21b0f25 | |||
| 3bf06bfe26 | |||
| 9166fa8684 | |||
| df2165403e | |||
| 44e0f0dfc7 | |||
| d6e496c396 | |||
| 7c94b95df4 | |||
| 85759eeb4a | |||
| 6230971dff | |||
| 8f08e40819 | |||
| ae635f0102 | |||
| cb8cc587f6 | |||
| 77f931c923 | |||
| 8dcb5d35df | |||
| 6483dcffa8 | |||
| ccaf6e2e3d | |||
| d5ae1a328c | |||
| 45c83ee661 | |||
| af7dbb4b70 | |||
| 72a6daaa58 | |||
| 898a5803dd | |||
| cffc6f6db9 | |||
| b2ba634cc4 | |||
| c62d6a6790 | |||
| f8bee1d4a2 | |||
| 41e66a73c4 | |||
| 5da0e4821f | |||
| 6aefdc3095 | |||
| 4055dc7775 | |||
| f48889627a | |||
| 3c59428fe8 | |||
| 8a8a982d21 | |||
| 7f890b946d | |||
| 1799ae9d84 | |||
| c7bdd2d64c | |||
| 5fde9194af | |||
| b6408512d3 | |||
| d5dc411e4c | |||
| e14fdd6678 | |||
| 8b5dfa66d5 | |||
| aef606dfb4 | |||
| 89b29ac97b | |||
| 908de32b8f | |||
| 4787400865 | |||
| 8b5e7dc25b | |||
| 7289f5f15a | |||
| 5e83d419ce | |||
| 3128000cf7 | |||
| b1626849ba | |||
| 99901d1e20 | |||
| 858faed92b | |||
| a4f94087df | |||
| 4236d920d9 | |||
| 56619c7b17 | |||
| ea65903b3f | |||
| 78671db5b8 | |||
| 7f38c1c9c3 | |||
| eefa9648c4 | |||
| 8c26fd2081 | |||
| 49fbb62259 | |||
| ad5a77dc55 | |||
| 2f078326c2 | |||
| 448be9a2cb | |||
| 0a1f675f45 | |||
| 052a50f9b0 | |||
| e331fb4eed | |||
| 4e197f4659 | |||
| 39bf2283cc | |||
| c87d1886cd | |||
| 6f693c4d0a | |||
| 1c5adb96d3 | |||
| 8f8fa8548d | |||
| bbe7575321 | |||
| dae5d054e7 | |||
| 0638cef79b | |||
| 8e2e8c58c9 | |||
| 4b4dde71b3 | |||
| d59b99a2a0 | |||
| df32898523 | |||
| 2bd8e61706 | |||
| 35724f36cd | |||
| 7fba51b59f | |||
| 599d80dcc3 | |||
| 3c186031be | |||
| 51c84df5a5 | |||
| 77c031abc5 | |||
| e81e71c81f | |||
| be07ecec2e | |||
| 3aa0bf11e6 | |||
| 329e68ccb1 | |||
| f3da41ba46 | |||
| 78384b3acf | |||
| 2912d5cd89 | |||
| c699577183 | |||
| 707fe9e0a3 | |||
| 71dd603c3d | |||
| 7aa5f1e4ec | |||
| 7c4e92a030 | |||
| 76125cc83e | |||
| 3fc0fd0bc0 | |||
| 5b0d8f4418 | |||
| d71f377ac1 | |||
| dbd63229c8 | |||
| 1e39cdaba4 | |||
| 5f76fa8d7d | |||
| f6358e319b | |||
| 5a52689d91 | |||
| 5187f0e96c | |||
| 086e471dd5 | |||
| 033de587f2 | |||
| bd0db41f67 | |||
| a5ac79c6a4 | |||
| 67a253164e | |||
| 3c93e0a414 | |||
| 1d9e0faaa3 | |||
| b440c8732f | |||
| 2aae01f3b6 | |||
| c1ffca04a7 | |||
| de703e82cb | |||
| a2a9cdba60 | |||
| 795ce62ce8 | |||
| bde51106a1 | |||
| 3ac9dcf0c0 | |||
| 2d9fe3b0be | |||
| 7e8af9c2ef | |||
| a94ce5d474 | |||
| eceaa6a59b | |||
| 103dd68b3c | |||
| 7decc8e146 | |||
| c58210ba80 | |||
| c68a497e77 | |||
| 03992c1206 | |||
| 7c0ba71314 | |||
| 878a439acd | |||
| f047ef9c55 | |||
| db6ebeb9f2 | |||
| 7631b41b27 | |||
| b2d8ca3023 | |||
| f11ff3475b | |||
| e74cc15d71 | |||
| 0294cb3b98 | |||
| 88dc32a43e | |||
| 42e1277bb9 | |||
| 2dcd95cf2a | |||
| 499abdf26c | |||
| 52e92ac3f9 | |||
| 885ebc8528 | |||
| 4f4ef694ff | |||
| 8cc68989dd | |||
| 57795b3210 | |||
| 68be4dcf57 | |||
| eb4d2a6c7b | |||
| 0c30c2681d | |||
| 36277eaed8 | |||
| ba4cac3fb6 | |||
| 3ce2911e3d | |||
| 89dc3c8805 | |||
| 1bbd6f59fe | |||
| bf6cb764ec | |||
| 7fdde7af3e | |||
| a2a265423f | |||
| 5f39e314b6 | |||
| b7e87b4af8 | |||
| b34b2f3d7b | |||
| 5b6bf42576 | |||
| a72ca7868e | |||
| a0f43c90c4 | |||
| 41b85052f0 | |||
| 4120031e0d | |||
| a499fa19fb | |||
| 6867dabf09 | |||
| 93875818f5 | |||
| d09223c362 | |||
| fb5ae96a44 | |||
| dbe62e82e1 | |||
| e5c72ac13e | |||
| dc143e7cb5 | |||
| 80443892f3 | |||
| 70e89c8f20 | |||
| 4f183f6772 | |||
| f61a7c91e4 | |||
| 610009eb1c | |||
| cd1b783917 | |||
| 57e8ef5463 | |||
| d5581b1bb4 | |||
| 1482ebba64 | |||
| 6d4253dff9 | |||
| cc4c863c82 | |||
| 4618e81d20 | |||
| 7fa971cbcd | |||
| 2267757c41 | |||
| e07727393b | |||
| 6a73930f4b | |||
| 6c382be737 | |||
| c43a234788 | |||
| 4e56321eb9 | |||
| 9e177c73ca | |||
| ccbe1911d2 | |||
| 015a7c10dc | |||
| f0c8eb55a6 | |||
| 24e5b78583 | |||
| ed89858fbc | |||
| a6d5a5f325 | |||
| e566db8396 | |||
| b52cf80bd5 | |||
| f1a31246b2 | |||
| e33b3ceae1 | |||
| c4e78cd3ba | |||
| fa1e74acf2 | |||
| 112a770f2c | |||
| b1bcac5a6d | |||
| ae79229c00 | |||
| 28ac708904 | |||
| 161b446bb2 | |||
| e0aee9863e | |||
| 072484dd5d | |||
| a86d3dc82b | |||
| b141b2edfe | |||
| cf9482bd76 | |||
| 4be2c3b6fd | |||
| 69a75f5c47 | |||
| 0ab4f8513b | |||
| 93f8597dd4 | |||
| 0f0e56a263 | |||
| 91a23bb2d3 | |||
| 19503d92b4 | |||
| e2648bba3e | |||
| 090eaba3aa | |||
| 1f98e75a4f | |||
| a38b4c8ee0 | |||
| 1def205b28 | |||
| 32bee63b50 | |||
| 4798bbdc86 | |||
| a854a341d3 | |||
| 1a4623589c | |||
| 51b19a6831 | |||
| b58ad5f876 | |||
| 4951116a6c | |||
| 46ca49e02b | |||
| 9133d6ecf5 | |||
| ad736edd9e | |||
| 1c7494861d | |||
| 31163e8516 | |||
| 853a26ae8e | |||
| 16c7aaf599 | |||
| d6df2514e1 | |||
| 2b33294a85 | |||
| 3ecdae6ab6 | |||
| fd1c840618 | |||
| 71f8315e06 | |||
| 633f7ffee5 | |||
| 35f53e66dd | |||
| 016d59e81a | |||
| 82851d519d | |||
| cfc18827bb | |||
| 04e8a204a3 | |||
| 4cc7f292b0 | |||
| 7c991b4dfc | |||
| 84d17737cf | |||
| 4c6b32633b | |||
| b933c5cf45 | |||
| 5b76b4a6a3 | |||
| 251d175517 | |||
| 69ce84c8df | |||
| f9e9771a87 | |||
| 18ff819ae9 | |||
| 98c09c653a | |||
| e1d4d85846 | |||
| e52e55a431 | |||
| 32d465dbe8 | |||
| 1f9bc92b66 | |||
| 9e5c82d6f8 | |||
| dd429208ce | |||
| 82278ab115 | |||
| 13cb8d4de7 | |||
| 2bc004f54d | |||
| e58b6041b4 | |||
| 6fd514230c | |||
| f75b4f34d8 | |||
| 6ecf7be826 | |||
| f7d4eebf56 | |||
| 8e735ec453 | |||
| 850bca80ab | |||
| 440dd809e3 | |||
| ffb416e4e8 | |||
| 0a17a9de19 | |||
| a0a7900381 | |||
| 5f77b9faae | |||
| 3fc97c1a83 | |||
| a3a8a7a842 | |||
| e28449b83d | |||
| 6ecc5cdbad | |||
| d64b6fba31 | |||
| d7ac86a6bd | |||
| 598f0d47f8 | |||
| 3b1ccdd2b1 | |||
| 054a8ad85a | |||
| 31ae3ea46e | |||
| 67d453cffe | |||
| ab557d4707 | |||
| e5a21bf7c6 | |||
| d5ef3b7d9b | |||
| f3753c8da0 | |||
| 9fce7ce812 | |||
| 85009ab5b3 | |||
| fe01285d58 | |||
| d1fa4d3439 | |||
| d0921f899c |
@@ -1,5 +1,5 @@
|
||||
disturl="https://electronjs.org/headers"
|
||||
target="34.2.0"
|
||||
target="34.3.2"
|
||||
ms_build_id="11044223"
|
||||
runtime="electron"
|
||||
build_from_source="true"
|
||||
|
||||
@@ -12,11 +12,9 @@ There are a few ways to contribute:
|
||||
|
||||
### Codebase Guide
|
||||
|
||||
We [highly recommend reading this](https://github.com/microsoft/vscode/wiki/Source-Code-Organization) article on VSCode's sourcecode organization too. Void's codebase is pretty simple when you know what a service is and what `browser/` and `common/` mean, and the article covers all the jargon.
|
||||
We [highly recommend reading this](https://github.com/voideditor/void/blob/main/VOID_CODEBASE_GUIDE.md) guide that we put together on Void's sourcecode if you'd like to contribute!
|
||||
|
||||
<!-- ADD BLOG HERE
|
||||
We wrote a [guide to working in VSCode].
|
||||
-->
|
||||
The repo is not as intimidating as it first seems if you read the guide!
|
||||
|
||||
Most of Void's code lives in the folder `src/vs/workbench/contrib/void/`.
|
||||
|
||||
@@ -90,7 +88,7 @@ Alternatively, if you want to build Void from the terminal, instead of pressing
|
||||
- If you get `"TypeError: Failed to fetch dynamically imported module"`, make sure all imports end with `.js`.
|
||||
- If you see missing styles, wait a few seconds and then reload.
|
||||
- If you have any questions, feel free to [submit an issue](https://github.com/voideditor/void/issues/new). You can also refer to VSCode's complete [How to Contribute](https://github.com/microsoft/vscode/wiki/How-to-Contribute) page.
|
||||
|
||||
- If you get errors like `npm error libtool: error: unrecognised option: '-static'`, make sure you have GNU libtool instead of BSD libtool (BSD is the default in macos)
|
||||
|
||||
|
||||
## Packaging
|
||||
@@ -104,12 +102,11 @@ We don't usually recommend packaging. Instead, you should probably just build. I
|
||||
|
||||
### Windows
|
||||
- `npm run gulp vscode-win32-x64` - most common
|
||||
- `npm run gulp vscode-win32-ia32`
|
||||
- `npm run gulp vscode-win32-arm64`
|
||||
|
||||
### Linux
|
||||
- `npm run gulp vscode-linux-x64` - most common
|
||||
- `npm run gulp vscode-linux-arm`
|
||||
- `npm run gulp vscode-linux-ia32`
|
||||
- `npm run gulp vscode-linux-arm64`
|
||||
|
||||
|
||||
### Output
|
||||
@@ -15,25 +15,26 @@ This repo contains the full sourcecode for Void. We are currently in [open beta]
|
||||
|
||||
- 👋 [Discord](https://discord.gg/RSNjgaugJs)
|
||||
|
||||
- 🔨 [Contribute](https://github.com/voideditor/void/blob/main/CONTRIBUTING.md)
|
||||
- 🔨 [Contribute](https://github.com/voideditor/void/blob/main/HOW_TO_CONTRIBUTE.md)
|
||||
|
||||
- 🚙 [Roadmap](https://github.com/orgs/voideditor/projects/2)
|
||||
|
||||
- 📝 [Changelog](https://voideditor.com/changelog)
|
||||
|
||||
- 🧭 [Codebase Guide](https://github.com/voideditor/void/blob/main/VOID_CODEBASE_GUIDE.md)
|
||||
|
||||
## Contributing
|
||||
|
||||
1. Feel free to attend a weekly meeting in our Discord channel if you'd like to contribute!
|
||||
|
||||
2. To get started working on Void, see [Contributing](https://github.com/voideditor/void/blob/main/CONTRIBUTING.md).
|
||||
2. To get started working on Void, see [here](https://github.com/voideditor/void/blob/main/HOW_TO_CONTRIBUTE.md).
|
||||
|
||||
3. We're open to collaborations and suggestions of all types - just reach out.
|
||||
|
||||
|
||||
## Reference
|
||||
|
||||
Void is a fork of the [vscode](https://github.com/microsoft/vscode) repository. For some useful links on VSCode, see [`VOID_USEFUL_LINKS.md`](https://github.com/voideditor/void/blob/main/VOID_USEFUL_LINKS.md).
|
||||
Void is a fork of the [vscode](https://github.com/microsoft/vscode) repository. For a guide to the VSCode/Void codebase, see [our Codebase Guide](https://github.com/voideditor/void/blob/main/VOID_CODEBASE_GUIDE.md).
|
||||
|
||||
## Support
|
||||
Feel free to reach out in our Discord or contact us via email: hello@voideditor.com.
|
||||
|
||||
@@ -0,0 +1,162 @@
|
||||
# Void Codebase Guide
|
||||
|
||||
The Void codebase is not as intimidating as it seems!
|
||||
|
||||
Most of Void's code lives in the folder `src/vs/workbench/contrib/void/`.
|
||||
|
||||
The purpose of this document is to explain how Void's codebase works. If you want build instructions, see [Contributing](https://github.com/voideditor/void/blob/main/HOW_TO_CONTRIBUTE.md).
|
||||
|
||||
|
||||
|
||||
## Void Codebase Guide
|
||||
|
||||
### Terminology
|
||||
|
||||
Here is some important terminology you should know if you're working inside VSCode:
|
||||
- An **Editor** is the thing that you type your code in. If you have 10 tabs open, that's just one editor! Editors contain tabs (or "models").
|
||||
- A **Model** is an internal representation of a file's contents. It's shared between editors (for example, if you press `Cmd+\` to make a new editor, then the model of a file like `A.ts` is shared between them. Two editors, one model. That's how changes sync.).
|
||||
- Each model has a **URI** it represents, like `/Users/.../my_file.txt`. (A URI or "resource" is generally just a path).
|
||||
- The **Workbench** is the wrapper that contains all the editors, the terminal, the file system tree, etc.
|
||||
- Usually you use the `ITextModel` type for models and the `ICodeEditor` type for editors. There aren't that many other types.
|
||||
|
||||
|
||||
|
||||
### Minimal VSCode Rundown
|
||||
Here's a minimal VSCode rundown if you're just getting started with Void:
|
||||
|
||||
- VSCode is (and therefore Void is) an Electron app. Electron runs two processes: a **main** process (for internals) and a **browser** process (browser means HTML in general, not just "web browser").
|
||||
- Code in a `browser/` folder always lives on the browser process, and it can use `window` and other browser items.
|
||||
- Code in an `electron-main/` folder always lives on the main process, and it can import `node_modules`.
|
||||
- Code in `common/` can be used by either process, but doesn't get any special imports.
|
||||
- The browser environment is not allowed to import `node_modules`, but there are two workarounds:
|
||||
1. Bundle the raw node_module code to the browser - we're doing this for React.
|
||||
2. Implement the code on `electron-main/` and set up a channel between main/browser - we're doing this for sendLLMMessage.
|
||||
|
||||
|
||||
|
||||
VSCode is organized into "Services". A service is just a class that mounts a single time (in computer science theory this is called a "singleton"). You can register services with `registerSingleton` so that you can easily use them in any constructor with `@<Service>`. See _dummyContrib for an example we put together on how to register them. The registration is the same every time.
|
||||
|
||||
Services are always lazily created, even if you register them as Eager. If you want something that always runs on Void's mount, you should use a "workbench contribution". See _dummyContrib for this. Very similar to a Service, just registered slightly differently.
|
||||
|
||||
Actions or "commands" are functions you register on VSCode so that either you or the user can call them later. You can run actions as a user by pressing Cmd+Shift+P (opens the command pallete), or you can run them internally by using the commandService to call them by ID. We use actions to register keybinding listeners like Cmd+L, Cmd+K, etc. The nice thing about actions is the user can change the keybindings.
|
||||
|
||||
|
||||
See [here](https://github.com/microsoft/vscode/wiki/Source-Code-Organization) for a decent VSCode guide with even more info.
|
||||
|
||||
|
||||
Each section below contains an overview of a core part of Void's sourcecode. You might want to scroll to find the item that's relevant to you.
|
||||
|
||||
### Internal LLM Message Pipeline
|
||||
|
||||
Here's a picture of all the dependencies that are relevent between the time you first send a message through Void's sidebar, and the time a request is sent to your provider.
|
||||
Sending LLM messages from the main process avoids CSP issues with local providers and lets us use node_modules more easily.
|
||||
|
||||
|
||||
<div align="center">
|
||||
<img width="100%" src="https://github.com/user-attachments/assets/9cf54dbb-82c4-4488-97a2-bd8dea890b50">
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
**Notes:** `modelCapabilities` is an important file that must be updated when new models come out!
|
||||
|
||||
|
||||
### Apply
|
||||
|
||||
Void has two types of Apply: **Fast Apply** (uses Search/Replace, see below), and **Slow Apply** (rewrites whole file).
|
||||
|
||||
When you click Apply and Fast Apply is enabled, we prompt the LLM to output Search/Replace block(s) like this:
|
||||
```
|
||||
<<<<<<< ORIGINAL
|
||||
// original code goes here
|
||||
=======
|
||||
// replaced code goes here
|
||||
>>>>>>> UPDATED
|
||||
```
|
||||
This is what allows Void to quickly apply code even on 1000-line files. It's the same as asking the LLM to press Ctrl+F and enter in a search/replace query.
|
||||
|
||||
### Apply Inner Workings
|
||||
|
||||
The `editCodeService` file runs Apply. The same exact code is also used when the LLM calls the Edit tool, and when you submit Cmd+K. Just different versions of Fast/Slow Apply mode.
|
||||
|
||||
Here is some important terminology:
|
||||
- A **DiffZone** is a {startLine, endLine} region in which we show Diffs (red/green areas). We update it when the user types, so it's always accurate.
|
||||
- A **DiffArea** is a generalization that tracks line numbers like a DiffZone.
|
||||
- The only type of zone that can "stream" is a DiffZone. Each DiffZone has an llmCancelToken if it's streaming.
|
||||
- When you click Apply, we create a **DiffZone** over that the full file so that any changes that the LLM makes will show up in red/green. We then stream the change.
|
||||
- When an LLM calls Edit, it's really calling Apply.
|
||||
- When you submit Cmd+K, it's the same as Apply except we create a smaller DiffZone (not on the whole file).
|
||||
|
||||
### Writing Files Inner Workings
|
||||
When Void wants to change your code, it just writes to a text model. This means all you need to know to write to a file is its URI - you don't have to load it, save it, etc. There are some annoying background URI/model things to think about to get this to work, but we handled them all in `voidModelService`.
|
||||
|
||||
### Void Settings Inner Workings
|
||||
We have a service `voidSettingsService` that stores all your Void settings (providers, models, global Void settings, etc). Imagine this as an implicit dependency for any of the core Void services:
|
||||
|
||||
<div align="center">
|
||||
<img width="800" src="https://github.com/user-attachments/assets/9f3cb68c-a61b-4810-8429-bb90b992b3fa">
|
||||
</div>
|
||||
|
||||
Here's a guide to some of the terminology we're using:
|
||||
- **FeatureName**: Autocomplete | Chat | CtrlK | Apply
|
||||
- **ModelSelection**: a {providerName, modelName} pair.
|
||||
- **ProviderName**: The name of a provider: `'ollama'`, `'openAI'`, etc.
|
||||
- **ModelName**: The name of a model (string type, eg `'gpt-4o'`).
|
||||
- **RefreshProvider**: a provider that we ping repeatedly to update the models list.
|
||||
- **ChatMode** = normal | gather | agent
|
||||
|
||||
|
||||
|
||||
### Approval State
|
||||
`editCodeService`'s data structures contain all the information about changes that the user needs to review. However, they don't store that information in a useful format. We wrote the following service to get a more useful derived state:
|
||||
|
||||
<div align="center">
|
||||
<img width="600" src="https://github.com/user-attachments/assets/f3645355-dff6-467c-bc38-ffe52077c08b">
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
### Build process
|
||||
If you want to know how our build pipeline works, see our build repo [here](https://github.com/voideditor/void-builder).
|
||||
|
||||
|
||||
|
||||
## VSCode Codebase Guide (Not Void)
|
||||
|
||||
The Void team put together this list of links to get up and running with VSCode's sourcecode, the foundation of Void. We hope it's helpful!
|
||||
|
||||
#### Links for Beginners
|
||||
|
||||
- [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.
|
||||
|
||||
#### Links for Contributors
|
||||
|
||||
- [How VSCode's sourcecode is organized](https://github.com/microsoft/vscode/wiki/Source-Code-Organization) - this explains where the entry point files are, what `browser/` and `common/` mean, etc. This is the most important read on this whole list! We recommend reading the whole thing.
|
||||
|
||||
- [Built-in VSCode styles](https://code.visualstudio.com/api/references/theme-color) - CSS variables that are built into VSCode. Use `var(--vscode-{theme but replacing . with -})`. You can also see their [Webview theming guide](https://code.visualstudio.com/api/extension-guides/webview#theming-webview-content).
|
||||
|
||||
|
||||
#### Misc
|
||||
|
||||
- [Every command](https://code.visualstudio.com/api/references/commands) built-in to VSCode - not used often, but here for reference.
|
||||
|
||||
- Note: VSCode's repo is the source code for the Monaco editor! An "editor" is a Monaco editor, and it shares the code for ITextModel, etc.
|
||||
|
||||
|
||||
#### VSCode's Extension API
|
||||
|
||||
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).
|
||||
|
||||
- [An extension's `package.json` schema](https://code.visualstudio.com/api/references/extension-manifest).
|
||||
|
||||
- ["Contributes" Guide](https://code.visualstudio.com/api/references/contribution-points) - the `"contributes"` part of `package.json` is how an extension mounts.
|
||||
|
||||
- [The Full VSCode Extension API](https://code.visualstudio.com/api/references/vscode-api) - look on the right side for organization. The [bottom](https://code.visualstudio.com/api/references/vscode-api#api-patterns) of the page is easy to miss but is useful - cancellation tokens, events, disposables.
|
||||
|
||||
- [Activation events](https://code.visualstudio.com/api/references/activation-events) you can define in `package.json` (not the most useful).
|
||||
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
# Useful links
|
||||
|
||||
The Void team put together this list of links to get up and running with VSCode's sourcecode. We hope it's helpful!
|
||||
|
||||
For a complete guide on building Void, see [Contributing](https://github.com/voideditor/void/blob/main/CONTRIBUTING.md).
|
||||
|
||||
## Contributing
|
||||
|
||||
- [How VSCode's sourcecode is organized](https://github.com/microsoft/vscode/wiki/Source-Code-Organization) - this explains where the entry point files are, what `browser/` and `common/` mean, etc. This is the most important read on this whole list! We recommend reading the whole thing.
|
||||
|
||||
- [Built-in VSCode styles](https://code.visualstudio.com/api/references/theme-color) - CSS variables that are built into VSCode. Use `var(--vscode-{theme but replacing . with -})`. You can also see their [Webview theming guide](https://code.visualstudio.com/api/extension-guides/webview#theming-webview-content).
|
||||
|
||||
## Beginners / Getting started
|
||||
|
||||
- [VSCode UI guide](https://code.visualstudio.com/docs/getstarted/userinterface) - covers auxbar, panels, etc.
|
||||
|
||||
- [UX guide](https://code.visualstudio.com/api/ux-guidelines/overview) - covers Containers, Views, Items, etc.
|
||||
|
||||
|
||||
## Misc
|
||||
|
||||
- [Every command](https://code.visualstudio.com/api/references/commands) built-in to VSCode - not used often, but here for reference.
|
||||
|
||||
|
||||
## VSCode's Extension API
|
||||
|
||||
Void is 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).
|
||||
|
||||
- [An extension's `package.json` schema](https://code.visualstudio.com/api/references/extension-manifest).
|
||||
|
||||
- ["Contributes" Guide](https://code.visualstudio.com/api/references/contribution-points) - the `"contributes"` part of `package.json` is how an extension mounts.
|
||||
|
||||
- [The Full VSCode Extension API](https://code.visualstudio.com/api/references/vscode-api) - look on the right side for organization. The [bottom](https://code.visualstudio.com/api/references/vscode-api#api-patterns) of the page is easy to miss but is useful - cancellation tokens, events, disposables.
|
||||
|
||||
- [Activation events](https://code.visualstudio.com/api/references/activation-events) you can define in `package.json` (not the most useful).
|
||||
|
||||
|
||||
@@ -1,75 +1,75 @@
|
||||
c0a187acca68906c4a6387e8fabd052cb031ace6132d60a71001d9a0e891958e *chromedriver-v34.2.0-darwin-arm64.zip
|
||||
fa5a46d752267d8497d375e19079e8b6a8df70c234a79b2d6b48f5862e1a0abc *chromedriver-v34.2.0-darwin-x64.zip
|
||||
61e03d4fa570976d80f740637f56192b6448a05a73d1fba9717900b29f2b1b4d *chromedriver-v34.2.0-linux-arm64.zip
|
||||
ec774d9b1a1b828a0db1502a1017fcab1dfed99b1b6b2fd2308dd600a1efa98a *chromedriver-v34.2.0-linux-armv7l.zip
|
||||
cc15a6e6206485a2d96649ceb60509b9da04fa2811c4824b2e0eb43d1f4b1417 *chromedriver-v34.2.0-linux-x64.zip
|
||||
9777122f6684180ef375b9b21dcabbc731d8a8befa300d1d47ad954a5b64c1c8 *chromedriver-v34.2.0-mas-arm64.zip
|
||||
69451fa148b105fec9644646b22ca758a206499574c5816591354835c8056679 *chromedriver-v34.2.0-mas-x64.zip
|
||||
eb7adc7e720f5e0f1d2c12ecbe886bdc01f2c9aaa3954bd6ebd313750bb18819 *chromedriver-v34.2.0-win32-arm64.zip
|
||||
cb1973b0c2f5565974d5c2cb51816692f064b6cdc7897fa341d528ba7f9b14bf *chromedriver-v34.2.0-win32-ia32.zip
|
||||
af5575b4727c3dbe7272016cbbaa8872043f843168a47d86748a50397efb4f77 *chromedriver-v34.2.0-win32-x64.zip
|
||||
fcc718af2a28fb953290dc971e945818b4dbb293f297e6e25acb669d450cc0dd *electron-api.json
|
||||
fa47e752e559a6472b87d8907f63296ed8cd53ecf862a4ae113018474d40aa8e *electron-v34.2.0-darwin-arm64-dsym-snapshot.zip
|
||||
336c3374e721e2379901141be6345459f78d243b037c65b55bac4ae8cb14bfbe *electron-v34.2.0-darwin-arm64-dsym.zip
|
||||
ac3b9d712d9f036f066d8eba42797117a513e2d250fcc117f0354300b7d5c1e5 *electron-v34.2.0-darwin-arm64-symbols.zip
|
||||
ee447c17b2ac545e48083113d7e39a916821c1316f60f42cbcbee4fffe7c022a *electron-v34.2.0-darwin-arm64.zip
|
||||
d7510bc038d06b26690ac9a78fbb256626502303ff7f5b1c2242f64a02832b1b *electron-v34.2.0-darwin-x64-dsym-snapshot.zip
|
||||
0e95c2bbda00afe78e6229b824e3ffe0cc8612956dc11a5a30380224acdbecae *electron-v34.2.0-darwin-x64-dsym.zip
|
||||
6d734ef8e8fd007071aae9a13534894dd801f11900f3e73e49cf5352b426e575 *electron-v34.2.0-darwin-x64-symbols.zip
|
||||
8ef741819c8a5370dabc3b9df5e6ac217366477c5d5c656ed23c800bc984d887 *electron-v34.2.0-darwin-x64.zip
|
||||
c2f448882a0392ebfd9810058a6a9580b087002c74fca3dcd3cf4ba5c3b27a7c *electron-v34.2.0-linux-arm64-debug.zip
|
||||
51e887c382593021127593ceba89ad662d3a6de0f748b94fe3a0ce78a7393923 *electron-v34.2.0-linux-arm64-symbols.zip
|
||||
818c91309da8ff948c43df58a996c05c0d27daa690e1d659355d9db01e351919 *electron-v34.2.0-linux-arm64.zip
|
||||
c2f448882a0392ebfd9810058a6a9580b087002c74fca3dcd3cf4ba5c3b27a7c *electron-v34.2.0-linux-armv7l-debug.zip
|
||||
71ec2b7473db766194bcf04648229c4affedce06e150e692745cc72dbc3749c5 *electron-v34.2.0-linux-armv7l-symbols.zip
|
||||
0c75996c6bf2d37d0441d3e1021386a9348f8d21783d75571cdb10ede606424f *electron-v34.2.0-linux-armv7l.zip
|
||||
d1f17be8df8ec6dc1a23afd8a7752f0b949c755a493b8a2c1e83f76c629258ca *electron-v34.2.0-linux-x64-debug.zip
|
||||
d8fa79154b0b663dbd0054d69f53aec326997449ef21943d27225a2d6899660f *electron-v34.2.0-linux-x64-symbols.zip
|
||||
f12a02d86cc657500978d263ec6d1841b6e2085cd3bd4d30a97cfe14685396f3 *electron-v34.2.0-linux-x64.zip
|
||||
71ef8bfebb8513a405fd2beb44ad18f802bbac2248f81a5328ddaaa12906d70c *electron-v34.2.0-mas-arm64-dsym-snapshot.zip
|
||||
437aca6cad3158f15fd852e5913462637052940f812b20ccc10fccc133d5a0d6 *electron-v34.2.0-mas-arm64-dsym.zip
|
||||
7f253375a7b43d34b770c03153e47185be7a64bc0c10a783993a93144eb35492 *electron-v34.2.0-mas-arm64-symbols.zip
|
||||
1f601c20430b036b485c7dc02a39a297f0d08905462a4568306c12f299375e2f *electron-v34.2.0-mas-arm64.zip
|
||||
d32664181804a17f825bf1fbfd8f0cbe01e0b41b284937359e6d9754b3481ed8 *electron-v34.2.0-mas-x64-dsym-snapshot.zip
|
||||
f6b394b89fb77dfeefdf525739fe8cd9695f8c2ac2251ed7c571a376cde059e9 *electron-v34.2.0-mas-x64-dsym.zip
|
||||
539328d93e9bc122e6e34faeab6a42f4845c523f796c7a11bbdcfe5e856f6b98 *electron-v34.2.0-mas-x64-symbols.zip
|
||||
749b6ced7a9d35a719ad98d4c3bf1a08c315c19ce97d2497235d4ac302cda665 *electron-v34.2.0-mas-x64.zip
|
||||
e843ea4cb7a93686728d056c957c124760886167932ff619b518e45917edf38a *electron-v34.2.0-win32-arm64-pdb.zip
|
||||
4908423be5f8ad1b5dd737edfd69ee0870460e16bb639bb963b0981bda2628e4 *electron-v34.2.0-win32-arm64-symbols.zip
|
||||
9d13b2bd61416eec28f43faa399cc5c0dc9e36dec226314bbf397828f55d74de *electron-v34.2.0-win32-arm64-toolchain-profile.zip
|
||||
1629cec7b5620e6ca3b5305c393ae147d1a3871a8f164d686b7bee3810fc1109 *electron-v34.2.0-win32-arm64.zip
|
||||
e7182f1ef5ed13187fe12f35133cefb50c59e1d52ada758cbb72813dda575205 *electron-v34.2.0-win32-ia32-pdb.zip
|
||||
a10f778f62bf060a7e38f5ea75ea472679b9ef4f12767ee0703e6d77effae78a *electron-v34.2.0-win32-ia32-symbols.zip
|
||||
9d13b2bd61416eec28f43faa399cc5c0dc9e36dec226314bbf397828f55d74de *electron-v34.2.0-win32-ia32-toolchain-profile.zip
|
||||
96396712a0240f04471f71246b3885a513b08e3f2d40154272d34e59419db7e6 *electron-v34.2.0-win32-ia32.zip
|
||||
0c55ef5b1a6ea4604e3f0e9f8b5e946944abd1cfc3b974432d40e924a9996812 *electron-v34.2.0-win32-x64-pdb.zip
|
||||
d6b67cf12edabcc62ae21e7c90ac6b1161dbefe4e6b765c69fc7040096b7d026 *electron-v34.2.0-win32-x64-symbols.zip
|
||||
9d13b2bd61416eec28f43faa399cc5c0dc9e36dec226314bbf397828f55d74de *electron-v34.2.0-win32-x64-toolchain-profile.zip
|
||||
a4fdf617dca787b7f1c6f8d0d1cb69c8adb37ee23f9553fe69803f9cad713360 *electron-v34.2.0-win32-x64.zip
|
||||
5ca44a4ee9cc74a56c9556ce3f3774986dbb8b4f4953088c7f4577bfba4356a8 *electron.d.ts
|
||||
86247a6815cc98f321374edcbfc0ab7ee79bcb13a5c25213f42151e4059ec690 *ffmpeg-v34.2.0-darwin-arm64.zip
|
||||
e94f4707a91194f97d0451f9d5a27982c873a5c13d83d20926ce6fdb613f536c *ffmpeg-v34.2.0-darwin-x64.zip
|
||||
947d7b7cb035eab94cc15b602dfa8a925cf238c1d9225c01732fe0c41f59c571 *ffmpeg-v34.2.0-linux-arm64.zip
|
||||
64d74b6b94ac4fd755b97e0ec8d50af9cd73810fdb52f6c081b13a3337ace0ea *ffmpeg-v34.2.0-linux-armv7l.zip
|
||||
a62cb20c5e5f2ba6f1df6f1bc406cc30f7ed44fe6380b506afa333d1b4ad7a87 *ffmpeg-v34.2.0-linux-x64.zip
|
||||
715bbb3d193b1ca57d4b329edd05cd6b125dc188cdb73d1c66f220f62c8562e3 *ffmpeg-v34.2.0-mas-arm64.zip
|
||||
866bf47106e8c3e50f046783eb8f5c756069c6de962a46f4edc7a5ee7e4593ee *ffmpeg-v34.2.0-mas-x64.zip
|
||||
71a2a6c4cca15ccbcbf8912f5d73f855b0ca79804f941f68527ae808f8163957 *ffmpeg-v34.2.0-win32-arm64.zip
|
||||
399f841cca166781078ca42c8ea060e3d5850ec6cb79716186d0806f3ce20842 *ffmpeg-v34.2.0-win32-ia32.zip
|
||||
afad2a44f20a0c0c01fb1fea637f3f821842399593c9961c74d650ca12d32cbb *ffmpeg-v34.2.0-win32-x64.zip
|
||||
c95fdc9dba05aa68aeccb69d4c34f0cb1fb98d7f5291d974d0b638488693655f *hunspell_dictionaries.zip
|
||||
0abe74138afdb6e45a085d77407659f13c75ab96f694313d4e98bd662f9c6df2 *libcxx-objects-v34.2.0-linux-arm64.zip
|
||||
3d0cbf6fb150b006428eab78235856d2204d5e93ca85f162e429b4c8bd9b0d3b *libcxx-objects-v34.2.0-linux-armv7l.zip
|
||||
3a5491e32cec825499919be1b8bf0550d28fe5a31ff00a95572d49a58bb4820a *libcxx-objects-v34.2.0-linux-x64.zip
|
||||
ff8753d52f759041b8e5462125ee2b96798fe8b5047f8fb8ae60cd07af2fb13d *libcxx_headers.zip
|
||||
c98cce0091681bc367a48f66c5f4602961aa9cb6dd1a995d8969d6b39ce732f3 *libcxxabi_headers.zip
|
||||
78a9606190fb227460ddcd276ad540873595d6a82fd1007f42900f53347e3eb1 *mksnapshot-v34.2.0-darwin-arm64.zip
|
||||
6d4587a36f509356a908b6752de527cfe8a294a2d435b82ea3a98288c3a3a178 *mksnapshot-v34.2.0-darwin-x64.zip
|
||||
17737bd34f7feefc5143b745f2c4f0e5a41678780e144eb43de41f8d4b9d7852 *mksnapshot-v34.2.0-linux-arm64-x64.zip
|
||||
565aa9a84d913b7b5eb8d3b868cff151cb8a6c16548233ffd92b2f9bf3789227 *mksnapshot-v34.2.0-linux-armv7l-x64.zip
|
||||
7332d7864ab4e5ee7fa8d00580d9357f30d0e69d3d1434d67c61f79f666314a4 *mksnapshot-v34.2.0-linux-x64.zip
|
||||
cc81bfc9894378a9fc8a777429e04cc80860663b3dff8eba1f8cc72559a4f23d *mksnapshot-v34.2.0-mas-arm64.zip
|
||||
c98230088698638159f6e7e0c5ddde3cb4dba9fa81d76e3c58fc86f96b63ef81 *mksnapshot-v34.2.0-mas-x64.zip
|
||||
cc7b42943d998e1083ad8119dc2201dd27d709c15aa2a9b78f409485d2927592 *mksnapshot-v34.2.0-win32-arm64-x64.zip
|
||||
ade9e3126f113318e226f9bdeba068b4383b30a98517a361386eff448459effa *mksnapshot-v34.2.0-win32-ia32.zip
|
||||
344292ea318dc0e21f37bc7c82d57a57449f0fb278d9868f1b1b597bdcb1f36f *mksnapshot-v34.2.0-win32-x64.zip
|
||||
c9b82c9f381742e839fea00aeb14f24519bcaf38a0f4eed25532191701f9535b *chromedriver-v34.3.2-darwin-arm64.zip
|
||||
d556c1e2b06f1bf131e83c2fb981de755c28e1083a884d257eb964815be16b0c *chromedriver-v34.3.2-darwin-x64.zip
|
||||
1cabad4f3303ac2ff172a9f22185f64944dbaa6fc68271609077158eaefdee35 *chromedriver-v34.3.2-linux-arm64.zip
|
||||
4213ce52c72ef414179b5c5c22ae8423847ff030d438296bd6c2aac763930a7b *chromedriver-v34.3.2-linux-armv7l.zip
|
||||
3c64c08221fdfc0f4be60ea8b1b126f2ecca45f60001b63778522f711022c6ea *chromedriver-v34.3.2-linux-x64.zip
|
||||
e8388734d88e011cb6cd79795431de9206820749219d80565ee49d90501d2bf3 *chromedriver-v34.3.2-mas-arm64.zip
|
||||
3ad1dd37bd6e0bb37e8503898db7aedd56bd5213e6d6760b05c3d11f4625062b *chromedriver-v34.3.2-mas-x64.zip
|
||||
d567b481a0f5d88e84bba7718f89fb08f56363bfc4cb5914e1c2086358a5c252 *chromedriver-v34.3.2-win32-arm64.zip
|
||||
df6732e9dc61cb20a3c0b2a2de453aac7e2bd54e7cbff43512afa614852c15fa *chromedriver-v34.3.2-win32-ia32.zip
|
||||
dda0765c8d064924632e18cd152014ecd767f3808fc51c8249a053bfb7ca70a2 *chromedriver-v34.3.2-win32-x64.zip
|
||||
1945f15caff98f2e0f1ee539c483d352fb8d4d0c13f342caa7abe247676d828c *electron-api.json
|
||||
c078bbf727b3c3026f60e07a0f4643b85c06c581b54be017d0a6c284ba6772d3 *electron-v34.3.2-darwin-arm64-dsym-snapshot.zip
|
||||
35f587754d6a3272606258386bf73688d63dd53c7e572d3a7cbaae6f3f60bdae *electron-v34.3.2-darwin-arm64-dsym.zip
|
||||
08b14ee02c98353de3c738120dfd017322666e82b914a7f6de9b9888dcc5c0f0 *electron-v34.3.2-darwin-arm64-symbols.zip
|
||||
2a4aa7e8fa30f229e465ebd18d3e4722e2b41529dc51a68a954d333a7e556ffe *electron-v34.3.2-darwin-arm64.zip
|
||||
1509ccdeb80024f5e3edd5ecf804b4cef4e47ea2bd74e33ef0b39044b0ccf892 *electron-v34.3.2-darwin-x64-dsym-snapshot.zip
|
||||
3bbe5d587c3f582ed8c126b0fb635cc02ad9a14d077b04892fe6f862092445b0 *electron-v34.3.2-darwin-x64-dsym.zip
|
||||
fa7ece82e6ecaf1c94ed341e8ebff98e64687c68fe113f52cd9a21400302e22f *electron-v34.3.2-darwin-x64-symbols.zip
|
||||
23938c62257a65a863ed7aa7c7966ba5f257a7d3dc16c78293e826962cc39c5c *electron-v34.3.2-darwin-x64.zip
|
||||
0547eecf8ab538d74fa854f591ce8b888a3dbb339256d2db3038e7bb2c6dd929 *electron-v34.3.2-linux-arm64-debug.zip
|
||||
676d0dc2b1c1c85c8b2abbb8cd5376ee22ecdb910493b910d9ae5a998532136a *electron-v34.3.2-linux-arm64-symbols.zip
|
||||
774e4ccb39d553e5487994a9f8c60774a90f08cdb049ff65f3963fc27c969ff2 *electron-v34.3.2-linux-arm64.zip
|
||||
0547eecf8ab538d74fa854f591ce8b888a3dbb339256d2db3038e7bb2c6dd929 *electron-v34.3.2-linux-armv7l-debug.zip
|
||||
ba33bf53fcb35dea568a2795f5b23ecf46c218abe8258946611c72a1f42f716c *electron-v34.3.2-linux-armv7l-symbols.zip
|
||||
73ae92c8fffb351d3a455569cf57ce9a3f676f42bf61939c613c607fe0fc3bfb *electron-v34.3.2-linux-armv7l.zip
|
||||
e61a9a69dd7ea6f2687708a8e83516670cdea53c728226e598e2f6f1fad5b77b *electron-v34.3.2-linux-x64-debug.zip
|
||||
f1a04df7fe67dd1cd29e7b87871525458d2eb24c0cf3b5835a1c56974707562a *electron-v34.3.2-linux-x64-symbols.zip
|
||||
7b74c0c4fae82e27c7e9cbca13e9763e046113dba8737d3e27de9a0b300ac87e *electron-v34.3.2-linux-x64.zip
|
||||
8571a6aa83e00925ceb39fdc5a45a9f6b9aa3d92fd84951c6f252ed723aea4ae *electron-v34.3.2-mas-arm64-dsym-snapshot.zip
|
||||
477410c6f9a6c5eeaedf376058a02c2996fc0a334aa40eeec7d3734c09347f4d *electron-v34.3.2-mas-arm64-dsym.zip
|
||||
c2e62dcd6630cb51b2d8e2869e74e47d29bda785521cea6e82e546d0fc58aabb *electron-v34.3.2-mas-arm64-symbols.zip
|
||||
a1698e8546a062fd59b7f8e5507a7f3220fb00b347f2377de83fc9a07f7f3507 *electron-v34.3.2-mas-arm64.zip
|
||||
741a24ac230a3651dca81d211f9f00b835c428a5ed0c5f67d370d4e88b62f8d6 *electron-v34.3.2-mas-x64-dsym-snapshot.zip
|
||||
aeff97ec9e5c9e173ac89e38acd94476025c5640d5f27be1e8c2abd54398bab3 *electron-v34.3.2-mas-x64-dsym.zip
|
||||
9f14b66b1d612ac66697288e8763171c388f7f200854871a5f0ab464a6a921c2 *electron-v34.3.2-mas-x64-symbols.zip
|
||||
c979d7e7175f1e8e03ca187997d4c156b878189fc3611b347fadebcb16f3e027 *electron-v34.3.2-mas-x64.zip
|
||||
f43c700641e8220205dd356952e32718d113cf530520c4ed7209b59851eac266 *electron-v34.3.2-win32-arm64-pdb.zip
|
||||
3ba6e01c99bffac6b5dd3fd6f122ecdb571cf6f675dc5498c65050bd7a382ef8 *electron-v34.3.2-win32-arm64-symbols.zip
|
||||
c23f84aabb09c24cd2ae759a547fdba4206af19a3bb0f4554a91cd9528648ad0 *electron-v34.3.2-win32-arm64-toolchain-profile.zip
|
||||
9b9cb65d75a16782088b492f9ef3bb4d27525012b819c12bf29bd27e159d749b *electron-v34.3.2-win32-arm64.zip
|
||||
1006e7af4c149114b5ebc3497617aaa6cd1bb0b131e0a225fd73709ff308f9c5 *electron-v34.3.2-win32-ia32-pdb.zip
|
||||
1ecb6430cd04454f08f557c9579163f3552144bfcc0b67b768dad8868b5b891d *electron-v34.3.2-win32-ia32-symbols.zip
|
||||
c23f84aabb09c24cd2ae759a547fdba4206af19a3bb0f4554a91cd9528648ad0 *electron-v34.3.2-win32-ia32-toolchain-profile.zip
|
||||
d004fd5f853754001fafaec33e383d1950b30c935ee71b297ec1c9e084355e9b *electron-v34.3.2-win32-ia32.zip
|
||||
4e0721552fd2f09e9466e88089af8b965f1bfbc4ae00a59aaf6245b1d1efabfd *electron-v34.3.2-win32-x64-pdb.zip
|
||||
9dea812a7e7cd0fb18e5fed9a99db5531959a068c24d3c0ecedceb644cd3ffa0 *electron-v34.3.2-win32-x64-symbols.zip
|
||||
c23f84aabb09c24cd2ae759a547fdba4206af19a3bb0f4554a91cd9528648ad0 *electron-v34.3.2-win32-x64-toolchain-profile.zip
|
||||
1785e161420fb90d2331c26e50bba3413cae9625b7db3c8524ea02ade631efba *electron-v34.3.2-win32-x64.zip
|
||||
722b304c31ddac58b0083d94a241c5276464f04bd8ea4fcbfd33051d197be103 *electron.d.ts
|
||||
31ce159b2e47d1de5bc907d8e1c89726b0f2ba530ec2e9d7a8e5c723b1ccf6e0 *ffmpeg-v34.3.2-darwin-arm64.zip
|
||||
565539bac64a6ee9cf6f188070f520210a1507341718f5dc388ac7c454b1e1d5 *ffmpeg-v34.3.2-darwin-x64.zip
|
||||
6006ea0f46ab229feb2685be086b0fafd65981e2939dd2218a078459c75ab527 *ffmpeg-v34.3.2-linux-arm64.zip
|
||||
9404ce2e85df7c40f809f2cf62c7af607de299839fe6b7ae978c3015500abcc8 *ffmpeg-v34.3.2-linux-armv7l.zip
|
||||
79aec96898b7e2462826780ee0b52b9ab299dc662af333e128a34fd5ddae87f1 *ffmpeg-v34.3.2-linux-x64.zip
|
||||
9190743c78210574faf5d5ecb82a00f8fa15e5f2253378cb925a99ca9d39961b *ffmpeg-v34.3.2-mas-arm64.zip
|
||||
48915adcb1a6342efeda896035101300f0432c0304cfb38f2378e98c6309ebae *ffmpeg-v34.3.2-mas-x64.zip
|
||||
745d5ef786de6d4a720475079836e2fda7b501cfcd255819485a47de5b24b74e *ffmpeg-v34.3.2-win32-arm64.zip
|
||||
d0d86d60978439dc8ae4a723d4e4c1f853891d596bfd84033440a232fa762e2f *ffmpeg-v34.3.2-win32-ia32.zip
|
||||
4441539fd8c9cbe79880ff1bade9bdc0c3744c33d7409130af6404e57ee401ff *ffmpeg-v34.3.2-win32-x64.zip
|
||||
39edd1eeefe881aa75af0e438204e0b1c6e6724e34fa5819109276331c0c2c9a *hunspell_dictionaries.zip
|
||||
20dd417536e5f4ebc01f480221284c0673729c27b082bc04e2922f16cd571719 *libcxx-objects-v34.3.2-linux-arm64.zip
|
||||
7e53c5779c04f895f8282c0450ec4a63074d15a0e910e41006cfea133d0288af *libcxx-objects-v34.3.2-linux-armv7l.zip
|
||||
92e2283c924ab523ffec3ea22513beaab6417f7fc3d570f57d51a1e1ceb7f510 *libcxx-objects-v34.3.2-linux-x64.zip
|
||||
9bf3c6e8ad68f523fe086fada4148dd04e5bb0b9290d104873e66f2584a5cf50 *libcxx_headers.zip
|
||||
34e4b44f9c5e08b557a2caed55456ce7690abab910196a783a2a47b58d2b9ac9 *libcxxabi_headers.zip
|
||||
11f67635e6659f9188198e4086c51b89890b61a22f6c17c99eff35595ee8f51d *mksnapshot-v34.3.2-darwin-arm64.zip
|
||||
c0add9ef4ac27c73fa043d04b4c9635fd3fd9f5c16d7a03e961864ba05252813 *mksnapshot-v34.3.2-darwin-x64.zip
|
||||
6262adf86a340d8d28059937b01ef1117b93212e945fddbceea5c18d7e7c64f0 *mksnapshot-v34.3.2-linux-arm64-x64.zip
|
||||
f7db8ebe91a1cc8d24ef6aad12949a18d8e4975ac296e3e5e9ecd88c9bccb143 *mksnapshot-v34.3.2-linux-armv7l-x64.zip
|
||||
6642038e86bda362980ff1c8973a404e2b02efdd87de9e35b650fc1e743833da *mksnapshot-v34.3.2-linux-x64.zip
|
||||
15883bf8e8cd737c3682d1e719d7cbac92f50b525681aac324dca876861dfc7d *mksnapshot-v34.3.2-mas-arm64.zip
|
||||
4da23a950bfcc377ef21c37d496017ab4c36da03f3b41049ac114042c42608ce *mksnapshot-v34.3.2-mas-x64.zip
|
||||
fab59573d3c2f9bdf31146a1896d24ac0c51f736aad86d2f3c7ecef13c05a7fd *mksnapshot-v34.3.2-win32-arm64-x64.zip
|
||||
66f25e07c6f8d5d2009577a129440255a3baf63c929a5b60b2e77cd52e46105b *mksnapshot-v34.3.2-win32-ia32.zip
|
||||
8168bfbf61882cfac80aed1e71e364e1c7f2fccd11eac298e6abade8b46894ea *mksnapshot-v34.3.2-win32-x64.zip
|
||||
@@ -54,9 +54,11 @@ async function main(buildDir) {
|
||||
...defaultOpts,
|
||||
// TODO(deepak1556): Incorrectly declared type in electron-osx-sign
|
||||
ignore: (filePath) => {
|
||||
const ext = path_1.default.extname(filePath);
|
||||
return filePath.includes(gpuHelperAppName) ||
|
||||
filePath.includes(rendererHelperAppName) ||
|
||||
filePath.includes(pluginHelperAppName);
|
||||
filePath.includes(pluginHelperAppName) ||
|
||||
ext === '.asar' || ext === '.dat' || ext === '.gif' || ext === '.icns' || ext === '.ico' || ext === '.json' || ext === '.mp3' || ext === '.nib' || ext === '.pak' || ext === '.png' || ext === '.scpt' || ext === '.ttf' || ext === '.wasm' || ext === '.woff' || ext === '.woff2';
|
||||
}
|
||||
};
|
||||
const gpuHelperOpts = {
|
||||
|
||||
@@ -58,9 +58,11 @@ async function main(buildDir?: string): Promise<void> {
|
||||
...defaultOpts,
|
||||
// TODO(deepak1556): Incorrectly declared type in electron-osx-sign
|
||||
ignore: (filePath: string) => {
|
||||
const ext = path.extname(filePath);
|
||||
return filePath.includes(gpuHelperAppName) ||
|
||||
filePath.includes(rendererHelperAppName) ||
|
||||
filePath.includes(pluginHelperAppName);
|
||||
filePath.includes(pluginHelperAppName) ||
|
||||
ext === '.asar' || ext === '.dat' || ext === '.gif' || ext === '.icns' || ext === '.ico' || ext === '.json' || ext === '.mp3' || ext === '.nib' || ext === '.pak' || ext === '.png' || ext === '.scpt' || ext === '.ttf' || ext === '.wasm' || ext === '.woff' || ext === '.woff2';
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -139,6 +139,7 @@ module.exports.indentationFilter = [
|
||||
'!extensions/simple-browser/media/*.js',
|
||||
|
||||
'!extensions/open-remote-ssh/out/*.js', // Void added this
|
||||
'!extensions/open-remote-wsl/out/*.js', // Void added this
|
||||
];
|
||||
|
||||
module.exports.copyrightFilter = [
|
||||
|
||||
@@ -73,6 +73,7 @@ const compilations = [
|
||||
'.vscode/extensions/vscode-selfhost-import-aid/tsconfig.json',
|
||||
|
||||
'extensions/open-remote-ssh/tsconfig.json', // Void added this
|
||||
'extensions/open-remote-wsl/tsconfig.json', // Void added this
|
||||
|
||||
];
|
||||
|
||||
|
||||
+16
-4
@@ -154,9 +154,20 @@ function getNodeChecksum(expectedName) {
|
||||
}
|
||||
|
||||
function extractAlpinefromDocker(nodeVersion, platform, arch) {
|
||||
const imageName = arch === 'arm64' ? 'arm64v8/node' : 'node';
|
||||
let imageName = 'node';
|
||||
let dockerPlatform = '';
|
||||
|
||||
if (arch === 'arm64') {
|
||||
imageName = 'arm64v8/node';
|
||||
|
||||
const architecture = cp.execSync(`docker info --format '{{json .Architecture}}'`, { encoding: 'utf8' }).trim();
|
||||
if (architecture !== '"aarch64"') {
|
||||
dockerPlatform = '--platform=linux/arm64';
|
||||
}
|
||||
}
|
||||
|
||||
log(`Downloading node.js ${nodeVersion} ${platform} ${arch} from docker image ${imageName}`);
|
||||
const contents = cp.execSync(`docker run --rm ${imageName}:${nodeVersion}-alpine /bin/sh -c 'cat \`which node\`'`, { maxBuffer: 100 * 1024 * 1024, encoding: 'buffer' });
|
||||
const contents = cp.execSync(`docker run --rm ${dockerPlatform} ${imageName}:${nodeVersion}-alpine /bin/sh -c 'cat \`which node\`'`, { maxBuffer: 100 * 1024 * 1024, encoding: 'buffer' });
|
||||
return es.readArray([new File({ path: 'node', contents, stat: { mode: parseInt('755', 8) } })]);
|
||||
}
|
||||
|
||||
@@ -308,10 +319,11 @@ function packageTask(type, platform, arch, sourceFolderName, destinationFolderNa
|
||||
}
|
||||
|
||||
const name = product.nameShort;
|
||||
const release = packageJson.release;
|
||||
|
||||
let packageJsonContents;
|
||||
const packageJsonStream = gulp.src(['remote/package.json'], { base: 'remote' })
|
||||
.pipe(json({ name, version, dependencies: undefined, optionalDependencies: undefined, type: 'module' }))
|
||||
.pipe(json({ name, version, release, dependencies: undefined, optionalDependencies: undefined, type: 'module' }))
|
||||
.pipe(es.through(function (file) {
|
||||
packageJsonContents = file.contents.toString();
|
||||
this.emit('data', file);
|
||||
@@ -319,7 +331,7 @@ function packageTask(type, platform, arch, sourceFolderName, destinationFolderNa
|
||||
|
||||
let productJsonContents;
|
||||
const productJsonStream = gulp.src(['product.json'], { base: '.' })
|
||||
.pipe(json({ commit, date: readISODate('out-build'), version }))
|
||||
.pipe(json({ commit, date: readISODate('out-build'), version, release }))
|
||||
.pipe(es.through(function (file) {
|
||||
productJsonContents = file.contents.toString();
|
||||
this.emit('data', file);
|
||||
|
||||
@@ -267,7 +267,8 @@ function packageTask(platform, arch, sourceFolderName, destinationFolderName, op
|
||||
}
|
||||
|
||||
const name = product.nameShort;
|
||||
const packageJsonUpdates = { name, version };
|
||||
const release = packageJson.release;
|
||||
const packageJsonUpdates = { name, version, release };
|
||||
|
||||
if (platform === 'linux') {
|
||||
packageJsonUpdates.desktopName = `${product.applicationName}.desktop`;
|
||||
@@ -284,7 +285,7 @@ function packageTask(platform, arch, sourceFolderName, destinationFolderName, op
|
||||
// Void - this is important, creates the product.json in .app
|
||||
let productJsonContents;
|
||||
const productJsonStream = gulp.src(['product.json'], { base: '.' })
|
||||
.pipe(json({ commit, date: readISODate('out-build'), checksums, version }))
|
||||
.pipe(json({ commit, date: readISODate('out-build'), checksums, version, release }))
|
||||
.pipe(es.through(function (file) {
|
||||
productJsonContents = file.contents.toString();
|
||||
this.emit('data', file);
|
||||
@@ -372,7 +373,7 @@ function packageTask(platform, arch, sourceFolderName, destinationFolderName, op
|
||||
} else if (platform === 'darwin') {
|
||||
const shortcut = gulp.src('resources/darwin/bin/code.sh')
|
||||
.pipe(replace('@@APPNAME@@', product.applicationName))
|
||||
.pipe(rename('bin/code'));
|
||||
.pipe(rename('bin/' + product.applicationName));
|
||||
|
||||
all = es.merge(all, shortcut);
|
||||
}
|
||||
|
||||
@@ -25,7 +25,6 @@ const exec = util.promisify(cp.exec);
|
||||
const root = path.dirname(__dirname);
|
||||
const commit = getVersion(root);
|
||||
|
||||
const linuxPackageRevision = Math.floor(new Date().getTime() / 1000);
|
||||
|
||||
/**
|
||||
* @param {string} arch
|
||||
@@ -87,7 +86,7 @@ function prepareDebPackage(arch) {
|
||||
const dependencies = await dependenciesGenerator.getDependencies('deb', binaryDir, product.applicationName, debArch);
|
||||
gulp.src('resources/linux/debian/control.template', { base: '.' })
|
||||
.pipe(replace('@@NAME@@', product.applicationName))
|
||||
.pipe(replace('@@VERSION@@', packageJson.version + '-' + linuxPackageRevision))
|
||||
.pipe(replace('@@VERSION@@', `${packageJson.version}.${packageJson.release}`))
|
||||
.pipe(replace('@@ARCHITECTURE@@', debArch))
|
||||
.pipe(replace('@@DEPENDS@@', dependencies.join(', ')))
|
||||
.pipe(replace('@@RECOMMENDS@@', debianRecommendedDependencies.join(', ')))
|
||||
@@ -202,8 +201,7 @@ function prepareRpmPackage(arch) {
|
||||
.pipe(replace('@@NAME@@', product.applicationName))
|
||||
.pipe(replace('@@NAME_LONG@@', product.nameLong))
|
||||
.pipe(replace('@@ICON@@', product.linuxIconName))
|
||||
.pipe(replace('@@VERSION@@', packageJson.version))
|
||||
.pipe(replace('@@RELEASE@@', linuxPackageRevision))
|
||||
.pipe(replace('@@VERSION@@', `${packageJson.version}.${packageJson.release}`))
|
||||
.pipe(replace('@@ARCHITECTURE@@', rpmArch))
|
||||
.pipe(replace('@@LICENSE@@', product.licenseName))
|
||||
.pipe(replace('@@QUALITY@@', product.quality || '@@QUALITY@@'))
|
||||
@@ -279,7 +277,7 @@ function prepareSnapPackage(arch) {
|
||||
|
||||
const snapcraft = gulp.src('resources/linux/snap/snapcraft.yaml', { base: '.' })
|
||||
.pipe(replace('@@NAME@@', product.applicationName))
|
||||
.pipe(replace('@@VERSION@@', commit.substr(0, 8)))
|
||||
.pipe(replace('@@VERSION@@', `${packageJson.version}.${packageJson.release}`))
|
||||
// Possible run-on values https://snapcraft.io/docs/architectures
|
||||
.pipe(replace('@@ARCHITECTURE@@', arch === 'x64' ? 'amd64' : arch))
|
||||
.pipe(rename('snap/snapcraft.yaml'));
|
||||
|
||||
@@ -87,8 +87,8 @@ function buildWin32Setup(arch, target) {
|
||||
NameLong: product.nameLong,
|
||||
NameShort: product.nameShort,
|
||||
DirName: product.win32DirName,
|
||||
Version: pkg.version,
|
||||
RawVersion: pkg.version.replace(/-\w+$/, ''),
|
||||
Version: `${pkg.version}.${pkg.release}`,
|
||||
RawVersion: `${pkg.version.replace(/-\w+$/, '')}.${pkg.release}`,
|
||||
NameVersion: product.win32NameVersion + (target === 'user' ? ' (User)' : ''),
|
||||
ExeBasename: product.nameShort,
|
||||
RegValueName: product.win32RegValueName,
|
||||
|
||||
@@ -85,10 +85,7 @@ function getExtensionDownloadStream(extension) {
|
||||
if (extension.vsix) {
|
||||
input = ext.fromVsix(path_1.default.join(root, extension.vsix), extension);
|
||||
}
|
||||
else if (productjson.extensionsGallery?.serviceUrl) {
|
||||
input = ext.fromMarketplace(productjson.extensionsGallery.serviceUrl, extension);
|
||||
}
|
||||
else {
|
||||
else { // Void - ext-from-gh.patch
|
||||
input = ext.fromGithub(extension);
|
||||
}
|
||||
return input.pipe((0, gulp_rename_1.default)(p => p.dirname = `${extension.name}/${p.dirname}`));
|
||||
|
||||
@@ -73,9 +73,7 @@ function getExtensionDownloadStream(extension: IExtensionDefinition) {
|
||||
|
||||
if (extension.vsix) {
|
||||
input = ext.fromVsix(path.join(root, extension.vsix), extension);
|
||||
} else if (productjson.extensionsGallery?.serviceUrl) {
|
||||
input = ext.fromMarketplace(productjson.extensionsGallery.serviceUrl, extension);
|
||||
} else {
|
||||
} else { // Void - ext-from-gh.patch
|
||||
input = ext.fromGithub(extension);
|
||||
}
|
||||
|
||||
|
||||
@@ -55,6 +55,7 @@ const dirs = [
|
||||
'.vscode/extensions/vscode-selfhost-import-aid',
|
||||
'.vscode/extensions/vscode-selfhost-test-provider',
|
||||
'extensions/open-remote-ssh', // Void added this
|
||||
'extensions/open-remote-wsl', // Void added this
|
||||
|
||||
];
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ function getMangledFileContents(projectPath) {
|
||||
* @type {webpack.LoaderDefinitionFunction}
|
||||
*/
|
||||
module.exports = async function (source, sourceMap, meta) {
|
||||
if (this.mode !== 'production') {
|
||||
if (true) { // Void - extensions-disable-mangler
|
||||
// Only enable mangling in production builds
|
||||
return source;
|
||||
}
|
||||
|
||||
@@ -71,7 +71,7 @@
|
||||
"type": "string",
|
||||
"description": "The URL from where the vscode server will be downloaded. You can use the following variables and they will be replaced dynamically:\n- ${quality}: vscode server quality, e.g. stable or insiders\n- ${version}: vscode server version, e.g. 1.69.0\n- ${commit}: vscode server release commit\n- ${arch}: vscode server arch, e.g. x64, armhf, arm64\n- ${release}: release number",
|
||||
"scope": "application",
|
||||
"default": "https://github.com/voideditor/${NAME_OF_REPO}/releases/download/${version}.${release}/void-server-${os}-${arch}-${version}.${release}.tar.gz"
|
||||
"default": "https://github.com/voideditor/binaries/releases/download/${version}.${release}/void-reh-${os}-${arch}-${version}.${release}.tar.gz"
|
||||
},
|
||||
"remote.SSH.remotePlatform": {
|
||||
"type": "object",
|
||||
|
||||
@@ -32,12 +32,14 @@ export async function getVSCodeServerConfig(): Promise<IServerConfig> {
|
||||
const customServerBinaryName = vscode.workspace.getConfiguration('remote.SSH.experimental').get<string>('serverBinaryName', '');
|
||||
|
||||
return {
|
||||
version: vscode.version.replace('-insider', ''),
|
||||
// version: vscode.version.replace('-insider', ''),
|
||||
commit: productJson.commit,
|
||||
quality: productJson.quality,
|
||||
release: productJson.release,
|
||||
serverApplicationName: customServerBinaryName || productJson.serverApplicationName,
|
||||
serverDataFolderName: productJson.serverDataFolderName,
|
||||
serverDownloadUrlTemplate: productJson.serverDownloadUrlTemplate
|
||||
serverDownloadUrlTemplate: productJson.serverDownloadUrlTemplate,
|
||||
// Void changed this
|
||||
version: productJson.voidVersion
|
||||
};
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ export class ServerInstallError extends Error {
|
||||
}
|
||||
}
|
||||
|
||||
const DEFAULT_DOWNLOAD_URL_TEMPLATE = 'https://github.com/voideditor/void-updates-server/releases/download/test/void-server-${os}-${arch}.tar.gz';
|
||||
const DEFAULT_DOWNLOAD_URL_TEMPLATE = 'https://github.com/voideditor/binaries/releases/download/${version}.${release}/void-reh-${os}-${arch}-${version}.${release}.tar.gz';
|
||||
|
||||
export async function installCodeServer(conn: SSHConnection, serverDownloadUrlTemplate: string | undefined, extensionIds: string[], envVariables: string[], platform: string | undefined, useSocketPath: boolean, logger: Log): Promise<ServerInstallResult> {
|
||||
let shell = 'powershell';
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
# Remote - WSL Support
|
||||
|
||||
Inherited for Void from [Open Remote - WSL](https://github.com/jeanp413/open-remote-wsl).
|
||||
@@ -0,0 +1,17 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
//@ts-check
|
||||
|
||||
'use strict';
|
||||
|
||||
const withBrowserDefaults = require('../shared.webpack.config').browser;
|
||||
|
||||
module.exports = withBrowserDefaults({
|
||||
context: __dirname,
|
||||
entry: {
|
||||
extension: './src/extension.ts'
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,20 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
//@ts-check
|
||||
|
||||
'use strict';
|
||||
|
||||
const withDefaults = require('../shared.webpack.config');
|
||||
|
||||
module.exports = withDefaults({
|
||||
context: __dirname,
|
||||
resolve: {
|
||||
mainFields: ['module', 'main']
|
||||
},
|
||||
entry: {
|
||||
extension: './src/extension.ts',
|
||||
}
|
||||
});
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"name": "open-remote-wsl",
|
||||
"version": "0.0.4",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "open-remote-wsl",
|
||||
"version": "0.0.4",
|
||||
"engines": {
|
||||
"vscode": "^1.70.2"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,281 @@
|
||||
{
|
||||
"name": "open-remote-wsl",
|
||||
"displayName": "Remote - WSL",
|
||||
"description": "Open any folder in the Windows Subsystem for Linux (WSL).",
|
||||
"version": "0.0.4",
|
||||
"icon": "resources/icon.png",
|
||||
"engines": {
|
||||
"vscode": "^1.70.2"
|
||||
},
|
||||
"extensionKind": [
|
||||
"ui"
|
||||
],
|
||||
"enabledApiProposals": [
|
||||
"resolvers",
|
||||
"contribViewsRemote"
|
||||
],
|
||||
"keywords": [
|
||||
"remote development",
|
||||
"remote",
|
||||
"wsl"
|
||||
],
|
||||
"api": "none",
|
||||
"activationEvents": [
|
||||
"onCommand:openremotewsl.connect",
|
||||
"onCommand:openremotewsl.connectInNewWindow",
|
||||
"onCommand:openremotewsl.connectUsingDistro",
|
||||
"onCommand:openremotewsl.connectUsingDistroInNewWindow",
|
||||
"onCommand:openremotewsl.showLog",
|
||||
"onResolveRemoteAuthority:wsl",
|
||||
"onView:wslTargets"
|
||||
],
|
||||
"main": "./out/extension.js",
|
||||
"contributes": {
|
||||
"configuration": {
|
||||
"title": "WSL",
|
||||
"properties": {
|
||||
"remote.WSL.serverDownloadUrlTemplate": {
|
||||
"type": "string",
|
||||
"description": "The URL from where the vscode server will be downloaded. You can use the following variables and they will be replaced dynamically:\n- ${quality}: vscode server quality, e.g. stable or insiders\n- ${version}: vscode server version, e.g. 1.69.0\n- ${commit}: vscode server release commit\n- ${arch}: vscode server arch, e.g. x64, armhf, arm64\n- ${release}: release number",
|
||||
"scope": "application",
|
||||
"default": "https://github.com/voideditor/binaries/releases/download/${version}.${release}/void-reh-${os}-${arch}-${version}.${release}.tar.gz"
|
||||
}
|
||||
}
|
||||
},
|
||||
"views": {
|
||||
"remote": [
|
||||
{
|
||||
"id": "wslTargets",
|
||||
"name": "WSL Targets",
|
||||
"group": "targets@1",
|
||||
"when": "(isWindows && !isWeb)",
|
||||
"remoteName": "wsl"
|
||||
}
|
||||
]
|
||||
},
|
||||
"commands": [
|
||||
{
|
||||
"command": "openremotewsl.connect",
|
||||
"title": "Connect to WSL",
|
||||
"category": "Remote-WSL"
|
||||
},
|
||||
{
|
||||
"command": "openremotewsl.connectInNewWindow",
|
||||
"title": "Connect to WSL in New Window",
|
||||
"category": "Remote-WSL"
|
||||
},
|
||||
{
|
||||
"command": "openremotewsl.connectUsingDistro",
|
||||
"title": "Connect to WSL using Distro...",
|
||||
"category": "Remote-WSL"
|
||||
},
|
||||
{
|
||||
"command": "openremotewsl.connectUsingDistroInNewWindow",
|
||||
"title": "Connect to WSL using Distro in New Window...",
|
||||
"category": "Remote-WSL"
|
||||
},
|
||||
{
|
||||
"command": "openremotewsl.showLog",
|
||||
"title": "Show Log",
|
||||
"category": "Remote-WSL"
|
||||
},
|
||||
{
|
||||
"command": "openremotewsl.explorer.emptyWindowInNewWindow",
|
||||
"title": "Connect in New Window",
|
||||
"icon": "$(empty-window)"
|
||||
},
|
||||
{
|
||||
"command": "openremotewsl.explorer.emptyWindowInCurrentWindow",
|
||||
"title": "Connect in Current Window"
|
||||
},
|
||||
{
|
||||
"command": "openremotewsl.explorer.reopenFolderInCurrentWindow",
|
||||
"title": "Open in Current Window"
|
||||
},
|
||||
{
|
||||
"command": "openremotewsl.explorer.reopenFolderInNewWindow",
|
||||
"title": "Open in New Window",
|
||||
"icon": "$(folder-opened)"
|
||||
},
|
||||
{
|
||||
"command": "openremotewsl.explorer.deleteFolderHistoryItem",
|
||||
"title": "Remove From Recent List",
|
||||
"icon": "$(x)"
|
||||
},
|
||||
{
|
||||
"command": "openremotewsl.explorer.refresh",
|
||||
"title": "Refresh",
|
||||
"icon": "$(refresh)"
|
||||
},
|
||||
{
|
||||
"command": "openremotewsl.explorer.addDistro",
|
||||
"title": "Add a Distro",
|
||||
"icon": "$(plus)"
|
||||
},
|
||||
{
|
||||
"command": "openremotewsl.explorer.setDefaultDistro",
|
||||
"title": "Set as Default Distro"
|
||||
},
|
||||
{
|
||||
"command": "openremotewsl.explorer.deleteDistro",
|
||||
"title": "Delete Distro"
|
||||
}
|
||||
],
|
||||
"resourceLabelFormatters": [
|
||||
{
|
||||
"scheme": "vscode-remote",
|
||||
"authority": "wsl+*",
|
||||
"formatting": {
|
||||
"label": "${path}",
|
||||
"separator": "/",
|
||||
"tildify": true,
|
||||
"workspaceSuffix": "WSL"
|
||||
}
|
||||
}
|
||||
],
|
||||
"menus": {
|
||||
"statusBar/remoteIndicator": [
|
||||
{
|
||||
"command": "openremotewsl.connect",
|
||||
"when": "(isWindows && !isWeb)",
|
||||
"group": "remote_20_wsl_1general@1"
|
||||
},
|
||||
{
|
||||
"command": "openremotewsl.connectUsingDistro",
|
||||
"when": "(isWindows && !isWeb)",
|
||||
"group": "remote_20_wsl_1general@2"
|
||||
},
|
||||
{
|
||||
"command": "openremotewsl.showLog",
|
||||
"when": "remoteName =~ /^wsl$/",
|
||||
"group": "remote_20_wsl_1general@4"
|
||||
}
|
||||
],
|
||||
"commandPalette": [
|
||||
{
|
||||
"command": "openremotewsl.connect",
|
||||
"when": "(isWindows && !isWeb)"
|
||||
},
|
||||
{
|
||||
"command": "openremotewsl.connectInNewWindow",
|
||||
"when": "(isWindows && !isWeb)"
|
||||
},
|
||||
{
|
||||
"command": "openremotewsl.connectUsingDistro",
|
||||
"when": "(isWindows && !isWeb)"
|
||||
},
|
||||
{
|
||||
"command": "openremotewsl.connectUsingDistroInNewWindow",
|
||||
"when": "(isWindows && !isWeb)"
|
||||
},
|
||||
{
|
||||
"command": "openremotewsl.explorer.refresh",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "openremotewsl.explorer.addDistro",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "openremotewsl.explorer.emptyWindowInNewWindow",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "openremotewsl.explorer.emptyWindowInCurrentWindow",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "openremotewsl.explorer.reopenFolderInCurrentWindow",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "openremotewsl.explorer.reopenFolderInNewWindow",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "openremotewsl.explorer.deleteFolderHistoryItem",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "openremotewsl.explorer.setDefaultDistro",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "openremotewsl.explorer.deleteDistro",
|
||||
"when": "false"
|
||||
}
|
||||
],
|
||||
"view/title": [
|
||||
{
|
||||
"command": "openremotewsl.explorer.addDistro",
|
||||
"when": "view == wslTargets",
|
||||
"group": "navigation"
|
||||
},
|
||||
{
|
||||
"command": "openremotewsl.explorer.refresh",
|
||||
"when": "view == wslTargets",
|
||||
"group": "navigation"
|
||||
}
|
||||
],
|
||||
"view/item/context": [
|
||||
{
|
||||
"command": "openremotewsl.explorer.emptyWindowInNewWindow",
|
||||
"when": "viewItem =~ /^openremotewsl.explorer.distro$/",
|
||||
"group": "inline@1"
|
||||
},
|
||||
{
|
||||
"command": "openremotewsl.explorer.emptyWindowInNewWindow",
|
||||
"when": "viewItem =~ /^openremotewsl.explorer.distro$/",
|
||||
"group": "navigation@2"
|
||||
},
|
||||
{
|
||||
"command": "openremotewsl.explorer.emptyWindowInCurrentWindow",
|
||||
"when": "viewItem =~ /^openremotewsl.explorer.distro$/",
|
||||
"group": "navigation@1"
|
||||
},
|
||||
{
|
||||
"command": "openremotewsl.explorer.setDefaultDistro",
|
||||
"when": "viewItem =~ /^openremotewsl.explorer.distro$/",
|
||||
"group": "management@1"
|
||||
},
|
||||
{
|
||||
"command": "openremotewsl.explorer.deleteDistro",
|
||||
"when": "viewItem =~ /^openremotewsl.explorer.distro$/",
|
||||
"group": "management@2"
|
||||
},
|
||||
{
|
||||
"command": "openremotewsl.explorer.reopenFolderInNewWindow",
|
||||
"when": "viewItem == openremotewsl.explorer.folder",
|
||||
"group": "inline@1"
|
||||
},
|
||||
{
|
||||
"command": "openremotewsl.explorer.reopenFolderInNewWindow",
|
||||
"when": "viewItem == openremotewsl.explorer.folder",
|
||||
"group": "navigation@2"
|
||||
},
|
||||
{
|
||||
"command": "openremotewsl.explorer.reopenFolderInCurrentWindow",
|
||||
"when": "viewItem == openremotewsl.explorer.folder",
|
||||
"group": "navigation@1"
|
||||
},
|
||||
{
|
||||
"command": "openremotewsl.explorer.deleteFolderHistoryItem",
|
||||
"when": "viewItem =~ /^openremotewsl.explorer.folder/",
|
||||
"group": "navigation@3"
|
||||
},
|
||||
{
|
||||
"command": "openremotewsl.explorer.deleteFolderHistoryItem",
|
||||
"when": "viewItem =~ /^openremotewsl.explorer.folder/",
|
||||
"group": "inline@2"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"vscode:prepublish": "npm run compile",
|
||||
"compile": "gulp compile-extension:open-remote-wsl",
|
||||
"compile-web": "npx webpack-cli --config extension-browser.webpack.config --mode none",
|
||||
"watch": "gulp watch-extension:open-remote-wsl",
|
||||
"watch-web": "npx webpack-cli --config extension-browser.webpack.config --mode none --watch --info-verbosity verbose"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
import Log from './common/logger';
|
||||
import { installCodeServer, ServerInstallError } from './serverSetup';
|
||||
import { WSLManager } from './wsl/wslManager';
|
||||
|
||||
export const REMOTE_WSL_AUTHORITY = 'wsl';
|
||||
|
||||
export function getRemoteAuthority(distro: string) {
|
||||
return `${REMOTE_WSL_AUTHORITY}+${distro}`;
|
||||
}
|
||||
|
||||
class Tunnel implements vscode.Tunnel {
|
||||
private _onDidDisposeEmitter = new vscode.EventEmitter<void>();
|
||||
|
||||
readonly onDidDispose = this._onDidDisposeEmitter.event;
|
||||
|
||||
constructor(
|
||||
readonly remoteAddress: { port: number; host: string },
|
||||
readonly localAddress: { port: number; host: string }
|
||||
) {
|
||||
// If ipv6 localhost 0:0:0:0:0:0:0:1 or [::1] replace with localhost
|
||||
if (localAddress.host !== 'localhost' && localAddress.host !== '127.0.0.1') {
|
||||
localAddress.host = 'localhost';
|
||||
}
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this._onDidDisposeEmitter.fire();
|
||||
}
|
||||
}
|
||||
|
||||
export class RemoteWSLResolver implements vscode.RemoteAuthorityResolver, vscode.Disposable {
|
||||
|
||||
private labelFormatterDisposable: vscode.Disposable | undefined;
|
||||
|
||||
constructor(
|
||||
private readonly wslManager: WSLManager,
|
||||
private readonly logger: Log
|
||||
) {
|
||||
}
|
||||
|
||||
resolve(authority: string, context: vscode.RemoteAuthorityResolverContext): Thenable<vscode.ResolverResult> {
|
||||
const [type, distroName] = authority.split('+');
|
||||
if (type !== REMOTE_WSL_AUTHORITY) {
|
||||
throw new Error(`Invalid authority type for WSL resolver: ${type}`);
|
||||
}
|
||||
|
||||
this.logger.info(`Resolving wsl remote authority '${authority}' (attemp #${context.resolveAttempt})`);
|
||||
|
||||
// It looks like default values are not loaded yet when resolving a remote,
|
||||
// so let's hardcode the default values here
|
||||
const remoteSSHconfig = vscode.workspace.getConfiguration('remote.WSL');
|
||||
const serverDownloadUrlTemplate = remoteSSHconfig.get<string>('serverDownloadUrlTemplate');
|
||||
|
||||
return vscode.window.withProgress({
|
||||
title: `Setting up WSL Distro: ${distroName}`,
|
||||
location: vscode.ProgressLocation.Notification,
|
||||
cancellable: false
|
||||
}, async () => {
|
||||
try {
|
||||
const installResult = await installCodeServer(this.wslManager, distroName, serverDownloadUrlTemplate, [], [], this.logger);
|
||||
|
||||
this.labelFormatterDisposable?.dispose();
|
||||
this.labelFormatterDisposable = vscode.workspace.registerResourceLabelFormatter({
|
||||
scheme: 'vscode-remote',
|
||||
authority: `${REMOTE_WSL_AUTHORITY}+*`,
|
||||
formatting: {
|
||||
label: '${path}',
|
||||
separator: '/',
|
||||
tildify: true,
|
||||
workspaceSuffix: `WSL: ${distroName}`,
|
||||
workspaceTooltip: `Running in ${distroName}`
|
||||
}
|
||||
});
|
||||
|
||||
return new vscode.ResolvedAuthority('127.0.0.1', installResult.listeningOn, installResult.connectionToken);
|
||||
} catch (e: unknown) {
|
||||
this.logger.error(`Error resolving authority`, e);
|
||||
|
||||
// Initial connection
|
||||
if (context.resolveAttempt === 1) {
|
||||
this.logger.show();
|
||||
|
||||
const closeRemote = 'Close Remote';
|
||||
const retry = 'Retry';
|
||||
const result = await vscode.window.showErrorMessage(`Could not establish connection to WSL distro "${distroName}"`, { modal: true }, closeRemote, retry);
|
||||
if (result === closeRemote) {
|
||||
await vscode.commands.executeCommand('workbench.action.remote.close');
|
||||
} else if (result === retry) {
|
||||
await vscode.commands.executeCommand('workbench.action.reloadWindow');
|
||||
}
|
||||
}
|
||||
|
||||
if (e instanceof ServerInstallError || !(e instanceof Error)) {
|
||||
throw vscode.RemoteAuthorityResolverError.NotAvailable(e instanceof Error ? e.message : String(e));
|
||||
} else {
|
||||
throw vscode.RemoteAuthorityResolverError.TemporarilyNotAvailable(e.message);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async tunnelFactory(tunnelOptions: vscode.TunnelOptions) {
|
||||
return new Tunnel(
|
||||
tunnelOptions.remoteAddress,
|
||||
{
|
||||
host: tunnelOptions.remoteAddress.host,
|
||||
port: tunnelOptions.localAddressPort ?? tunnelOptions.remoteAddress.port
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this.labelFormatterDisposable?.dispose();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
import { getRemoteAuthority } from './authResolver';
|
||||
import { WSLDistro, WSLManager, WSLOnlineDistro } from './wsl/wslManager';
|
||||
import wslTerminal from './wsl/wslTerminal';
|
||||
|
||||
async function showDistrosPicker(wslManager: WSLManager, placeHolder: string): Promise<WSLDistro | undefined> {
|
||||
const pickItemsPromise = wslManager.listDistros()
|
||||
.then(distros => distros.map(distroData => {
|
||||
return {
|
||||
...distroData,
|
||||
label: `${distroData.name}`,
|
||||
detail: distroData.isDefault ? 'default distro' : undefined,
|
||||
};
|
||||
}));
|
||||
|
||||
const picked = await vscode.window.showQuickPick(pickItemsPromise, { canPickMany: false, placeHolder });
|
||||
return picked;
|
||||
}
|
||||
|
||||
async function showOnlineDistrosPicker(wslManager: WSLManager, placeHolder: string): Promise<WSLOnlineDistro | undefined> {
|
||||
const pickItemsPromise = Promise.all([wslManager.listOnlineDistros(), wslManager.listDistros()])
|
||||
.then(([onlineDistros, localDistros]) => {
|
||||
const distroToInstall = onlineDistros.filter(d => !localDistros.some(l => l.name === d.name));
|
||||
return distroToInstall.map(distroData => {
|
||||
return {
|
||||
...distroData,
|
||||
label: `${distroData.friendlyName}`,
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
const picked = await vscode.window.showQuickPick(pickItemsPromise, { canPickMany: false, placeHolder });
|
||||
return picked;
|
||||
}
|
||||
|
||||
export async function promptOpenRemoteWSLWindow(wslManager: WSLManager, useDefault: boolean, reuseWindow: boolean) {
|
||||
let distroName: string | undefined;
|
||||
if (useDefault) {
|
||||
const distros = await wslManager.listDistros();
|
||||
distroName = distros.find(distro => distro.isDefault)?.name;
|
||||
} else {
|
||||
distroName = (await showDistrosPicker(wslManager, 'Select WSL distro'))?.name;
|
||||
}
|
||||
|
||||
if (!distroName) {
|
||||
return;
|
||||
}
|
||||
|
||||
openRemoteWSLWindow(distroName, reuseWindow);
|
||||
}
|
||||
|
||||
export async function promptInstallNewWSLDistro(wslManager: WSLManager) {
|
||||
const distroName = (await showOnlineDistrosPicker(wslManager, 'Select the WSL distro to install'))?.name;
|
||||
if (!distroName) {
|
||||
return;
|
||||
}
|
||||
|
||||
wslTerminal.runCommand(`wsl.exe --install -d ${distroName}`);
|
||||
}
|
||||
|
||||
export function openRemoteWSLWindow(distro: string, reuseWindow: boolean) {
|
||||
vscode.commands.executeCommand('vscode.newWindow', { remoteAuthority: getRemoteAuthority(distro), reuseWindow });
|
||||
}
|
||||
|
||||
export function openRemoteWSLLocationWindow(distro: string, path: string, reuseWindow: boolean) {
|
||||
vscode.commands.executeCommand('vscode.openFolder', vscode.Uri.from({ scheme: 'vscode-remote', authority: getRemoteAuthority(distro), path }), { forceNewWindow: !reuseWindow });
|
||||
}
|
||||
|
||||
export async function setDefaultWSLDistro(wslManager: WSLManager, distroName: string) {
|
||||
await wslManager.setDefaultDistro(distroName);
|
||||
}
|
||||
|
||||
export async function deleteWSLDistro(wslManager: WSLManager, distroName: string) {
|
||||
const deleteAction = 'Delete';
|
||||
const resp = await vscode.window.showInformationMessage(`Are you sure you want to permanently delete the distro "${distroName}" including all its data?`, { modal: true }, deleteAction);
|
||||
if (resp === deleteAction) {
|
||||
await wslManager.deleteDistro(distroName);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
export function timeout(millis: number): Promise<void> {
|
||||
return new Promise((resolve) => setTimeout(resolve, millis));
|
||||
}
|
||||
|
||||
export interface ITask<T> {
|
||||
(): T;
|
||||
}
|
||||
|
||||
export async function retry<T>(task: ITask<Promise<T>>, delay: number, retries: number): Promise<T> {
|
||||
let lastError: Error | undefined;
|
||||
|
||||
for (let i = 0; i < retries; i++) {
|
||||
try {
|
||||
return await task();
|
||||
} catch (error) {
|
||||
lastError = error;
|
||||
|
||||
await timeout(delay);
|
||||
}
|
||||
}
|
||||
|
||||
throw lastError;
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
|
||||
export function disposeAll(disposables: vscode.Disposable[]): void {
|
||||
while (disposables.length) {
|
||||
const item = disposables.pop();
|
||||
if (item) {
|
||||
item.dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export abstract class Disposable {
|
||||
private _isDisposed = false;
|
||||
|
||||
protected _disposables: vscode.Disposable[] = [];
|
||||
|
||||
public dispose(): any {
|
||||
if (this._isDisposed) {
|
||||
return;
|
||||
}
|
||||
this._isDisposed = true;
|
||||
disposeAll(this._disposables);
|
||||
}
|
||||
|
||||
protected _register<T extends vscode.Disposable>(value: T): T {
|
||||
if (this._isDisposed) {
|
||||
value.dispose();
|
||||
} else {
|
||||
this._disposables.push(value);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
protected get isDisposed(): boolean {
|
||||
return this._isDisposed;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
export interface IDisposable {
|
||||
dispose(): void;
|
||||
}
|
||||
|
||||
export interface Event<T> {
|
||||
(listener: (e: T) => any, thisArgs?: any, disposables?: IDisposable[]): IDisposable;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a promise that resolves when the event fires, or when cancellation
|
||||
* is requested, whichever happens first.
|
||||
*/
|
||||
export function toPromise<T>(event: Event<T>): Promise<T>;
|
||||
export function toPromise<T>(event: Event<T>, signal: AbortSignal): Promise<T | undefined>;
|
||||
export function toPromise<T>(event: Event<T>, signal?: AbortSignal): Promise<T | undefined> {
|
||||
if (!signal) {
|
||||
return new Promise<T>((resolve) => once(event, resolve));
|
||||
}
|
||||
|
||||
if (signal.aborted) {
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
|
||||
return new Promise((resolve) => {
|
||||
const d2 = once(event, (data) => {
|
||||
(signal as any).removeEventListener('abort', d1);
|
||||
resolve(data);
|
||||
});
|
||||
|
||||
const d1 = () => {
|
||||
d2.dispose();
|
||||
(signal as any).removeEventListener('abort', d1);
|
||||
resolve(undefined);
|
||||
};
|
||||
|
||||
(signal as any).addEventListener('abort', d1);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a handler that handles one event on the emitter, then disposes itself.
|
||||
*/
|
||||
export const once = <T>(event: Event<T>, listener: (data: T) => void): IDisposable => {
|
||||
const disposable = event((value) => {
|
||||
listener(value);
|
||||
disposable.dispose();
|
||||
});
|
||||
|
||||
return disposable;
|
||||
};
|
||||
|
||||
/**
|
||||
* Base event emitter. Calls listeners when data is emitted.
|
||||
*/
|
||||
export class EventEmitter<T> {
|
||||
private listeners?: Array<(data: T) => void> | ((data: T) => void);
|
||||
|
||||
/**
|
||||
* Event<T> function.
|
||||
*/
|
||||
public readonly event: Event<T> = (listener, thisArgs, disposables) => {
|
||||
const d = this.add(thisArgs ? listener.bind(thisArgs) : listener);
|
||||
disposables?.push(d);
|
||||
return d;
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the number of event listeners.
|
||||
*/
|
||||
public get size() {
|
||||
if (!this.listeners) {
|
||||
return 0;
|
||||
} else if (typeof this.listeners === 'function') {
|
||||
return 1;
|
||||
} else {
|
||||
return this.listeners.length;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Emits event data.
|
||||
*/
|
||||
public fire(value: T) {
|
||||
if (!this.listeners) {
|
||||
// no-op
|
||||
} else if (typeof this.listeners === 'function') {
|
||||
this.listeners(value);
|
||||
} else {
|
||||
for (const listener of this.listeners) {
|
||||
listener(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Disposes of the emitter.
|
||||
*/
|
||||
public dispose() {
|
||||
this.listeners = undefined;
|
||||
}
|
||||
|
||||
private add(listener: (data: T) => void): IDisposable {
|
||||
if (!this.listeners) {
|
||||
this.listeners = listener;
|
||||
} else if (typeof this.listeners === 'function') {
|
||||
this.listeners = [this.listeners, listener];
|
||||
} else {
|
||||
this.listeners.push(listener);
|
||||
}
|
||||
|
||||
return { dispose: () => this.rm(listener) };
|
||||
}
|
||||
|
||||
private rm(listener: (data: T) => void) {
|
||||
if (!this.listeners) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof this.listeners === 'function') {
|
||||
if (this.listeners === listener) {
|
||||
this.listeners = undefined;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const index = this.listeners.indexOf(listener);
|
||||
if (index === -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.listeners.length === 2) {
|
||||
this.listeners = index === 0 ? this.listeners[1] : this.listeners[0];
|
||||
} else {
|
||||
this.listeners = this.listeners.slice(0, index).concat(this.listeners.slice(index + 1));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as fs from 'fs';
|
||||
import * as os from 'os';
|
||||
|
||||
const homeDir = os.homedir();
|
||||
|
||||
export async function exists(path: string) {
|
||||
try {
|
||||
await fs.promises.access(path);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function untildify(path: string) {
|
||||
return path.replace(/^~(?=$|\/|\\)/, homeDir);
|
||||
}
|
||||
|
||||
export function normalizeToSlash(path: string) {
|
||||
return path.replace(/\\/g, '/');
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
|
||||
type LogLevel = 'Trace' | 'Info' | 'Error';
|
||||
|
||||
export default class Log {
|
||||
private output: vscode.OutputChannel;
|
||||
|
||||
constructor(name: string) {
|
||||
this.output = vscode.window.createOutputChannel(name);
|
||||
}
|
||||
|
||||
private data2String(data: any): string {
|
||||
if (data instanceof Error) {
|
||||
return data.stack || data.message;
|
||||
}
|
||||
if (data.success === false && data.message) {
|
||||
return data.message;
|
||||
}
|
||||
return data.toString();
|
||||
}
|
||||
|
||||
public trace(message: string, data?: any): void {
|
||||
this.logLevel('Trace', message, data);
|
||||
}
|
||||
|
||||
public info(message: string, data?: any): void {
|
||||
this.logLevel('Info', message, data);
|
||||
}
|
||||
|
||||
public error(message: string, data?: any): void {
|
||||
this.logLevel('Error', message, data);
|
||||
}
|
||||
|
||||
public logLevel(level: LogLevel, message: string, data?: any): void {
|
||||
this.output.appendLine(`[${level} - ${this.now()}] ${message}`);
|
||||
if (data) {
|
||||
this.output.appendLine(this.data2String(data));
|
||||
}
|
||||
}
|
||||
|
||||
private now(): string {
|
||||
const now = new Date();
|
||||
return padLeft(now.getUTCHours() + '', 2, '0')
|
||||
+ ':' + padLeft(now.getMinutes() + '', 2, '0')
|
||||
+ ':' + padLeft(now.getUTCSeconds() + '', 2, '0') + '.' + now.getMilliseconds();
|
||||
}
|
||||
|
||||
public show() {
|
||||
this.output.show();
|
||||
}
|
||||
|
||||
public dispose() {
|
||||
this.output.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
function padLeft(s: string, n: number, pad = ' ') {
|
||||
return pad.repeat(Math.max(0, n - s.length)) + s;
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
export const isWindows = process.platform === 'win32';
|
||||
export const isMacintosh = process.platform === 'darwin';
|
||||
export const isLinux = process.platform === 'linux';
|
||||
@@ -0,0 +1,134 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as net from 'net';
|
||||
|
||||
/**
|
||||
* Finds a random unused port assigned by the operating system. Will reject in case no free port can be found.
|
||||
*/
|
||||
export function findRandomPort(): Promise<number> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const server = net.createServer({ pauseOnConnect: true });
|
||||
server.on('error', reject);
|
||||
server.on('listening', () => {
|
||||
const port = (server.address() as net.AddressInfo).port;
|
||||
server.close(() => resolve(port));
|
||||
});
|
||||
server.listen(0, '127.0.0.1');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a start point and a max number of retries, will find a port that
|
||||
* is openable. Will return 0 in case no free port can be found.
|
||||
*/
|
||||
export function findFreePort(startPort: number, giveUpAfter: number, timeout: number, stride = 1): Promise<number> {
|
||||
let done = false;
|
||||
|
||||
return new Promise(resolve => {
|
||||
const timeoutHandle = setTimeout(() => {
|
||||
if (!done) {
|
||||
done = true;
|
||||
return resolve(0);
|
||||
}
|
||||
}, timeout);
|
||||
|
||||
doFindFreePort(startPort, giveUpAfter, stride, (port) => {
|
||||
if (!done) {
|
||||
done = true;
|
||||
clearTimeout(timeoutHandle);
|
||||
return resolve(port);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function doFindFreePort(startPort: number, giveUpAfter: number, stride: number, clb: (port: number) => void): void {
|
||||
if (giveUpAfter === 0) {
|
||||
return clb(0);
|
||||
}
|
||||
|
||||
const client = new net.Socket();
|
||||
|
||||
// If we can connect to the port it means the port is already taken so we continue searching
|
||||
client.once('connect', () => {
|
||||
dispose(client);
|
||||
|
||||
return doFindFreePort(startPort + stride, giveUpAfter - 1, stride, clb);
|
||||
});
|
||||
|
||||
client.once('data', () => {
|
||||
// this listener is required since node.js 8.x
|
||||
});
|
||||
|
||||
client.once('error', (err: Error & { code?: string }) => {
|
||||
dispose(client);
|
||||
|
||||
// If we receive any non ECONNREFUSED error, it means the port is used but we cannot connect
|
||||
if (err.code !== 'ECONNREFUSED') {
|
||||
return doFindFreePort(startPort + stride, giveUpAfter - 1, stride, clb);
|
||||
}
|
||||
|
||||
// Otherwise it means the port is free to use!
|
||||
return clb(startPort);
|
||||
});
|
||||
|
||||
client.connect(startPort, '127.0.0.1');
|
||||
}
|
||||
|
||||
/**
|
||||
* Uses listen instead of connect. Is faster, but if there is another listener on 0.0.0.0 then this will take 127.0.0.1 from that listener.
|
||||
*/
|
||||
export function findFreePortFaster(startPort: number, giveUpAfter: number, timeout: number): Promise<number> {
|
||||
let resolved = false;
|
||||
let timeoutHandle: NodeJS.Timeout | undefined = undefined;
|
||||
let countTried = 1;
|
||||
const server = net.createServer({ pauseOnConnect: true });
|
||||
function doResolve(port: number, resolve: (port: number) => void) {
|
||||
if (!resolved) {
|
||||
resolved = true;
|
||||
server.removeAllListeners();
|
||||
server.close();
|
||||
if (timeoutHandle) {
|
||||
clearTimeout(timeoutHandle);
|
||||
}
|
||||
resolve(port);
|
||||
}
|
||||
}
|
||||
return new Promise<number>(resolve => {
|
||||
timeoutHandle = setTimeout(() => {
|
||||
doResolve(0, resolve);
|
||||
}, timeout);
|
||||
|
||||
server.on('listening', () => {
|
||||
doResolve(startPort, resolve);
|
||||
});
|
||||
server.on('error', err => {
|
||||
if (err && ((<any>err).code === 'EADDRINUSE' || (<any>err).code === 'EACCES') && (countTried < giveUpAfter)) {
|
||||
startPort++;
|
||||
countTried++;
|
||||
server.listen(startPort, '127.0.0.1');
|
||||
} else {
|
||||
doResolve(0, resolve);
|
||||
}
|
||||
});
|
||||
server.on('close', () => {
|
||||
doResolve(0, resolve);
|
||||
});
|
||||
server.listen(startPort, '127.0.0.1');
|
||||
});
|
||||
}
|
||||
|
||||
function dispose(socket: net.Socket): void {
|
||||
try {
|
||||
socket.removeAllListeners('connect');
|
||||
socket.removeAllListeners('error');
|
||||
socket.end();
|
||||
socket.destroy();
|
||||
socket.unref();
|
||||
} catch (error) {
|
||||
console.error(error); // otherwise this error would get lost in the callback chain
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
import * as path from 'path';
|
||||
import { RemoteLocationHistory } from './remoteLocationHistory';
|
||||
import { Disposable } from './common/disposable';
|
||||
import { openRemoteWSLWindow, openRemoteWSLLocationWindow, promptInstallNewWSLDistro, deleteWSLDistro, setDefaultWSLDistro } from './commands';
|
||||
import { WSLManager } from './wsl/wslManager';
|
||||
|
||||
class DistroItem {
|
||||
constructor(
|
||||
public name: string,
|
||||
public isDefault: boolean,
|
||||
public locations: string[]
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
||||
class DistroLocationItem {
|
||||
constructor(
|
||||
public path: string,
|
||||
public name: string
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
||||
type DataTreeItem = DistroItem | DistroLocationItem;
|
||||
|
||||
export class DistroTreeDataProvider extends Disposable implements vscode.TreeDataProvider<DataTreeItem> {
|
||||
|
||||
private readonly _onDidChangeTreeData = this._register(new vscode.EventEmitter<DataTreeItem | DataTreeItem[] | void>());
|
||||
public readonly onDidChangeTreeData = this._onDidChangeTreeData.event;
|
||||
|
||||
constructor(
|
||||
private readonly locationHistory: RemoteLocationHistory,
|
||||
private readonly wslManager: WSLManager
|
||||
) {
|
||||
super();
|
||||
|
||||
this._register(vscode.commands.registerCommand('openremotewsl.explorer.addDistro', () => promptInstallNewWSLDistro(wslManager)));
|
||||
this._register(vscode.commands.registerCommand('openremotewsl.explorer.refresh', () => this.refresh()));
|
||||
this._register(vscode.commands.registerCommand('openremotewsl.explorer.emptyWindowInNewWindow', e => this.openRemoteWSLWindow(e, false)));
|
||||
this._register(vscode.commands.registerCommand('openremotewsl.explorer.emptyWindowInCurrentWindow', e => this.openRemoteWSLWindow(e, true)));
|
||||
this._register(vscode.commands.registerCommand('openremotewsl.explorer.reopenFolderInNewWindow', e => this.openRemoteWSLocationWindow(e, false)));
|
||||
this._register(vscode.commands.registerCommand('openremotewsl.explorer.reopenFolderInCurrentWindow', e => this.openRemoteWSLocationWindow(e, true)));
|
||||
this._register(vscode.commands.registerCommand('openremotewsl.explorer.deleteFolderHistoryItem', e => this.deleteDistroLocation(e)));
|
||||
this._register(vscode.commands.registerCommand('openremotewsl.explorer.setDefaultDistro', e => this.setDefaultDistro(e)));
|
||||
this._register(vscode.commands.registerCommand('openremotewsl.explorer.deleteDistro', e => this.deleteDistro(e)));
|
||||
}
|
||||
|
||||
getTreeItem(element: DataTreeItem): vscode.TreeItem {
|
||||
if (element instanceof DistroLocationItem) {
|
||||
const label = path.posix.basename(element.path).replace(/\.code-workspace$/, ' (Workspace)');
|
||||
const treeItem = new vscode.TreeItem(label);
|
||||
treeItem.description = path.posix.dirname(element.path);
|
||||
treeItem.iconPath = new vscode.ThemeIcon('folder');
|
||||
treeItem.contextValue = 'openremotewsl.explorer.folder';
|
||||
return treeItem;
|
||||
}
|
||||
|
||||
const treeItem = new vscode.TreeItem(element.name);
|
||||
treeItem.description = element.isDefault ? 'default distro' : undefined;
|
||||
treeItem.collapsibleState = element.locations.length ? vscode.TreeItemCollapsibleState.Collapsed : vscode.TreeItemCollapsibleState.None;
|
||||
treeItem.iconPath = new vscode.ThemeIcon('vm');
|
||||
treeItem.contextValue = 'openremotewsl.explorer.distro';
|
||||
return treeItem;
|
||||
}
|
||||
|
||||
async getChildren(element?: DistroItem): Promise<DataTreeItem[]> {
|
||||
if (!element) {
|
||||
const distros = await this.wslManager.listDistros();
|
||||
return distros.map(distro => new DistroItem(distro.name, distro.isDefault, this.locationHistory.getHistory(distro.name)));
|
||||
}
|
||||
if (element instanceof DistroItem) {
|
||||
return element.locations.map(location => new DistroLocationItem(location, element.name));
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
private refresh() {
|
||||
this._onDidChangeTreeData.fire();
|
||||
}
|
||||
|
||||
private async deleteDistroLocation(element: DistroLocationItem) {
|
||||
await this.locationHistory.removeLocation(element.name, element.path);
|
||||
this.refresh();
|
||||
}
|
||||
|
||||
private async openRemoteWSLWindow(element: DistroItem, reuseWindow: boolean) {
|
||||
openRemoteWSLWindow(element.name, reuseWindow);
|
||||
}
|
||||
|
||||
private async openRemoteWSLocationWindow(element: DistroLocationItem, reuseWindow: boolean) {
|
||||
openRemoteWSLLocationWindow(element.name, element.path, reuseWindow);
|
||||
}
|
||||
|
||||
private async setDefaultDistro(element: DistroItem) {
|
||||
await setDefaultWSLDistro(this.wslManager, element.name);
|
||||
this.refresh();
|
||||
}
|
||||
|
||||
private async deleteDistro(element: DistroItem) {
|
||||
await deleteWSLDistro(this.wslManager, element.name);
|
||||
this.refresh();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
import Log from './common/logger';
|
||||
import { RemoteWSLResolver, REMOTE_WSL_AUTHORITY } from './authResolver';
|
||||
import { promptOpenRemoteWSLWindow } from './commands';
|
||||
import { DistroTreeDataProvider } from './distroTreeView';
|
||||
import { getRemoteWorkspaceLocationData, RemoteLocationHistory } from './remoteLocationHistory';
|
||||
import { WSLManager } from './wsl/wslManager';
|
||||
import { isWindows } from './common/platform';
|
||||
|
||||
export async function activate(context: vscode.ExtensionContext) {
|
||||
if (!isWindows) {
|
||||
return;
|
||||
}
|
||||
|
||||
const logger = new Log('Remote - WSL');
|
||||
context.subscriptions.push(logger);
|
||||
|
||||
const wslManager = new WSLManager(logger);
|
||||
const remoteWSLResolver = new RemoteWSLResolver(wslManager, logger);
|
||||
context.subscriptions.push(vscode.workspace.registerRemoteAuthorityResolver(REMOTE_WSL_AUTHORITY, remoteWSLResolver));
|
||||
context.subscriptions.push(remoteWSLResolver);
|
||||
|
||||
const locationHistory = new RemoteLocationHistory(context);
|
||||
const locationData = getRemoteWorkspaceLocationData();
|
||||
if (locationData) {
|
||||
await locationHistory.addLocation(locationData[0], locationData[1]);
|
||||
}
|
||||
|
||||
const distroTreeDataProvider = new DistroTreeDataProvider(locationHistory, wslManager);
|
||||
context.subscriptions.push(vscode.window.createTreeView('wslTargets', { treeDataProvider: distroTreeDataProvider }));
|
||||
context.subscriptions.push(distroTreeDataProvider);
|
||||
|
||||
context.subscriptions.push(vscode.commands.registerCommand('openremotewsl.connect', () => promptOpenRemoteWSLWindow(wslManager, true, true)));
|
||||
context.subscriptions.push(vscode.commands.registerCommand('openremotewsl.connectInNewWindow', () => promptOpenRemoteWSLWindow(wslManager, true, false)));
|
||||
context.subscriptions.push(vscode.commands.registerCommand('openremotewsl.connectUsingDistro', () => promptOpenRemoteWSLWindow(wslManager, false, true)));
|
||||
context.subscriptions.push(vscode.commands.registerCommand('openremotewsl.connectUsingDistroInNewWindow', () => promptOpenRemoteWSLWindow(wslManager, false, false)));
|
||||
context.subscriptions.push(vscode.commands.registerCommand('openremotewsl.showLog', () => logger.show()));
|
||||
}
|
||||
|
||||
export function deactivate() {
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
import { REMOTE_WSL_AUTHORITY } from './authResolver';
|
||||
|
||||
export class RemoteLocationHistory {
|
||||
private static STORAGE_KEY = 'remoteLocationHistory_v0';
|
||||
|
||||
private remoteLocationHistory: Record<string, string[]> = {};
|
||||
|
||||
constructor(private context: vscode.ExtensionContext) {
|
||||
// context.globalState.update(RemoteLocationHistory.STORAGE_KEY, undefined);
|
||||
this.remoteLocationHistory = context.globalState.get(RemoteLocationHistory.STORAGE_KEY) || {};
|
||||
}
|
||||
|
||||
getHistory(host: string): string[] {
|
||||
return this.remoteLocationHistory[host] || [];
|
||||
}
|
||||
|
||||
async addLocation(host: string, path: string) {
|
||||
const hostLocations = this.remoteLocationHistory[host] || [];
|
||||
if (!hostLocations.includes(path)) {
|
||||
hostLocations.unshift(path);
|
||||
this.remoteLocationHistory[host] = hostLocations;
|
||||
|
||||
await this.context.globalState.update(RemoteLocationHistory.STORAGE_KEY, this.remoteLocationHistory);
|
||||
}
|
||||
}
|
||||
|
||||
async removeLocation(host: string, path: string) {
|
||||
let hostLocations = this.remoteLocationHistory[host] || [];
|
||||
hostLocations = hostLocations.filter(l => l !== path);
|
||||
this.remoteLocationHistory[host] = hostLocations;
|
||||
|
||||
await this.context.globalState.update(RemoteLocationHistory.STORAGE_KEY, this.remoteLocationHistory);
|
||||
}
|
||||
}
|
||||
|
||||
export function getRemoteWorkspaceLocationData(): [string, string] | undefined {
|
||||
let location = vscode.workspace.workspaceFile;
|
||||
if (location && location.scheme === 'vscode-remote' && location.authority.startsWith(REMOTE_WSL_AUTHORITY) && location.path.endsWith('.code-workspace')) {
|
||||
const [, distroName] = location.authority.split('+');
|
||||
return [distroName, location.path];
|
||||
}
|
||||
|
||||
location = vscode.workspace.workspaceFolders?.[0].uri;
|
||||
if (location && location.scheme === 'vscode-remote' && location.authority.startsWith(REMOTE_WSL_AUTHORITY)) {
|
||||
const [, distroName] = location.authority.split('+');
|
||||
return [distroName, location.path];
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
let vscodeProductJson: any;
|
||||
async function getVSCodeProductJson() {
|
||||
if (!vscodeProductJson) {
|
||||
const productJsonStr = await fs.promises.readFile(path.join(vscode.env.appRoot, 'product.json'), 'utf8');
|
||||
vscodeProductJson = JSON.parse(productJsonStr);
|
||||
}
|
||||
|
||||
return vscodeProductJson;
|
||||
}
|
||||
|
||||
export interface IServerConfig {
|
||||
version: string;
|
||||
commit: string;
|
||||
quality: string;
|
||||
release?: string; // void-like specific
|
||||
serverApplicationName: string;
|
||||
serverDataFolderName: string;
|
||||
serverDownloadUrlTemplate?: string; // void-like specific
|
||||
}
|
||||
|
||||
export async function getVSCodeServerConfig(): Promise<IServerConfig> {
|
||||
const productJson = await getVSCodeProductJson();
|
||||
|
||||
return {
|
||||
// version: vscode.version.replace('-insider', ''),
|
||||
commit: productJson.commit,
|
||||
quality: productJson.quality,
|
||||
release: productJson.release,
|
||||
serverApplicationName: productJson.serverApplicationName,
|
||||
serverDataFolderName: productJson.serverDataFolderName,
|
||||
serverDownloadUrlTemplate: productJson.serverDownloadUrlTemplate,
|
||||
// Void changed this
|
||||
version: productJson.voidVersion
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,353 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as crypto from 'crypto';
|
||||
import Log from './common/logger';
|
||||
import { getVSCodeServerConfig } from './serverConfig';
|
||||
import { WSLManager } from './wsl/wslManager';
|
||||
|
||||
export interface ServerInstallOptions {
|
||||
id: string;
|
||||
quality: string;
|
||||
commit: string;
|
||||
version: string;
|
||||
release?: string; // void specific
|
||||
extensionIds: string[];
|
||||
envVariables: string[];
|
||||
serverApplicationName: string;
|
||||
serverDataFolderName: string;
|
||||
serverDownloadUrlTemplate: string;
|
||||
}
|
||||
|
||||
export interface ServerInstallResult {
|
||||
exitCode: number;
|
||||
listeningOn: number;
|
||||
connectionToken: string;
|
||||
logFile: string;
|
||||
osReleaseId: string;
|
||||
arch: string;
|
||||
platform: string;
|
||||
tmpDir: string;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export class ServerInstallError extends Error {
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
|
||||
const DEFAULT_DOWNLOAD_URL_TEMPLATE = 'https://github.com/voideditor/binaries/releases/download/${version}.${release}/void-reh-${os}-${arch}-${version}.${release}.tar.gz';
|
||||
|
||||
export async function installCodeServer(wslManager: WSLManager, distroName: string, serverDownloadUrlTemplate: string | undefined, extensionIds: string[], envVariables: string[], logger: Log): Promise<ServerInstallResult> {
|
||||
const scriptId = crypto.randomBytes(12).toString('hex');
|
||||
|
||||
const vscodeServerConfig = await getVSCodeServerConfig();
|
||||
const installOptions: ServerInstallOptions = {
|
||||
id: scriptId,
|
||||
version: vscodeServerConfig.version,
|
||||
commit: vscodeServerConfig.commit,
|
||||
quality: vscodeServerConfig.quality,
|
||||
release: vscodeServerConfig.release,
|
||||
extensionIds,
|
||||
envVariables,
|
||||
serverApplicationName: vscodeServerConfig.serverApplicationName,
|
||||
serverDataFolderName: vscodeServerConfig.serverDataFolderName,
|
||||
serverDownloadUrlTemplate: serverDownloadUrlTemplate ?? vscodeServerConfig.serverDownloadUrlTemplate ?? DEFAULT_DOWNLOAD_URL_TEMPLATE,
|
||||
};
|
||||
|
||||
const installServerScript = generateBashInstallScript(installOptions);
|
||||
|
||||
// Fish shell does not support heredoc so let's workaround it using -c option,
|
||||
// also replace single quotes (') within the script with ('\'') as there's no quoting within single quotes, see https://unix.stackexchange.com/a/24676
|
||||
const resp = await wslManager.exec('bash', ['-c', `'${installServerScript.replace(/'/g, `'\\''`)}'`], distroName);
|
||||
|
||||
const endScriptRegex = new RegExp(`${scriptId}: Server installation script done`, 'm');
|
||||
const commandOutput = await Promise.race([
|
||||
resp.exitPromise.then(result => ({ stdout: resp.stdout, stderr: resp.stderr, exitCode: result.exitCode })),
|
||||
new Promise<{ stdout: string; stderr: string; exitCode: number }>((resolve) => {
|
||||
resp.onStdoutData(buffer => {
|
||||
if (endScriptRegex.test(buffer.toString('utf8'))) {
|
||||
resolve({ stdout: resp.stdout, stderr: resp.stderr, exitCode: 0 });
|
||||
}
|
||||
});
|
||||
})
|
||||
]);
|
||||
|
||||
if (commandOutput.exitCode) {
|
||||
logger.trace('Server install command stderr:', commandOutput.stderr);
|
||||
}
|
||||
logger.trace('Server install command stdout:', commandOutput.stdout);
|
||||
|
||||
const resultMap = parseServerInstallOutput(commandOutput.stdout, scriptId);
|
||||
if (!resultMap) {
|
||||
throw new ServerInstallError(`Failed parsing install script output`);
|
||||
}
|
||||
|
||||
const exitCode = parseInt(resultMap.exitCode, 10);
|
||||
if (exitCode !== 0) {
|
||||
throw new ServerInstallError(`Couldn't install void server on remote server, install script returned non-zero exit status`);
|
||||
}
|
||||
|
||||
const listeningOn = parseInt(resultMap.listeningOn, 10);
|
||||
|
||||
const remoteEnvVars = Object.fromEntries(Object.entries(resultMap).filter(([key,]) => envVariables.includes(key)));
|
||||
|
||||
return {
|
||||
exitCode,
|
||||
listeningOn,
|
||||
connectionToken: resultMap.connectionToken,
|
||||
logFile: resultMap.logFile,
|
||||
osReleaseId: resultMap.osReleaseId,
|
||||
arch: resultMap.arch,
|
||||
platform: resultMap.platform,
|
||||
tmpDir: resultMap.tmpDir,
|
||||
...remoteEnvVars
|
||||
};
|
||||
}
|
||||
|
||||
function parseServerInstallOutput(str: string, scriptId: string): { [k: string]: string } | undefined {
|
||||
const startResultStr = `${scriptId}: start`;
|
||||
const endResultStr = `${scriptId}: end`;
|
||||
|
||||
const startResultIdx = str.indexOf(startResultStr);
|
||||
if (startResultIdx < 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const endResultIdx = str.indexOf(endResultStr, startResultIdx + startResultStr.length);
|
||||
if (endResultIdx < 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const installResult = str.substring(startResultIdx + startResultStr.length, endResultIdx);
|
||||
|
||||
const resultMap: { [k: string]: string } = {};
|
||||
const resultArr = installResult.split(/\r?\n/);
|
||||
for (const line of resultArr) {
|
||||
const [key, value] = line.split('==');
|
||||
resultMap[key] = value;
|
||||
}
|
||||
|
||||
return resultMap;
|
||||
}
|
||||
|
||||
function generateBashInstallScript({ id, quality, version, commit, release, extensionIds, envVariables, serverApplicationName, serverDataFolderName, serverDownloadUrlTemplate }: ServerInstallOptions) {
|
||||
const extensions = extensionIds.map(id => '--install-extension ' + id).join(' ');
|
||||
return `
|
||||
# Server installation script
|
||||
|
||||
TMP_DIR="\${XDG_RUNTIME_DIR:-"/tmp"}"
|
||||
|
||||
DISTRO_VERSION="${version}"
|
||||
DISTRO_COMMIT="${commit}"
|
||||
DISTRO_QUALITY="${quality}"
|
||||
DISTRO_VSCODIUM_RELEASE="${release ?? ''}"
|
||||
|
||||
SERVER_APP_NAME="${serverApplicationName}"
|
||||
SERVER_INITIAL_EXTENSIONS="${extensions}"
|
||||
SERVER_LISTEN_FLAG="--port=0"
|
||||
SERVER_DATA_DIR="$HOME/${serverDataFolderName}"
|
||||
SERVER_DIR="$SERVER_DATA_DIR/bin/$DISTRO_COMMIT"
|
||||
SERVER_SCRIPT="$SERVER_DIR/bin/$SERVER_APP_NAME"
|
||||
SERVER_LOGFILE="$SERVER_DATA_DIR/.$DISTRO_COMMIT.log"
|
||||
SERVER_PIDFILE="$SERVER_DATA_DIR/.$DISTRO_COMMIT.pid"
|
||||
SERVER_TOKENFILE="$SERVER_DATA_DIR/.$DISTRO_COMMIT.token"
|
||||
SERVER_OS=
|
||||
SERVER_ARCH=
|
||||
SERVER_CONNECTION_TOKEN=
|
||||
SERVER_DOWNLOAD_URL=
|
||||
|
||||
LISTENING_ON=
|
||||
OS_RELEASE_ID=
|
||||
ARCH=
|
||||
PLATFORM=
|
||||
|
||||
# Mimic output from logs of remote-ssh extension
|
||||
print_install_results_and_exit() {
|
||||
echo "${id}: start"
|
||||
echo "exitCode==$1=="
|
||||
echo "listeningOn==$LISTENING_ON=="
|
||||
echo "connectionToken==$SERVER_CONNECTION_TOKEN=="
|
||||
echo "logFile==$SERVER_LOGFILE=="
|
||||
echo "osReleaseId==$OS_RELEASE_ID=="
|
||||
echo "arch==$ARCH=="
|
||||
echo "platform==$PLATFORM=="
|
||||
echo "tmpDir==$TMP_DIR=="
|
||||
${envVariables.map(envVar => `echo "${envVar}==$${envVar}=="`).join('\n')}
|
||||
echo "${id}: end"
|
||||
exit 0
|
||||
}
|
||||
|
||||
# Check if platform is supported
|
||||
PLATFORM="$(uname -s)"
|
||||
case $PLATFORM in
|
||||
Linux)
|
||||
SERVER_OS="linux"
|
||||
;;
|
||||
*)
|
||||
echo "Error platform not supported: $PLATFORM"
|
||||
print_install_results_and_exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
# Check machine architecture
|
||||
ARCH="$(uname -m)"
|
||||
case $ARCH in
|
||||
x86_64 | amd64)
|
||||
SERVER_ARCH="x64"
|
||||
;;
|
||||
armv7l | armv8l)
|
||||
SERVER_ARCH="armhf"
|
||||
;;
|
||||
arm64 | aarch64)
|
||||
SERVER_ARCH="arm64"
|
||||
;;
|
||||
*)
|
||||
echo "Error architecture not supported: $ARCH"
|
||||
print_install_results_and_exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
# https://www.freedesktop.org/software/systemd/man/os-release.html
|
||||
OS_RELEASE_ID="$(grep -i '^ID=' /etc/os-release 2>/dev/null | sed 's/^ID=//gi' | sed 's/"//g')"
|
||||
if [[ -z $OS_RELEASE_ID ]]; then
|
||||
OS_RELEASE_ID="$(grep -i '^ID=' /usr/lib/os-release 2>/dev/null | sed 's/^ID=//gi' | sed 's/"//g')"
|
||||
if [[ -z $OS_RELEASE_ID ]]; then
|
||||
OS_RELEASE_ID="unknown"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Create installation folder
|
||||
if [[ ! -d $SERVER_DIR ]]; then
|
||||
mkdir -p $SERVER_DIR
|
||||
if (( $? > 0 )); then
|
||||
echo "Error creating server install directory"
|
||||
print_install_results_and_exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
SERVER_DOWNLOAD_URL="$(echo "${serverDownloadUrlTemplate.replace(/\$\{/g, '\\${')}" | sed "s/\\\${quality}/$DISTRO_QUALITY/g" | sed "s/\\\${version}/$DISTRO_VERSION/g" | sed "s/\\\${commit}/$DISTRO_COMMIT/g" | sed "s/\\\${os}/$SERVER_OS/g" | sed "s/\\\${arch}/$SERVER_ARCH/g" | sed "s/\\\${release}/$DISTRO_VSCODIUM_RELEASE/g")"
|
||||
|
||||
# Check if server script is already installed
|
||||
if [[ ! -f $SERVER_SCRIPT ]]; then
|
||||
if [[ "$SERVER_OS" = "dragonfly" ]] || [[ "$SERVER_OS" = "freebsd" ]]; then
|
||||
echo "Error "$SERVER_OS" needs manual installation of remote extension host"
|
||||
print_install_results_and_exit 1
|
||||
fi
|
||||
|
||||
pushd $SERVER_DIR > /dev/null
|
||||
|
||||
if [[ ! -z $(which wget) ]]; then
|
||||
wget --tries=3 --timeout=10 --continue --no-verbose -O vscode-server.tar.gz $SERVER_DOWNLOAD_URL
|
||||
elif [[ ! -z $(which curl) ]]; then
|
||||
curl --retry 3 --connect-timeout 10 --location --show-error --silent --output vscode-server.tar.gz $SERVER_DOWNLOAD_URL
|
||||
else
|
||||
echo "Error no tool to download server binary"
|
||||
print_install_results_and_exit 1
|
||||
fi
|
||||
|
||||
if (( $? > 0 )); then
|
||||
echo "Error downloading server from $SERVER_DOWNLOAD_URL"
|
||||
print_install_results_and_exit 1
|
||||
fi
|
||||
|
||||
tar -xf vscode-server.tar.gz --strip-components 1
|
||||
if (( $? > 0 )); then
|
||||
echo "Error while extracting server contents"
|
||||
print_install_results_and_exit 1
|
||||
fi
|
||||
|
||||
if [[ ! -f $SERVER_SCRIPT ]]; then
|
||||
echo "Error server contents are corrupted"
|
||||
print_install_results_and_exit 1
|
||||
fi
|
||||
|
||||
rm -f vscode-server.tar.gz
|
||||
|
||||
popd > /dev/null
|
||||
else
|
||||
echo "Server script already installed in $SERVER_SCRIPT"
|
||||
fi
|
||||
|
||||
# Try to find if server is already running
|
||||
if [[ -f $SERVER_PIDFILE ]]; then
|
||||
SERVER_PID="$(cat $SERVER_PIDFILE)"
|
||||
SERVER_RUNNING_PROCESS="$(ps -o pid,args -p $SERVER_PID | grep $SERVER_SCRIPT)"
|
||||
else
|
||||
SERVER_RUNNING_PROCESS="$(ps -o pid,args -A | grep $SERVER_SCRIPT | grep -v grep)"
|
||||
fi
|
||||
|
||||
if [[ -z $SERVER_RUNNING_PROCESS ]]; then
|
||||
if [[ -f $SERVER_LOGFILE ]]; then
|
||||
rm $SERVER_LOGFILE
|
||||
fi
|
||||
if [[ -f $SERVER_TOKENFILE ]]; then
|
||||
rm $SERVER_TOKENFILE
|
||||
fi
|
||||
|
||||
touch $SERVER_TOKENFILE
|
||||
chmod 600 $SERVER_TOKENFILE
|
||||
SERVER_CONNECTION_TOKEN="${crypto.randomUUID()}"
|
||||
echo $SERVER_CONNECTION_TOKEN > $SERVER_TOKENFILE
|
||||
|
||||
$SERVER_SCRIPT --start-server --host=127.0.0.1 $SERVER_LISTEN_FLAG $SERVER_INITIAL_EXTENSIONS --connection-token-file $SERVER_TOKENFILE --telemetry-level off --use-host-proxy --disable-websocket-compression --without-browser-env-var --enable-remote-auto-shutdown --accept-server-license-terms &> $SERVER_LOGFILE &
|
||||
echo $! > $SERVER_PIDFILE
|
||||
else
|
||||
echo "Server script is already running $SERVER_SCRIPT"
|
||||
fi
|
||||
|
||||
if [[ -f $SERVER_TOKENFILE ]]; then
|
||||
SERVER_CONNECTION_TOKEN="$(cat $SERVER_TOKENFILE)"
|
||||
else
|
||||
echo "Error server token file not found $SERVER_TOKENFILE"
|
||||
print_install_results_and_exit 1
|
||||
fi
|
||||
|
||||
if [[ -f $SERVER_LOGFILE ]]; then
|
||||
for i in {1..5}; do
|
||||
LISTENING_ON="$(cat $SERVER_LOGFILE | grep -E 'Extension host agent listening on .+' | sed 's/Extension host agent listening on //')"
|
||||
if [[ -n $LISTENING_ON ]]; then
|
||||
break
|
||||
fi
|
||||
sleep 0.5
|
||||
done
|
||||
|
||||
if [[ -z $LISTENING_ON ]]; then
|
||||
echo "Error server did not start sucessfully"
|
||||
print_install_results_and_exit 1
|
||||
fi
|
||||
else
|
||||
echo "Error server log file not found $SERVER_LOGFILE"
|
||||
print_install_results_and_exit 1
|
||||
fi
|
||||
|
||||
# Finish server setup and keep script running
|
||||
if [[ -z $SERVER_RUNNING_PROCESS ]]; then
|
||||
echo "${id}: start"
|
||||
echo "exitCode==0=="
|
||||
echo "listeningOn==$LISTENING_ON=="
|
||||
echo "connectionToken==$SERVER_CONNECTION_TOKEN=="
|
||||
echo "logFile==$SERVER_LOGFILE=="
|
||||
echo "osReleaseId==$OS_RELEASE_ID=="
|
||||
echo "arch==$ARCH=="
|
||||
echo "platform==$PLATFORM=="
|
||||
echo "tmpDir==$TMP_DIR=="
|
||||
${envVariables.map(envVar => `echo "${envVar}==$${envVar}=="`).join('\n')}
|
||||
echo "${id}: end"
|
||||
|
||||
echo "${id}: Server installation script done"
|
||||
|
||||
SERVER_PID="$(cat $SERVER_PIDFILE)"
|
||||
SERVER_RUNNING_PROCESS="$(ps -o pid,args -p $SERVER_PID | grep $SERVER_SCRIPT)"
|
||||
while [[ -n $SERVER_RUNNING_PROCESS ]]; do
|
||||
sleep 300;
|
||||
SERVER_RUNNING_PROCESS="$(ps -o pid,args -p $SERVER_PID | grep $SERVER_SCRIPT)"
|
||||
done
|
||||
else
|
||||
print_install_results_and_exit 0
|
||||
fi
|
||||
`;
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as cp from 'child_process';
|
||||
import Log from '../common/logger';
|
||||
import { EventEmitter } from '../common/event';
|
||||
|
||||
const wslBinary = 'wsl.exe';
|
||||
|
||||
export interface WSLDistro {
|
||||
isDefault: boolean;
|
||||
name: string;
|
||||
state: string;
|
||||
version: string;
|
||||
}
|
||||
|
||||
export interface WSLOnlineDistro {
|
||||
name: string;
|
||||
friendlyName: string;
|
||||
}
|
||||
|
||||
export class WSLManager {
|
||||
constructor(private readonly logger: Log) {
|
||||
}
|
||||
|
||||
async listDistros() {
|
||||
const resp = this._runWSLCommand(['--list', '--verbose'], 'utf16le');
|
||||
const { exitCode } = await resp.exitPromise;
|
||||
const { stdout, stderr } = resp;
|
||||
if (exitCode) {
|
||||
this.logger.trace(`Command wsl listDistros exited with code ${exitCode}`, stdout + '\n\n' + stderr);
|
||||
throw new Error(`Command wsl listDistros exited with code ${exitCode}`);
|
||||
}
|
||||
|
||||
const regex = /(?<default>\*|\s)\s+(?<name>[\w\.-]+)\s+(?<state>[\w]+)\s+(?<version>\d)/;
|
||||
const distros: WSLDistro[] = [];
|
||||
for (const line of stdout.split(/\r?\n/)) {
|
||||
const matches = line.match(regex);
|
||||
if (matches && matches.groups) {
|
||||
distros.push({
|
||||
isDefault: matches.groups.default === '*',
|
||||
name: matches.groups.name,
|
||||
state: matches.groups.state,
|
||||
version: matches.groups.version,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return distros;
|
||||
}
|
||||
|
||||
async listOnlineDistros() {
|
||||
const resp = this._runWSLCommand(['--list', '--online'], 'utf16le');
|
||||
const { exitCode } = await resp.exitPromise;
|
||||
const { stdout, stderr } = resp;
|
||||
if (exitCode) {
|
||||
this.logger.trace(`Command wsl listOnlineDistros exited with code ${exitCode}`, stdout + '\n\n' + stderr);
|
||||
throw new Error(`Command wsl listOnlineDistros exited with code ${exitCode}`);
|
||||
}
|
||||
|
||||
let lines = stdout.split(/\r?\n/);
|
||||
const idx = lines.findIndex(l => /\s*NAME\s+FRIENDLY NAME\s*/.test(l));
|
||||
lines = lines.slice(idx + 1);
|
||||
|
||||
const regex = /(?<name>[\w\.-]+)\s+(?<friendlyName>\w.+\w)/;
|
||||
const distros: WSLOnlineDistro[] = [];
|
||||
for (const line of lines) {
|
||||
const matches = line.match(regex);
|
||||
if (matches && matches.groups) {
|
||||
distros.push({
|
||||
name: matches.groups.name,
|
||||
friendlyName: matches.groups.friendlyName,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return distros;
|
||||
}
|
||||
|
||||
async setDefaultDistro(distroName: string) {
|
||||
const resp = this._runWSLCommand(['--set-default', distroName], 'utf16le');
|
||||
const { exitCode } = await resp.exitPromise;
|
||||
const { stdout, stderr } = resp;
|
||||
if (exitCode) {
|
||||
this.logger.trace(`Command wsl setDefaultDistro exited with code ${exitCode}`, stdout + '\n\n' + stderr);
|
||||
throw new Error(`Command wsl setDefaultDistro exited with code ${exitCode}`);
|
||||
}
|
||||
}
|
||||
|
||||
async deleteDistro(distroName: string) {
|
||||
const resp = this._runWSLCommand(['--unregister', distroName], 'utf16le');
|
||||
const { exitCode } = await resp.exitPromise;
|
||||
const { stdout, stderr } = resp;
|
||||
if (exitCode) {
|
||||
this.logger.trace(`Command wsl deleteDistro exited with code ${exitCode}`, stdout + '\n\n' + stderr);
|
||||
throw new Error(`Command wsl deleteDistro exited with code ${exitCode}`);
|
||||
}
|
||||
}
|
||||
|
||||
async exec(cmd: string, args: string[], distro: string) {
|
||||
return this._runWSLCommand(['--distribution', distro, '--', cmd, ...args], 'utf8');
|
||||
}
|
||||
|
||||
private _runWSLCommand(args: string[], encoding: 'utf8' | 'utf16le') {
|
||||
this.logger.trace(`Running WSL command: ${wslBinary} ${args.join(' ')}`);
|
||||
|
||||
const cmd = cp.spawn(wslBinary, args, { windowsHide: true, windowsVerbatimArguments: true });
|
||||
|
||||
const stdoutDataEmitter = new EventEmitter<Buffer>();
|
||||
const stdoutData: Buffer[] = [];
|
||||
const stderrDataEmitter = new EventEmitter<Buffer>();
|
||||
const stderrData: Buffer[] = [];
|
||||
cmd.stdout.on('data', (data: Buffer) => {
|
||||
stdoutData.push(data);
|
||||
stdoutDataEmitter.fire(data);
|
||||
});
|
||||
cmd.stderr.on('data', (data: Buffer) => {
|
||||
stderrData.push(data);
|
||||
stderrDataEmitter.fire(data);
|
||||
});
|
||||
|
||||
const exitPromise = new Promise<{ exitCode: number }>((resolve, reject) => {
|
||||
cmd.on('error', (err) => {
|
||||
this.logger.error(`Error running WSL command: ${wslBinary} ${args.join(' ')}`, err);
|
||||
reject(err);
|
||||
});
|
||||
cmd.on('exit', (code, _signal) => {
|
||||
resolve({ exitCode: code ?? 0 });
|
||||
});
|
||||
});
|
||||
|
||||
return {
|
||||
get stdout() {
|
||||
return Buffer.concat(stdoutData).toString(encoding);
|
||||
},
|
||||
get stderr() {
|
||||
return Buffer.concat(stderrData).toString(encoding);
|
||||
},
|
||||
get onStdoutData() {
|
||||
return stdoutDataEmitter.event;
|
||||
},
|
||||
get onStderrData() {
|
||||
return stderrDataEmitter.event;
|
||||
},
|
||||
exitPromise
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
|
||||
class WSLTerminal {
|
||||
static NAME = 'WSL';
|
||||
|
||||
private getTerminal() {
|
||||
const wslTerminal = vscode.window.terminals.find(t => t.name === WSLTerminal.NAME);
|
||||
if (wslTerminal) {
|
||||
return wslTerminal;
|
||||
}
|
||||
return vscode.window.createTerminal(WSLTerminal.NAME);
|
||||
}
|
||||
|
||||
runCommand(command: string) {
|
||||
const wslTerminal = this.getTerminal();
|
||||
wslTerminal.show(false);
|
||||
wslTerminal.sendText(command, true);
|
||||
}
|
||||
}
|
||||
|
||||
export default new WSLTerminal();
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"extends": "../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./out",
|
||||
},
|
||||
"include": [
|
||||
"src/**/*",
|
||||
"../../src/vscode-dts/vscode.d.ts",
|
||||
"../../src/vscode-dts/vscode.proposed.resolvers.d.ts",
|
||||
"../../src/vscode-dts/vscode.proposed.contribViewsRemote.d.ts",
|
||||
]
|
||||
}
|
||||
Generated
+44
-14
@@ -62,6 +62,7 @@
|
||||
"posthog-node": "^4.8.1",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-tooltip": "^5.28.1",
|
||||
"tas-client-umd": "0.2.0",
|
||||
"v8-inspect-profiler": "^0.1.1",
|
||||
"vscode-html-languageservice": "^5.3.1",
|
||||
@@ -117,7 +118,7 @@
|
||||
"cssnano": "^6.0.3",
|
||||
"debounce": "^1.0.0",
|
||||
"deemon": "^1.8.0",
|
||||
"electron": "34.2.0",
|
||||
"electron": "34.3.2",
|
||||
"eslint": "^9.11.1",
|
||||
"eslint-formatter-compact": "^8.40.0",
|
||||
"eslint-plugin-header": "3.1.1",
|
||||
@@ -3587,6 +3588,13 @@
|
||||
"tslib": "^2.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/helpers/node_modules/tslib": {
|
||||
"version": "2.8.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||
"dev": true,
|
||||
"license": "0BSD"
|
||||
},
|
||||
"node_modules/@szmarczak/http-timer": {
|
||||
"version": "4.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz",
|
||||
@@ -6614,6 +6622,12 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/classnames": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz",
|
||||
"integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/cli-cursor": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-4.0.0.tgz",
|
||||
@@ -8001,9 +8015,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/electron": {
|
||||
"version": "34.2.0",
|
||||
"resolved": "https://registry.npmjs.org/electron/-/electron-34.2.0.tgz",
|
||||
"integrity": "sha512-SYwBJNeXBTm1q/ErybQMUBZAYqEreBUqBwTrNkw1rV4YatDZk5Aittpcus3PPeC4UoI/tqmJ946uG8AKHTd6CA==",
|
||||
"version": "34.3.2",
|
||||
"resolved": "https://registry.npmjs.org/electron/-/electron-34.3.2.tgz",
|
||||
"integrity": "sha512-n9tzmFexVLxipZXwMTY30H10f0X9k2OP0SkpSwL5VvnDZi0l/Hc+8CEArKkQPbbSf/IS7nxgc96gtTaR+XoSBg==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
@@ -15087,10 +15101,11 @@
|
||||
}
|
||||
},
|
||||
"node_modules/nan": {
|
||||
"version": "2.14.2",
|
||||
"resolved": "https://registry.npmjs.org/nan/-/nan-2.14.2.tgz",
|
||||
"integrity": "sha512-M2ufzIiINKCuDfBSAUr1vWQ+vuVcA9kqx8JJUsbQi6yf1uGRyb7HfpdfUr5qLXf3B/t8dPvcjhKMmlfnP47EzQ==",
|
||||
"version": "2.22.2",
|
||||
"resolved": "https://registry.npmjs.org/nan/-/nan-2.22.2.tgz",
|
||||
"integrity": "sha512-DANghxFkS1plDdRsX0X9pm0Z6SJNN6gBdtXfanwoZ8hooC5gosGFSBGRYHUVPz1asKA/kMRqDRdHrluZ61SpBQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/nanoid": {
|
||||
@@ -17992,6 +18007,20 @@
|
||||
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/react-tooltip": {
|
||||
"version": "5.28.1",
|
||||
"resolved": "https://registry.npmjs.org/react-tooltip/-/react-tooltip-5.28.1.tgz",
|
||||
"integrity": "sha512-ZA4oHwoIIK09TS7PvSLFcRlje1wGZaxw6xHvfrzn6T82UcMEfEmHVCad16Gnr4NDNDh93HyN037VK4HDi5odfQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@floating-ui/dom": "^1.6.1",
|
||||
"classnames": "^2.3.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.14.0",
|
||||
"react-dom": ">=16.14.0"
|
||||
}
|
||||
},
|
||||
"node_modules/read-cache": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
|
||||
@@ -18800,10 +18829,11 @@
|
||||
}
|
||||
},
|
||||
"node_modules/sax": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz",
|
||||
"integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==",
|
||||
"dev": true
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/sax/-/sax-1.4.1.tgz",
|
||||
"integrity": "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/scheduler": {
|
||||
"version": "0.25.0",
|
||||
@@ -21443,9 +21473,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/tslib": {
|
||||
"version": "2.8.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||
"version": "2.6.3",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz",
|
||||
"integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==",
|
||||
"dev": true,
|
||||
"license": "0BSD"
|
||||
},
|
||||
|
||||
+3
-2
@@ -69,7 +69,7 @@
|
||||
"extensions-ci-pr": "node ./node_modules/gulp/bin/gulp.js extensions-ci-pr",
|
||||
"perf": "node scripts/code-perf.js",
|
||||
"update-build-ts-version": "npm install typescript@next && tsc -p ./build/tsconfig.build.json"
|
||||
},
|
||||
},
|
||||
"dependencies": {
|
||||
"@anthropic-ai/sdk": "^0.39.0",
|
||||
"@floating-ui/react": "^0.27.5",
|
||||
@@ -123,6 +123,7 @@
|
||||
"posthog-node": "^4.8.1",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-tooltip": "^5.28.1",
|
||||
"tas-client-umd": "0.2.0",
|
||||
"v8-inspect-profiler": "^0.1.1",
|
||||
"vscode-html-languageservice": "^5.3.1",
|
||||
@@ -178,7 +179,7 @@
|
||||
"cssnano": "^6.0.3",
|
||||
"debounce": "^1.0.0",
|
||||
"deemon": "^1.8.0",
|
||||
"electron": "34.2.0",
|
||||
"electron": "34.3.2",
|
||||
"eslint": "^9.11.1",
|
||||
"eslint-formatter-compact": "^8.40.0",
|
||||
"eslint-plugin-header": "3.1.1",
|
||||
|
||||
+6
-2
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"nameShort": "Void",
|
||||
"nameLong": "Void",
|
||||
"voidVersion": "1.0.2",
|
||||
"voidVersion": "1.2.1",
|
||||
"applicationName": "void",
|
||||
"dataFolderName": ".void-editor",
|
||||
"win32MutexName": "voideditor",
|
||||
@@ -35,5 +35,9 @@
|
||||
"serviceUrl": "https://marketplace.visualstudio.com/_apis/public/gallery",
|
||||
"itemUrl": "https://marketplace.visualstudio.com/items"
|
||||
},
|
||||
"builtInExtensions": []
|
||||
"builtInExtensions": [],
|
||||
"linkProtectionTrustedDomains": [
|
||||
"https://voideditor.com",
|
||||
"https://voideditor.dev"
|
||||
]
|
||||
}
|
||||
|
||||
+1231
-1039
File diff suppressed because it is too large
Load Diff
Binary file not shown.
|
Before Width: | Height: | Size: 173 KiB After Width: | Height: | Size: 200 KiB |
@@ -3,6 +3,9 @@
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
// This bootstrap-fork module handles the initialization of a forked process in VS Code.
|
||||
// It sets up logging, exception handling, and loads the ESM module system.
|
||||
|
||||
import * as performance from './vs/base/common/performance.js';
|
||||
import { removeGlobalNodeJsModuleLookupPaths, devInjectNodeModuleLookupPath } from './bootstrap-node.js';
|
||||
import { bootstrapESM } from './bootstrap-esm.js';
|
||||
|
||||
+14
@@ -6,6 +6,7 @@
|
||||
import * as path from 'path';
|
||||
import * as fs from 'original-fs';
|
||||
import * as os from 'os';
|
||||
import { createRequire } from 'node:module';
|
||||
import { performance } from 'perf_hooks';
|
||||
import { configurePortable } from './bootstrap-node.js';
|
||||
import { bootstrapESM } from './bootstrap-esm.js';
|
||||
@@ -22,6 +23,7 @@ import { INLSConfiguration } from './vs/nls.js';
|
||||
import { NativeParsedArgs } from './vs/platform/environment/common/argv.js';
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const require = createRequire(import.meta.url);
|
||||
|
||||
perf.mark('code/didStartMain');
|
||||
|
||||
@@ -110,6 +112,17 @@ protocol.registerSchemesAsPrivileged([
|
||||
// Global app listeners
|
||||
registerListeners();
|
||||
|
||||
function resolveUserProduct() {
|
||||
const userProductPath = path.join(userDataPath, 'product.json');
|
||||
try {
|
||||
// Assign the product configuration to the global scope
|
||||
const productJson = require(userProductPath);
|
||||
// @ts-expect-error
|
||||
globalThis._VSCODE_USER_PRODUCT_JSON = productJson;
|
||||
} catch (ex) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* We can resolve the NLS configuration early if it is defined
|
||||
* in argv.json before `app.ready` event. Otherwise we can only
|
||||
@@ -206,6 +219,7 @@ async function onReady() {
|
||||
async function startup(codeCachePath: string | undefined, nlsConfig: INLSConfiguration): Promise<void> {
|
||||
process.env['VSCODE_NLS_CONFIG'] = JSON.stringify(nlsConfig);
|
||||
process.env['VSCODE_CODE_CACHE_PATH'] = codeCachePath || '';
|
||||
resolveUserProduct();
|
||||
|
||||
// Bootstrap ESM
|
||||
await bootstrapESM();
|
||||
|
||||
@@ -56,7 +56,8 @@ export type ExtensionVirtualWorkspaceSupport = {
|
||||
|
||||
export interface IProductConfiguration {
|
||||
readonly version: string;
|
||||
readonly voidVersion?: string; // Void added this
|
||||
readonly voidVersion: string; // Void added this
|
||||
readonly release: string; // VSCodium added this
|
||||
readonly date?: string;
|
||||
readonly quality?: string;
|
||||
readonly commit?: string;
|
||||
|
||||
@@ -121,6 +121,93 @@ export class SmartSelectController implements IEditorContribution {
|
||||
}
|
||||
this._state = this._state.map(state => state.mov(forward));
|
||||
const newSelections = this._state.map(state => Selection.fromPositions(state.ranges[state.index].getStartPosition(), state.ranges[state.index].getEndPosition()));
|
||||
|
||||
// Void changed this to skip over added whitespace when using smartSelect
|
||||
// // Store the original selections for comparison
|
||||
// const originalSelections = selections;
|
||||
|
||||
// // Keep skipping while we're only adding/removing whitespace
|
||||
// let keepSkipping = true;
|
||||
// let skipCount = 0;
|
||||
// const MAX_SKIPS = 5; // Avoid infinite loops by setting a reasonable limit
|
||||
|
||||
// while (keepSkipping && skipCount < MAX_SKIPS) {
|
||||
// keepSkipping = false; // Reset for each iteration
|
||||
|
||||
// // Check if all selections only added/removed whitespace
|
||||
// if (originalSelections.length === newSelections.length) {
|
||||
// for (let i = 0; i < originalSelections.length; i++) {
|
||||
// const oldSel = originalSelections[i];
|
||||
// const newSel = newSelections[i];
|
||||
|
||||
// if (forward) { // For expanding (^+Shift+Right)
|
||||
// // Skip if only whitespace was added
|
||||
// const oldText = model.getValueInRange(oldSel).trim();
|
||||
// const newText = model.getValueInRange(newSel).trim();
|
||||
// const onlyWhitespaceAdded = oldText === newText && oldText.length > 0;
|
||||
|
||||
// if (onlyWhitespaceAdded) {
|
||||
// console.log(`SMART SELECT - SKIPPING (EXPAND) [${skipCount + 1}]:`, {
|
||||
// reason: 'only whitespace added',
|
||||
// oldText: model.getValueInRange(oldSel),
|
||||
// newText: model.getValueInRange(newSel)
|
||||
// });
|
||||
// keepSkipping = true;
|
||||
// break;
|
||||
// }
|
||||
// } else { // For shrinking (^+Shift+Left)
|
||||
// // Skip if only whitespace was removed
|
||||
// const oldText = model.getValueInRange(oldSel).trim();
|
||||
// const newText = model.getValueInRange(newSel).trim();
|
||||
// const onlyWhitespaceRemoved = oldText === newText && newText.length > 0;
|
||||
|
||||
// if (onlyWhitespaceRemoved) {
|
||||
// console.log(`SMART SELECT - SKIPPING (SHRINK) [${skipCount + 1}]:`, {
|
||||
// reason: 'only whitespace removed',
|
||||
// oldText: model.getValueInRange(oldSel),
|
||||
// newText: model.getValueInRange(newSel)
|
||||
// });
|
||||
// keepSkipping = true;
|
||||
// break;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// // If we need to skip, move one more time
|
||||
// if (keepSkipping) {
|
||||
// skipCount++;
|
||||
|
||||
// // Try to move to the next range
|
||||
// const prevState = this._state;
|
||||
// this._state = this._state.map(state => state.mov(forward));
|
||||
|
||||
// // Check if we've reached the end of available ranges
|
||||
// const stateUnchanged = this._state.every((state, idx) =>
|
||||
// state.index === prevState[idx].index
|
||||
// );
|
||||
|
||||
// if (stateUnchanged) {
|
||||
// // We can't move any further, so stop skipping
|
||||
// keepSkipping = false;
|
||||
// } else {
|
||||
// // Update selections for the next iteration
|
||||
// newSelections = this._state.map(state => Selection.fromPositions(
|
||||
// state.ranges[state.index].getStartPosition(),
|
||||
// state.ranges[state.index].getEndPosition()
|
||||
// ));
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// // Print AFTER selection (before actually setting it)
|
||||
// console.log('SMART SELECT - AFTER:', newSelections.map(s => {
|
||||
// return {
|
||||
// range: `(${s.startLineNumber},${s.startColumn}) -> (${s.endLineNumber},${s.endColumn})`,
|
||||
// text: model.getValueInRange(s)
|
||||
// };
|
||||
// }));
|
||||
|
||||
this._ignoreSelection = true;
|
||||
try {
|
||||
this._editor.setSelections(newSelections);
|
||||
|
||||
@@ -230,7 +230,7 @@ export class DiagnosticsService implements IDiagnosticsService {
|
||||
|
||||
private formatEnvironment(info: IMainProcessDiagnostics): string {
|
||||
const output: string[] = [];
|
||||
output.push(`Version: ${this.productService.nameShort} ${this.productService.version} (${this.productService.commit || 'Commit unknown'}, ${this.productService.date || 'Date unknown'})`);
|
||||
output.push(`Version: ${this.productService.nameShort} ${this.productService.version} ${this.productService.release || 'Release unknown'} (${this.productService.commit || 'Commit unknown'}, ${this.productService.date || 'Date unknown'})`);
|
||||
output.push(`OS Version: ${osLib.type()} ${osLib.arch()} ${osLib.release()}`);
|
||||
const cpus = osLib.cpus();
|
||||
if (cpus && cpus.length > 0) {
|
||||
|
||||
@@ -52,7 +52,6 @@ import { IProductService } from '../../product/common/productService.js';
|
||||
import { ITelemetryService } from '../../telemetry/common/telemetry.js';
|
||||
import { IUriIdentityService } from '../../uriIdentity/common/uriIdentity.js';
|
||||
import { IUserDataProfilesService } from '../../userDataProfile/common/userDataProfile.js';
|
||||
import { IConfigurationService } from '../../configuration/common/configuration.js';
|
||||
import { isLinux } from '../../../base/common/platform.js';
|
||||
|
||||
export const INativeServerExtensionManagementService = refineServiceDecorator<IExtensionManagementService, INativeServerExtensionManagementService>(IExtensionManagementService);
|
||||
@@ -85,7 +84,6 @@ export class ExtensionManagementService extends AbstractExtensionManagementServi
|
||||
@IDownloadService private downloadService: IDownloadService,
|
||||
@IInstantiationService private readonly instantiationService: IInstantiationService,
|
||||
@IFileService private readonly fileService: IFileService,
|
||||
@IConfigurationService private readonly configurationService: IConfigurationService,
|
||||
@IProductService productService: IProductService,
|
||||
@IAllowedExtensionsService allowedExtensionsService: IAllowedExtensionsService,
|
||||
@IUriIdentityService uriIdentityService: IUriIdentityService,
|
||||
@@ -324,8 +322,7 @@ export class ExtensionManagementService extends AbstractExtensionManagementServi
|
||||
|
||||
private async downloadExtension(extension: IGalleryExtension, operation: InstallOperation, verifySignature: boolean, clientTargetPlatform?: TargetPlatform): Promise<{ readonly location: URI; readonly verificationStatus: ExtensionSignatureVerificationCode | undefined }> {
|
||||
if (verifySignature) {
|
||||
const value = this.configurationService.getValue('extensions.verifySignature');
|
||||
verifySignature = isBoolean(value) ? value : true;
|
||||
verifySignature = false; // Void disable-signature-verification-patch
|
||||
}
|
||||
const { location, verificationStatus } = await this.extensionsDownloader.download(extension, operation, verifySignature, clientTargetPlatform);
|
||||
|
||||
|
||||
@@ -404,7 +404,7 @@ export class NativeHostMainService extends Disposable implements INativeHostMain
|
||||
}
|
||||
|
||||
private async getShellCommandLink(): Promise<{ readonly source: string; readonly target: string }> {
|
||||
const target = resolve(this.environmentMainService.appRoot, 'bin', 'code');
|
||||
const target = resolve(this.environmentMainService.appRoot, 'bin', this.productService.applicationName);
|
||||
const source = `/usr/local/bin/${this.productService.applicationName}`;
|
||||
|
||||
// Ensure source exists
|
||||
@@ -639,7 +639,7 @@ export class NativeHostMainService extends Disposable implements INativeHostMain
|
||||
|
||||
// macOS
|
||||
if (this.environmentMainService.isBuilt) {
|
||||
return join(this.environmentMainService.appRoot, 'bin', 'code');
|
||||
return join(this.environmentMainService.appRoot, 'bin', `${this.productService.applicationName}`);
|
||||
}
|
||||
|
||||
return join(this.environmentMainService.appRoot, 'scripts', 'code-cli.sh');
|
||||
|
||||
@@ -43,10 +43,11 @@ else if (globalThis._VSCODE_PRODUCT_JSON && globalThis._VSCODE_PACKAGE_JSON) {
|
||||
// want to have it running out of sources so we
|
||||
// read it from package.json only when we need it.
|
||||
if (!product.version) {
|
||||
const pkg = globalThis._VSCODE_PACKAGE_JSON as { version: string };
|
||||
const pkg = globalThis._VSCODE_PACKAGE_JSON as { version: string, release: string };
|
||||
|
||||
Object.assign(product, {
|
||||
version: pkg.version
|
||||
version: pkg.version,
|
||||
release: pkg.release
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,7 +49,8 @@ export const enum StateType {
|
||||
export const enum UpdateType {
|
||||
Setup,
|
||||
Archive,
|
||||
Snap
|
||||
Snap,
|
||||
WindowsInstaller
|
||||
}
|
||||
|
||||
export const enum DisablementReason {
|
||||
@@ -108,3 +109,41 @@ export interface IUpdateService {
|
||||
isLatestVersion(): Promise<boolean | undefined>;
|
||||
_applySpecificUpdate(packagePath: string): Promise<void>;
|
||||
}
|
||||
|
||||
|
||||
export type Architecture =
|
||||
| 'arm'
|
||||
| 'arm64'
|
||||
| 'ia32'
|
||||
| 'loong64'
|
||||
| 'mips'
|
||||
| 'mipsel'
|
||||
| 'ppc'
|
||||
| 'ppc64'
|
||||
| 'riscv64'
|
||||
| 's390'
|
||||
| 's390x'
|
||||
| 'x64';
|
||||
|
||||
export type Platform =
|
||||
| 'aix'
|
||||
| 'android'
|
||||
| 'darwin'
|
||||
| 'freebsd'
|
||||
| 'haiku'
|
||||
| 'linux'
|
||||
| 'openbsd'
|
||||
| 'sunos'
|
||||
| 'win32'
|
||||
| 'cygwin'
|
||||
| 'netbsd';
|
||||
|
||||
export type Quality =
|
||||
| 'insider'
|
||||
| 'stable';
|
||||
|
||||
export type Target =
|
||||
| 'archive'
|
||||
| 'msi'
|
||||
| 'system'
|
||||
| 'user';
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
// import { timeout } from '../../../base/common/async.js';
|
||||
import { timeout } from '../../../base/common/async.js';
|
||||
import { CancellationToken } from '../../../base/common/cancellation.js';
|
||||
import { Emitter, Event } from '../../../base/common/event.js';
|
||||
import { IConfigurationService } from '../../configuration/common/configuration.js';
|
||||
@@ -13,13 +14,16 @@ import { ILifecycleMainService, LifecycleMainPhase } from '../../lifecycle/elect
|
||||
import { ILogService } from '../../log/common/log.js';
|
||||
import { IProductService } from '../../product/common/productService.js';
|
||||
import { IRequestService } from '../../request/common/request.js';
|
||||
import { AvailableForDownload, DisablementReason, IUpdateService, State, StateType, UpdateType } from '../common/update.js';
|
||||
import { Architecture, AvailableForDownload, DisablementReason, IUpdateService, Platform, State, StateType, Target, UpdateType } from '../common/update.js';
|
||||
|
||||
export function createUpdateURL(platform: string, quality: string, productService: IProductService): string {
|
||||
// return `https://voideditor.dev/api/update/${platform}/stable`;
|
||||
// return `${productService.updateUrl}/api/update/${platform}/${quality}/${productService.commit}`;
|
||||
// https://github.com/VSCodium/update-api
|
||||
return `https://updates.voideditor.dev/api/update/${platform}/${quality}/${productService.commit}`;
|
||||
// Void - VSCodium's version-1-update.patch
|
||||
export function createUpdateURL(productService: IProductService, quality: string, platform: Platform, architecture: Architecture, target?: Target): string { // return `https://voideditor.dev/api/update/${platform}/stable`;
|
||||
if (target) {
|
||||
return `${productService.updateUrl}/${quality}/${platform}/${architecture}/${target}/latest.json`;
|
||||
} else { // we shouldn't usually have a target:
|
||||
// https://raw.githubusercontent.com/voideditor/versions/refs/heads/main/stable/darwin/arm64/latest.json
|
||||
return `${productService.updateUrl}/${quality}/${platform}/${architecture}/latest.json`;
|
||||
}
|
||||
}
|
||||
|
||||
export type UpdateErrorClassification = {
|
||||
@@ -81,22 +85,20 @@ export abstract class AbstractUpdateService implements IUpdateService {
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState(State.Disabled(DisablementReason.ManuallyDisabled));
|
||||
// Void - re-enabled auto updates
|
||||
// this.setState(State.Disabled(DisablementReason.ManuallyDisabled));
|
||||
|
||||
|
||||
// Void - temporarily disabled while we figure out how to do this the right way
|
||||
|
||||
// this.setState(State.Idle(this.getUpdateType()));
|
||||
this.setState(State.Idle(this.getUpdateType()));
|
||||
|
||||
// start checking for updates after 10 seconds
|
||||
// this.scheduleCheckForUpdates(10 * 1000).then(undefined, err => this.logService.error(err));
|
||||
this.scheduleCheckForUpdates(10 * 1000).then(undefined, err => this.logService.error(err));
|
||||
}
|
||||
|
||||
// private async scheduleCheckForUpdates(delay = 60 * 60 * 1000): Promise<void> {
|
||||
// await timeout(delay);
|
||||
// await this.checkForUpdates(false);
|
||||
// return await this.scheduleCheckForUpdates(60 * 60 * 1000);
|
||||
// }
|
||||
private async scheduleCheckForUpdates(delay = 60 * 60 * 1000): Promise<void> {
|
||||
await timeout(delay);
|
||||
await this.checkForUpdates(false);
|
||||
return await this.scheduleCheckForUpdates(60 * 60 * 1000);
|
||||
}
|
||||
|
||||
async checkForUpdates(explicit: boolean): Promise<void> {
|
||||
this.logService.trace('update#checkForUpdates, state = ', this.state.type);
|
||||
|
||||
@@ -13,11 +13,12 @@ import { IEnvironmentMainService } from '../../environment/electron-main/environ
|
||||
import { ILifecycleMainService, IRelaunchHandler, IRelaunchOptions } from '../../lifecycle/electron-main/lifecycleMainService.js';
|
||||
import { ILogService } from '../../log/common/log.js';
|
||||
import { IProductService } from '../../product/common/productService.js';
|
||||
import { IRequestService } from '../../request/common/request.js';
|
||||
import { IRequestService, asJson } from '../../request/common/request.js';
|
||||
import { ITelemetryService } from '../../telemetry/common/telemetry.js';
|
||||
import { IUpdate, State, StateType, UpdateType } from '../common/update.js';
|
||||
import { AbstractUpdateService, createUpdateURL, UpdateErrorClassification } from './abstractUpdateService.js';
|
||||
|
||||
import { CancellationToken } from '../../../base/common/cancellation.js';
|
||||
import * as semver from 'semver';
|
||||
export class DarwinUpdateService extends AbstractUpdateService implements IRelaunchHandler {
|
||||
|
||||
private readonly disposables = new DisposableStore();
|
||||
@@ -75,13 +76,7 @@ export class DarwinUpdateService extends AbstractUpdateService implements IRelau
|
||||
|
||||
// Void: buildUpdateFeedUrl -> doBuildUpdateFeedUrl
|
||||
protected doBuildUpdateFeedUrl(quality: string): string | undefined {
|
||||
let assetID: string;
|
||||
if (!this.productService.darwinUniversalAssetId) {
|
||||
assetID = process.arch === 'x64' ? 'darwin' : 'darwin-arm64';
|
||||
} else {
|
||||
assetID = this.productService.darwinUniversalAssetId;
|
||||
}
|
||||
const url = createUpdateURL(assetID, quality, this.productService);
|
||||
const url = createUpdateURL(this.productService, quality, process.platform, process.arch);
|
||||
try {
|
||||
electron.autoUpdater.setFeedURL({ url });
|
||||
} catch (e) {
|
||||
@@ -93,8 +88,36 @@ export class DarwinUpdateService extends AbstractUpdateService implements IRelau
|
||||
}
|
||||
|
||||
protected doCheckForUpdates(context: any): void {
|
||||
if (!this.url) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState(State.CheckingForUpdates(context));
|
||||
electron.autoUpdater.checkForUpdates();
|
||||
// electron.autoUpdater.checkForUpdates();
|
||||
|
||||
this.requestService.request({ url: this.url }, CancellationToken.None)
|
||||
.then<IUpdate | null>(asJson)
|
||||
.then(update => {
|
||||
if (!update || !update.url || !update.version || !update.productVersion) {
|
||||
this.setState(State.Idle(UpdateType.Setup));
|
||||
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
|
||||
const fetchedVersion = update.productVersion.replace(/(\d+\.\d+\.\d+)(?:\.(\d+))(\-\w+)?/, '$1$3+$2');
|
||||
const currentVersion = `${this.productService.voidVersion}+${this.productService.release}`;
|
||||
// Void compares voidVersion, not VSCode version
|
||||
// const currentVersion = `${this.productService.version}+${this.productService.release}`;
|
||||
|
||||
if (semver.compareBuild(currentVersion, fetchedVersion) >= 0) {
|
||||
this.setState(State.Idle(UpdateType.Setup));
|
||||
}
|
||||
else {
|
||||
electron.autoUpdater.checkForUpdates();
|
||||
}
|
||||
|
||||
return Promise.resolve(null);
|
||||
})
|
||||
}
|
||||
|
||||
private onUpdateAvailable(): void {
|
||||
|
||||
@@ -30,7 +30,7 @@ export class LinuxUpdateService extends AbstractUpdateService {
|
||||
|
||||
// Void: buildUpdateFeedUrl -> doBuildUpdateFeedUrl
|
||||
protected doBuildUpdateFeedUrl(quality: string): string {
|
||||
return createUpdateURL(`linux-${process.arch}`, quality, this.productService);
|
||||
return createUpdateURL(this.productService, quality, process.platform, process.arch);
|
||||
}
|
||||
|
||||
protected doCheckForUpdates(context: any): void {
|
||||
|
||||
@@ -9,7 +9,6 @@ import { tmpdir } from 'os';
|
||||
import { timeout } from '../../../base/common/async.js';
|
||||
import { CancellationToken } from '../../../base/common/cancellation.js';
|
||||
import { memoize } from '../../../base/common/decorators.js';
|
||||
import { hash } from '../../../base/common/hash.js';
|
||||
import * as path from '../../../base/common/path.js';
|
||||
import { URI } from '../../../base/common/uri.js';
|
||||
import { checksum } from '../../../base/node/crypto.js';
|
||||
@@ -23,8 +22,9 @@ import { INativeHostMainService } from '../../native/electron-main/nativeHostMai
|
||||
import { IProductService } from '../../product/common/productService.js';
|
||||
import { asJson, IRequestService } from '../../request/common/request.js';
|
||||
import { ITelemetryService } from '../../telemetry/common/telemetry.js';
|
||||
import { AvailableForDownload, DisablementReason, IUpdate, State, StateType, UpdateType } from '../common/update.js';
|
||||
import { AbstractUpdateService, createUpdateURL, UpdateErrorClassification } from './abstractUpdateService.js';
|
||||
import { AvailableForDownload, DisablementReason, IUpdate, State, StateType, Target, UpdateType } from '../common/update.js';
|
||||
import { AbstractUpdateService, createUpdateURL } from './abstractUpdateService.js';
|
||||
import * as semver from 'semver';
|
||||
|
||||
async function pollUntil(fn: () => boolean, millis = 1000): Promise<void> {
|
||||
while (!fn()) {
|
||||
@@ -40,9 +40,13 @@ interface IAvailableUpdate {
|
||||
let _updateType: UpdateType | undefined = undefined;
|
||||
function getUpdateType(): UpdateType {
|
||||
if (typeof _updateType === 'undefined') {
|
||||
_updateType = fs.existsSync(path.join(path.dirname(process.execPath), 'unins000.exe'))
|
||||
? UpdateType.Setup
|
||||
: UpdateType.Archive;
|
||||
if (fs.existsSync(path.join(path.dirname(process.execPath), 'unins000.exe'))) {
|
||||
_updateType = UpdateType.Setup;
|
||||
} else if (path.basename(path.normalize(path.join(process.execPath, '..', '..'))) === 'Program Files') {
|
||||
_updateType = UpdateType.WindowsInstaller;
|
||||
} else {
|
||||
_updateType = UpdateType.Archive;
|
||||
}
|
||||
}
|
||||
|
||||
return _updateType;
|
||||
@@ -61,6 +65,7 @@ export class Win32UpdateService extends AbstractUpdateService implements IRelaun
|
||||
constructor(
|
||||
@ILifecycleMainService lifecycleMainService: ILifecycleMainService,
|
||||
@IConfigurationService configurationService: IConfigurationService,
|
||||
// @ts-expect-error
|
||||
@ITelemetryService private readonly telemetryService: ITelemetryService,
|
||||
@IEnvironmentMainService environmentMainService: IEnvironmentMainService,
|
||||
@IRequestService requestService: IRequestService,
|
||||
@@ -101,15 +106,24 @@ export class Win32UpdateService extends AbstractUpdateService implements IRelaun
|
||||
|
||||
// Void: buildUpdateFeedUrl -> doBuildUpdateFeedUrl
|
||||
protected doBuildUpdateFeedUrl(quality: string): string | undefined {
|
||||
let platform = `win32-${process.arch}`;
|
||||
|
||||
if (getUpdateType() === UpdateType.Archive) {
|
||||
platform += '-archive';
|
||||
} else if (this.productService.target === 'user') {
|
||||
platform += '-user';
|
||||
let target: Target;
|
||||
switch (getUpdateType()) {
|
||||
case UpdateType.Archive:
|
||||
target = 'archive'
|
||||
break;
|
||||
case UpdateType.WindowsInstaller:
|
||||
target = 'msi'
|
||||
break;
|
||||
default:
|
||||
if (this.productService.target === 'user') {
|
||||
target = 'user'
|
||||
}
|
||||
else {
|
||||
target = 'system'
|
||||
}
|
||||
}
|
||||
|
||||
return createUpdateURL(platform, quality, this.productService);
|
||||
return createUpdateURL(this.productService, quality, process.platform, process.arch, target);
|
||||
}
|
||||
|
||||
protected doCheckForUpdates(context: any): void {
|
||||
@@ -129,6 +143,16 @@ export class Win32UpdateService extends AbstractUpdateService implements IRelaun
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
|
||||
const fetchedVersion = update.productVersion.replace(/(\d+\.\d+\.\d+)(?:\.(\d+))(\-\w+)?/, '$1$3+$2');
|
||||
const currentVersion = `${this.productService.voidVersion}+${this.productService.release}`;
|
||||
// Void compares voidVersion, not VSCode version
|
||||
// const currentVersion = `${this.productService.version}+${this.productService.release}`;
|
||||
|
||||
if (semver.compareBuild(currentVersion, fetchedVersion) >= 0) {
|
||||
this.setState(State.Idle(updateType));
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
|
||||
if (updateType === UpdateType.Archive) {
|
||||
this.setState(State.AvailableForDownload(update));
|
||||
return Promise.resolve(null);
|
||||
@@ -155,7 +179,7 @@ export class Win32UpdateService extends AbstractUpdateService implements IRelaun
|
||||
this.availableUpdate = { packagePath };
|
||||
this.setState(State.Downloaded(update));
|
||||
|
||||
const fastUpdatesEnabled = this.configurationService.getValue('update.enableWindowsBackgroundUpdates');
|
||||
const fastUpdatesEnabled = getUpdateType() === UpdateType.Setup && this.configurationService.getValue('update.enableWindowsBackgroundUpdates');
|
||||
if (fastUpdatesEnabled) {
|
||||
if (this.productService.target === 'user') {
|
||||
this.doApplyUpdate();
|
||||
@@ -167,7 +191,6 @@ export class Win32UpdateService extends AbstractUpdateService implements IRelaun
|
||||
});
|
||||
})
|
||||
.then(undefined, err => {
|
||||
this.telemetryService.publicLog2<{ messageHash: string }, UpdateErrorClassification>('update:error', { messageHash: String(hash(String(err))) });
|
||||
this.logService.error(err);
|
||||
|
||||
// only show message when explicitly checking for updates
|
||||
@@ -251,10 +274,18 @@ export class Win32UpdateService extends AbstractUpdateService implements IRelaun
|
||||
if (this.availableUpdate.updateFilePath) {
|
||||
fs.unlinkSync(this.availableUpdate.updateFilePath);
|
||||
} else {
|
||||
spawn(this.availableUpdate.packagePath, ['/silent', '/log', '/mergetasks=runcode,!desktopicon,!quicklaunchicon'], {
|
||||
detached: true,
|
||||
stdio: ['ignore', 'ignore', 'ignore']
|
||||
});
|
||||
const type = getUpdateType();
|
||||
if (type === UpdateType.WindowsInstaller) {
|
||||
spawn('msiexec.exe', ['/i', this.availableUpdate.packagePath], {
|
||||
detached: true,
|
||||
stdio: ['ignore', 'ignore', 'ignore']
|
||||
});
|
||||
} else {
|
||||
spawn(this.availableUpdate.packagePath, ['/silent', '/log', '/mergetasks=runcode,!desktopicon,!quicklaunchicon'], {
|
||||
detached: true,
|
||||
stdio: ['ignore', 'ignore', 'ignore']
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -52,7 +52,7 @@ export class AuxiliaryBarPart extends AbstractPaneCompositePart {
|
||||
static readonly viewContainersWorkspaceStateKey = 'workbench.auxiliarybar.viewContainersWorkspaceState';
|
||||
|
||||
// Use the side bar dimensions
|
||||
override readonly minimumWidth: number = 230; // Void changed this (was 170)
|
||||
override readonly minimumWidth: number = 280; // Void changed this (was 170)
|
||||
override readonly maximumWidth: number = Number.POSITIVE_INFINITY;
|
||||
override readonly minimumHeight: number = 0;
|
||||
override readonly maximumHeight: number = Number.POSITIVE_INFINITY;
|
||||
|
||||
@@ -21,6 +21,7 @@ import { MarkdownRenderer, openLinkFromMarkdown } from '../../../../editor/brows
|
||||
import { defaultButtonStyles, defaultCheckboxStyles, defaultDialogStyles, defaultInputBoxStyles } from '../../../../platform/theme/browser/defaultStyles.js';
|
||||
import { ResultKind } from '../../../../platform/keybinding/common/keybindingResolver.js';
|
||||
import { IOpenerService } from '../../../../platform/opener/common/opener.js';
|
||||
import { getReleaseString } from '../../../../workbench/common/release.js';
|
||||
|
||||
export class BrowserDialogHandler extends AbstractDialogHandler {
|
||||
|
||||
@@ -79,13 +80,14 @@ export class BrowserDialogHandler extends AbstractDialogHandler {
|
||||
|
||||
async about(): Promise<void> {
|
||||
const detailString = (useAgo: boolean): string => {
|
||||
const releaseString = getReleaseString();
|
||||
return localize('aboutDetail',
|
||||
"Version: {0}\nCommit: {1}\nDate: {2}\nBrowser: {3}",
|
||||
this.productService.version || 'Unknown',
|
||||
this.productService.commit || 'Unknown',
|
||||
this.productService.date ? `${this.productService.date}${useAgo ? ' (' + fromNow(new Date(this.productService.date), true) + ')' : ''}` : 'Unknown',
|
||||
navigator.userAgent
|
||||
);
|
||||
).replace('\n', `\n${releaseString} ${this.productService.release || 'Unknown'}\n`);
|
||||
};
|
||||
|
||||
const detail = detailString(true);
|
||||
|
||||
@@ -250,7 +250,9 @@ export class EditorGroupWatermark extends Disposable {
|
||||
linkSpan.appendChild(dirSpan);
|
||||
|
||||
return linkSpan
|
||||
}).filter(v => !!v)
|
||||
})
|
||||
.filter(v => !!v)
|
||||
.slice(0, 5) // take 5 most recent
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -612,7 +612,7 @@ const registry = Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Con
|
||||
'description': localize('workbench.hover.delay', "Controls the delay in milliseconds after which the hover is shown for workbench items (ex. some extension provided tree view items). Already visible items may require a refresh before reflecting this setting change."),
|
||||
// Testing has indicated that on Windows and Linux 500 ms matches the native hovers most closely.
|
||||
// On Mac, the delay is 1500.
|
||||
'default': isMacintosh ? 1500 : 500,
|
||||
'default': 300, // Void changed this from isMacintosh ? 1500 : 500,
|
||||
'minimum': 0
|
||||
},
|
||||
'workbench.reduceMotion': {
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
// added by VSCodium
|
||||
import { language } from '../../../vs/base/common/platform.js';
|
||||
|
||||
const DEFAULT_LABEL = 'Release:';
|
||||
const LABELS: { [key: string]: string } = {
|
||||
'en': DEFAULT_LABEL,
|
||||
'fr': 'Révision :',
|
||||
'ru': 'Релиз:',
|
||||
'zh-hans': '发布版本:',
|
||||
'zh-hant': '發布版本:',
|
||||
};
|
||||
|
||||
export function getReleaseString(): string {
|
||||
return LABELS[language] ?? DEFAULT_LABEL;
|
||||
}
|
||||
@@ -648,15 +648,18 @@ const defaultChat = {
|
||||
providerSetting: product.defaultChatAgent?.providerSetting ?? '',
|
||||
};
|
||||
|
||||
// Void commented this out - copilot head
|
||||
// Add next to the command center if command center is disabled
|
||||
MenuRegistry.appendMenuItem(MenuId.CommandCenter, {
|
||||
/* MenuRegistry.appendMenuItem(MenuId.CommandCenter, {
|
||||
submenu: MenuId.ChatTitleBarMenu,
|
||||
title: localize('title4', "Copilot"),
|
||||
icon: Codicon.copilot,
|
||||
when: ContextKeyExpr.and(
|
||||
ChatContextKeys.supported,
|
||||
ContextKeyExpr.has('config.chat.commandCenter.enabled')
|
||||
),
|
||||
// Void commented this out - copilot head
|
||||
when: ContextKeyExpr.false(),
|
||||
// when: ContextKeyExpr.and(
|
||||
// ChatContextKeys.supported,
|
||||
// ContextKeyExpr.has('config.chat.commandCenter.enabled')
|
||||
// ),
|
||||
order: 10001 // to the right of command center
|
||||
});
|
||||
|
||||
@@ -666,13 +669,15 @@ MenuRegistry.appendMenuItem(MenuId.TitleBar, {
|
||||
title: localize('title4', "Copilot"),
|
||||
group: 'navigation',
|
||||
icon: Codicon.copilot,
|
||||
when: ContextKeyExpr.and(
|
||||
ChatContextKeys.supported,
|
||||
ContextKeyExpr.has('config.chat.commandCenter.enabled'),
|
||||
ContextKeyExpr.has('config.window.commandCenter').negate(),
|
||||
),
|
||||
when: ContextKeyExpr.false(),
|
||||
// Void commented this out - copilot head
|
||||
// when: ContextKeyExpr.and(
|
||||
// ChatContextKeys.supported,
|
||||
// ContextKeyExpr.has('config.chat.commandCenter.enabled'),
|
||||
// ContextKeyExpr.has('config.window.commandCenter').negate(),
|
||||
// ),
|
||||
order: 1
|
||||
});
|
||||
}); */
|
||||
|
||||
registerAction2(class ToggleCopilotControl extends ToggleTitleBarConfigAction {
|
||||
constructor() {
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
import { Disposable, DisposableStore } from '../../../../base/common/lifecycle.js';
|
||||
import { localize } from '../../../../nls.js';
|
||||
import { Action2, MenuId, MenuRegistry, registerAction2 } from '../../../../platform/actions/common/actions.js';
|
||||
import { Action2, MenuId, registerAction2 } from '../../../../platform/actions/common/actions.js';
|
||||
import { ContextKeyExpr, IContextKey, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';
|
||||
import { IEnvironmentService } from '../../../../platform/environment/common/environment.js';
|
||||
import { IFileService } from '../../../../platform/files/common/files.js';
|
||||
@@ -15,7 +15,7 @@ import { IStorageService, StorageScope, StorageTarget } from '../../../../platfo
|
||||
import { createSyncHeaders, IAuthenticationProvider, IResourceRefHandle } from '../../../../platform/userDataSync/common/userDataSync.js';
|
||||
import { AuthenticationSession, AuthenticationSessionsChangeEvent, IAuthenticationService } from '../../../services/authentication/common/authentication.js';
|
||||
import { IExtensionService } from '../../../services/extensions/common/extensions.js';
|
||||
import { EDIT_SESSIONS_SIGNED_IN, EditSession, EDIT_SESSION_SYNC_CATEGORY, IEditSessionsStorageService, EDIT_SESSIONS_SIGNED_IN_KEY, IEditSessionsLogService, SyncResource, EDIT_SESSIONS_PENDING_KEY } from '../common/editSessions.js';
|
||||
import { EDIT_SESSIONS_SIGNED_IN, EditSession, EDIT_SESSION_SYNC_CATEGORY, IEditSessionsStorageService, EDIT_SESSIONS_SIGNED_IN_KEY, IEditSessionsLogService, SyncResource } from '../common/editSessions.js';
|
||||
import { IDialogService } from '../../../../platform/dialogs/common/dialogs.js';
|
||||
import { generateUuid } from '../../../../base/common/uuid.js';
|
||||
import { getCurrentAuthenticationSessionInfo } from '../../../services/authentication/browser/authenticationService.js';
|
||||
@@ -91,7 +91,6 @@ export class EditSessionsWorkbenchService extends Disposable implements IEditSes
|
||||
// If another window changes the preferred session storage, reset our cached auth state in memory
|
||||
this._register(this.storageService.onDidChangeValue(StorageScope.APPLICATION, EditSessionsWorkbenchService.CACHED_SESSION_STORAGE_KEY, this._store)(() => this.onDidChangeStorage()));
|
||||
|
||||
this.registerSignInAction();
|
||||
this.registerResetAuthenticationAction();
|
||||
|
||||
this.signedInContext = EDIT_SESSIONS_SIGNED_IN.bindTo(this.contextKeyService);
|
||||
@@ -454,46 +453,6 @@ export class EditSessionsWorkbenchService extends Disposable implements IEditSes
|
||||
}
|
||||
}
|
||||
|
||||
private registerSignInAction() {
|
||||
if (!this.serverConfiguration?.url) {
|
||||
return;
|
||||
}
|
||||
const that = this;
|
||||
const id = 'workbench.editSessions.actions.signIn';
|
||||
const when = ContextKeyExpr.and(ContextKeyExpr.equals(EDIT_SESSIONS_PENDING_KEY, false), ContextKeyExpr.equals(EDIT_SESSIONS_SIGNED_IN_KEY, false));
|
||||
this._register(registerAction2(class ResetEditSessionAuthenticationAction extends Action2 {
|
||||
constructor() {
|
||||
super({
|
||||
id,
|
||||
title: localize('sign in', 'Turn on Cloud Changes...'),
|
||||
category: EDIT_SESSION_SYNC_CATEGORY,
|
||||
precondition: when,
|
||||
menu: [{
|
||||
id: MenuId.CommandPalette,
|
||||
},
|
||||
{
|
||||
id: MenuId.AccountsContext,
|
||||
group: '2_editSessions',
|
||||
when,
|
||||
}]
|
||||
});
|
||||
}
|
||||
|
||||
async run() {
|
||||
return await that.initialize('write', false);
|
||||
}
|
||||
}));
|
||||
|
||||
this._register(MenuRegistry.appendMenuItem(MenuId.AccountsContext, {
|
||||
group: '2_editSessions',
|
||||
command: {
|
||||
id,
|
||||
title: localize('sign in badge', 'Turn on Cloud Changes... (1)'),
|
||||
},
|
||||
when: ContextKeyExpr.and(ContextKeyExpr.equals(EDIT_SESSIONS_PENDING_KEY, true), ContextKeyExpr.equals(EDIT_SESSIONS_SIGNED_IN_KEY, false))
|
||||
}));
|
||||
}
|
||||
|
||||
private registerResetAuthenticationAction() {
|
||||
const that = this;
|
||||
this._register(registerAction2(class ResetEditSessionAuthenticationAction extends Action2 {
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
/*--------------------------------------------------------------------------------------
|
||||
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import { KeyCode, KeyMod } from '../../../../base/common/keyCodes.js';
|
||||
import { Disposable } from '../../../../base/common/lifecycle.js';
|
||||
import { ServicesAccessor } from '../../../../editor/browser/editorExtensions.js';
|
||||
import { ICodeEditorService } from '../../../../editor/browser/services/codeEditorService.js';
|
||||
import { localize2 } from '../../../../nls.js';
|
||||
import { Action2, registerAction2 } from '../../../../platform/actions/common/actions.js';
|
||||
import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js';
|
||||
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
|
||||
import { KeybindingWeight } from '../../../../platform/keybinding/common/keybindingsRegistry.js';
|
||||
import { IWorkbenchContribution, registerWorkbenchContribution2, WorkbenchPhase } from '../../../common/contributions.js';
|
||||
|
||||
|
||||
// to change this, just Cmd+Shift+F and replace DummyService with YourServiceName, and create a unique ID below
|
||||
export interface IDummyService {
|
||||
readonly _serviceBrand: undefined; // services need this, just leave it undefined
|
||||
}
|
||||
|
||||
export const IDummyService = createDecorator<IDummyService>('DummyService');
|
||||
|
||||
|
||||
|
||||
// An example of an action (delete if you're not using an action):
|
||||
registerAction2(class extends Action2 {
|
||||
constructor() {
|
||||
super({
|
||||
f1: true,
|
||||
id: 'void.dummy',
|
||||
title: localize2('dummy', 'dummy: Init'),
|
||||
keybinding: {
|
||||
primary: KeyMod.CtrlCmd | KeyCode.Digit0,
|
||||
weight: KeybindingWeight.VoidExtension,
|
||||
}
|
||||
});
|
||||
}
|
||||
async run(accessor: ServicesAccessor): Promise<void> {
|
||||
console.log('hi')
|
||||
const n = accessor.get(IDummyService)
|
||||
console.log('Hi', n._serviceBrand)
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
class DummyService extends Disposable implements IWorkbenchContribution, IDummyService {
|
||||
static readonly ID = 'workbench.contrib.void.dummy' // workbenchContributions need this, services do not
|
||||
_serviceBrand: undefined;
|
||||
|
||||
constructor(
|
||||
@ICodeEditorService codeEditorService: ICodeEditorService,
|
||||
) {
|
||||
super()
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// pick one and delete the other:
|
||||
registerSingleton(IDummyService, DummyService, InstantiationType.Eager);
|
||||
|
||||
registerWorkbenchContribution2(DummyService.ID, DummyService, WorkbenchPhase.BlockRestore);
|
||||
@@ -34,7 +34,7 @@
|
||||
// // const result = await new Promise((res, rej) => {
|
||||
// // sendLLMMessage({
|
||||
// // messages,
|
||||
// // tools: ['text_search'],
|
||||
// // tools: ['search_for_files'],
|
||||
// // onFinalMessage: ({ result: r, }) => {
|
||||
// // res(r)
|
||||
// // },
|
||||
@@ -73,7 +73,7 @@
|
||||
// // const result = new Promise((res, rej) => {
|
||||
// // sendLLMMessage({
|
||||
// // messages,
|
||||
// // tools: ['text_search'],
|
||||
// // tools: ['search_for_files'],
|
||||
// // onResult: (r) => {
|
||||
// // res(r)
|
||||
// // }
|
||||
|
||||
@@ -8,8 +8,7 @@ import { ILanguageFeaturesService } from '../../../../editor/common/services/lan
|
||||
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
|
||||
import { ITextModel } from '../../../../editor/common/model.js';
|
||||
import { Position } from '../../../../editor/common/core/position.js';
|
||||
import { InlineCompletion, InlineCompletionContext, } from '../../../../editor/common/languages.js';
|
||||
import { CancellationToken } from '../../../../base/common/cancellation.js';
|
||||
import { InlineCompletion, } from '../../../../editor/common/languages.js';
|
||||
import { Range } from '../../../../editor/common/core/range.js';
|
||||
import { IEditorService } from '../../../services/editor/common/editorService.js';
|
||||
import { isCodeEditor } from '../../../../editor/browser/editorBrowser.js';
|
||||
@@ -21,6 +20,7 @@ import { ILLMMessageService } from '../common/sendLLMMessageService.js';
|
||||
import { isWindows } from '../../../../base/common/platform.js';
|
||||
import { IVoidSettingsService } from '../common/voidSettingsService.js';
|
||||
import { FeatureName } from '../common/voidSettingsTypes.js';
|
||||
import { IConvertToLLMMessageService } from './convertToLLMMessageService.js';
|
||||
// import { IContextGatheringService } from './contextGatheringService.js';
|
||||
|
||||
|
||||
@@ -633,8 +633,6 @@ export class AutocompleteService extends Disposable implements IAutocompleteServ
|
||||
async _provideInlineCompletionItems(
|
||||
model: ITextModel,
|
||||
position: Position,
|
||||
context: InlineCompletionContext,
|
||||
token: CancellationToken,
|
||||
): Promise<InlineCompletion[]> {
|
||||
|
||||
const isEnabled = this._settingsService.state.globalSettings.enableAutocomplete
|
||||
@@ -793,19 +791,22 @@ export class AutocompleteService extends Disposable implements IAutocompleteServ
|
||||
|
||||
const featureName: FeatureName = 'Autocomplete'
|
||||
const modelSelection = this._settingsService.state.modelSelectionOfFeature[featureName]
|
||||
const modelSelectionOptions = modelSelection ? this._settingsService.state.optionsOfModelSelection[modelSelection.providerName]?.[modelSelection.modelName] : undefined
|
||||
|
||||
const modelSelectionOptions = modelSelection ? this._settingsService.state.optionsOfModelSelection[featureName][modelSelection.providerName]?.[modelSelection.modelName] : undefined
|
||||
const aiInstructions = this._settingsService.state.globalSettings.aiInstructions
|
||||
|
||||
// set parameters of `newAutocompletion` appropriately
|
||||
newAutocompletion.llmPromise = new Promise((resolve, reject) => {
|
||||
|
||||
const requestId = this._llmMessageService.sendLLMMessage({
|
||||
messagesType: 'FIMMessage',
|
||||
messages: {
|
||||
prefix: llmPrefix,
|
||||
suffix: llmSuffix,
|
||||
stopTokens: stopTokens,
|
||||
},
|
||||
messages: this._convertToLLMMessageService.prepareFIMMessage({
|
||||
messages: {
|
||||
prefix: llmPrefix,
|
||||
suffix: llmSuffix,
|
||||
stopTokens: stopTokens,
|
||||
},
|
||||
aiInstructions
|
||||
}),
|
||||
modelSelection,
|
||||
modelSelectionOptions,
|
||||
logging: { loggingName: 'Autocomplete' },
|
||||
@@ -852,7 +853,7 @@ export class AutocompleteService extends Disposable implements IAutocompleteServ
|
||||
newAutocompletion.status = 'error'
|
||||
reject(message)
|
||||
},
|
||||
onAbort: () => { },
|
||||
onAbort: () => { reject('Aborted autocomplete') },
|
||||
})
|
||||
newAutocompletion.requestId = requestId
|
||||
|
||||
@@ -893,13 +894,14 @@ export class AutocompleteService extends Disposable implements IAutocompleteServ
|
||||
@IEditorService private readonly _editorService: IEditorService,
|
||||
@IModelService private readonly _modelService: IModelService,
|
||||
@IVoidSettingsService private readonly _settingsService: IVoidSettingsService,
|
||||
@IConvertToLLMMessageService private readonly _convertToLLMMessageService: IConvertToLLMMessageService
|
||||
// @IContextGatheringService private readonly _contextGatheringService: IContextGatheringService,
|
||||
) {
|
||||
super()
|
||||
|
||||
this._langFeatureService.inlineCompletionsProvider.register('*', {
|
||||
this._register(this._langFeatureService.inlineCompletionsProvider.register('*', {
|
||||
provideInlineCompletions: async (model, position, context, token) => {
|
||||
const items = await this._provideInlineCompletionItems(model, position, context, token)
|
||||
const items = await this._provideInlineCompletionItems(model, position)
|
||||
|
||||
// console.log('item: ', items?.[0]?.insertText)
|
||||
return { items: items, }
|
||||
@@ -936,7 +938,7 @@ export class AutocompleteService extends Disposable implements IAutocompleteServ
|
||||
});
|
||||
|
||||
},
|
||||
})
|
||||
}))
|
||||
}
|
||||
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,607 @@
|
||||
import { Disposable } from '../../../../base/common/lifecycle.js';
|
||||
import { deepClone } from '../../../../base/common/objects.js';
|
||||
import { IModelService } from '../../../../editor/common/services/model.js';
|
||||
import { registerSingleton, InstantiationType } from '../../../../platform/instantiation/common/extensions.js';
|
||||
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
|
||||
import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js';
|
||||
import { IEditorService } from '../../../services/editor/common/editorService.js';
|
||||
import { ChatMessage } from '../common/chatThreadServiceTypes.js';
|
||||
import { getIsReasoningEnabledState, getMaxOutputTokens, getModelCapabilities } from '../common/modelCapabilities.js';
|
||||
import { toolCallXMLStr, chat_systemMessage, ToolName } from '../common/prompt/prompts.js';
|
||||
import { AnthropicLLMChatMessage, AnthropicReasoning, LLMChatMessage, LLMFIMMessage, OpenAILLMChatMessage, RawToolParamsObj } from '../common/sendLLMMessageTypes.js';
|
||||
import { IVoidSettingsService } from '../common/voidSettingsService.js';
|
||||
import { ChatMode, FeatureName, ModelSelection } from '../common/voidSettingsTypes.js';
|
||||
import { IDirectoryStrService } from './directoryStrService.js';
|
||||
import { ITerminalToolService } from './terminalToolService.js';
|
||||
|
||||
|
||||
|
||||
|
||||
type SimpleLLMMessage = {
|
||||
role: 'tool';
|
||||
content: string;
|
||||
id: string;
|
||||
name: ToolName;
|
||||
rawParams: RawToolParamsObj;
|
||||
} | {
|
||||
role: 'user';
|
||||
content: string;
|
||||
} | {
|
||||
role: 'assistant';
|
||||
content: string;
|
||||
anthropicReasoning: AnthropicReasoning[] | null;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
const EMPTY_MESSAGE = '(empty message)'
|
||||
|
||||
const CHARS_PER_TOKEN = 4
|
||||
const TRIM_TO_LEN = 60
|
||||
|
||||
|
||||
|
||||
|
||||
// convert messages as if about to send to openai
|
||||
/*
|
||||
reference - https://platform.openai.com/docs/guides/function-calling#function-calling-steps
|
||||
openai MESSAGE (role=assistant):
|
||||
"tool_calls":[{
|
||||
"type": "function",
|
||||
"id": "call_12345xyz",
|
||||
"function": {
|
||||
"name": "get_weather",
|
||||
"arguments": "{\"latitude\":48.8566,\"longitude\":2.3522}"
|
||||
}]
|
||||
|
||||
openai RESPONSE (role=user):
|
||||
{ "role": "tool",
|
||||
"tool_call_id": tool_call.id,
|
||||
"content": str(result) }
|
||||
|
||||
also see
|
||||
openai on prompting - https://platform.openai.com/docs/guides/reasoning#advice-on-prompting
|
||||
openai on developer system message - https://cdn.openai.com/spec/model-spec-2024-05-08.html#follow-the-chain-of-command
|
||||
*/
|
||||
|
||||
|
||||
const prepareMessages_openai_tools = (messages: SimpleLLMMessage[]): LLMChatMessage[] => {
|
||||
|
||||
const newMessages: OpenAILLMChatMessage[] = [];
|
||||
|
||||
for (let i = 0; i < messages.length; i += 1) {
|
||||
const currMsg = messages[i]
|
||||
|
||||
if (currMsg.role !== 'tool') {
|
||||
newMessages.push(currMsg)
|
||||
continue
|
||||
}
|
||||
|
||||
// edit previous assistant message to have called the tool
|
||||
const prevMsg = 0 <= i - 1 && i - 1 <= newMessages.length ? newMessages[i - 1] : undefined
|
||||
if (prevMsg?.role === 'assistant') {
|
||||
prevMsg.tool_calls = [{
|
||||
type: 'function',
|
||||
id: currMsg.id,
|
||||
function: {
|
||||
name: currMsg.name,
|
||||
arguments: JSON.stringify(currMsg.rawParams)
|
||||
}
|
||||
}]
|
||||
}
|
||||
|
||||
// add the tool
|
||||
newMessages.push({
|
||||
role: 'tool',
|
||||
tool_call_id: currMsg.id,
|
||||
content: currMsg.content,
|
||||
})
|
||||
}
|
||||
return newMessages
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
// convert messages as if about to send to anthropic
|
||||
/*
|
||||
https://docs.anthropic.com/en/docs/build-with-claude/tool-use#tool-use-examples
|
||||
anthropic MESSAGE (role=assistant):
|
||||
"content": [{
|
||||
"type": "text",
|
||||
"text": "<thinking>I need to call the get_weather function, and the user wants SF, which is likely San Francisco, CA.</thinking>"
|
||||
}, {
|
||||
"type": "tool_use",
|
||||
"id": "toolu_01A09q90qw90lq917835lq9",
|
||||
"name": "get_weather",
|
||||
"input": { "location": "San Francisco, CA", "unit": "celsius" }
|
||||
}]
|
||||
anthropic RESPONSE (role=user):
|
||||
"content": [{
|
||||
"type": "tool_result",
|
||||
"tool_use_id": "toolu_01A09q90qw90lq917835lq9",
|
||||
"content": "15 degrees"
|
||||
}]
|
||||
|
||||
|
||||
Converts:
|
||||
assistant: ...content
|
||||
tool: (id, name, params)
|
||||
->
|
||||
assistant: ...content, call(name, id, params)
|
||||
user: ...content, result(id, content)
|
||||
*/
|
||||
|
||||
|
||||
const prepareMessages_anthropic_tools = (messages: SimpleLLMMessage[], supportsAnthropicReasoning: boolean): LLMChatMessage[] => {
|
||||
const newMessages: (AnthropicLLMChatMessage | (SimpleLLMMessage & { role: 'tool' }))[] = messages;
|
||||
|
||||
for (let i = 0; i < messages.length; i += 1) {
|
||||
const currMsg = messages[i]
|
||||
|
||||
// add anthropic reasoning
|
||||
if (currMsg.role === 'assistant') {
|
||||
if (currMsg.anthropicReasoning && supportsAnthropicReasoning) {
|
||||
|
||||
const content = currMsg.content
|
||||
newMessages[i] = {
|
||||
role: 'assistant',
|
||||
content: content ? [...currMsg.anthropicReasoning, { type: 'text' as const, text: content }] : currMsg.anthropicReasoning
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if (currMsg.role === 'user') {
|
||||
newMessages[i] = {
|
||||
role: 'user',
|
||||
content: currMsg.content,
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if (currMsg.role === 'tool') {
|
||||
// add anthropic tools
|
||||
const prevMsg = 0 <= i - 1 && i - 1 <= newMessages.length ? newMessages[i - 1] : undefined
|
||||
|
||||
// make it so the assistant called the tool
|
||||
if (prevMsg?.role === 'assistant') {
|
||||
if (typeof prevMsg.content === 'string') prevMsg.content = [{ type: 'text', text: prevMsg.content }]
|
||||
prevMsg.content.push({ type: 'tool_use', id: currMsg.id, name: currMsg.name, input: currMsg.rawParams })
|
||||
}
|
||||
|
||||
// turn each tool into a user message with tool results at the end
|
||||
newMessages[i] = {
|
||||
role: 'user',
|
||||
content: [{ type: 'tool_result', tool_use_id: currMsg.id, content: currMsg.content }]
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// we just removed the tools
|
||||
return newMessages as AnthropicLLMChatMessage[]
|
||||
}
|
||||
|
||||
|
||||
const prepareMessages_XML_tools = (messages: SimpleLLMMessage[], supportsAnthropicReasoning: boolean): LLMChatMessage[] => {
|
||||
|
||||
const llmChatMessages: LLMChatMessage[] = [];
|
||||
for (let i = 0; i < messages.length; i += 1) {
|
||||
|
||||
const c = messages[i]
|
||||
const next = 0 <= i + 1 && i + 1 <= messages.length - 1 ? messages[i + 1] : null
|
||||
|
||||
if (c.role === 'assistant') {
|
||||
// if called a tool (message after it), re-add its XML to the message
|
||||
// alternatively, could just hold onto the original output, but this way requires less piping raw strings everywhere
|
||||
let content: LLMChatMessage['content'] = c.content
|
||||
if (next?.role === 'tool') {
|
||||
content = `${content}\n\n${toolCallXMLStr(next.name, next.rawParams)}`
|
||||
}
|
||||
|
||||
// anthropic reasoning
|
||||
if (c.anthropicReasoning && supportsAnthropicReasoning) {
|
||||
content = content ? [...c.anthropicReasoning, { type: 'text' as const, text: content }] : c.anthropicReasoning
|
||||
}
|
||||
llmChatMessages.push({
|
||||
role: 'assistant',
|
||||
content
|
||||
})
|
||||
}
|
||||
// add user or tool to the previous user message
|
||||
else if (c.role === 'user' || c.role === 'tool') {
|
||||
if (c.role === 'tool')
|
||||
c.content = `<${c.name}_result>\n${c.content}\n</${c.name}_result>`
|
||||
|
||||
if (llmChatMessages.length === 0 || llmChatMessages[llmChatMessages.length - 1].role !== 'user')
|
||||
llmChatMessages.push({
|
||||
role: 'user',
|
||||
content: c.content
|
||||
})
|
||||
else
|
||||
llmChatMessages[llmChatMessages.length - 1].content += '\n\n' + c.content
|
||||
}
|
||||
}
|
||||
return llmChatMessages
|
||||
}
|
||||
|
||||
|
||||
|
||||
const prepareMessages_providerSpecific = (messages: SimpleLLMMessage[], specialToolFormat: 'openai-style' | 'anthropic-style' | undefined, supportsAnthropicReasoning: boolean): LLMChatMessage[] => {
|
||||
const llmChatMessages: LLMChatMessage[] = []
|
||||
if (!specialToolFormat) { // XML tool behavior
|
||||
return prepareMessages_XML_tools(messages, supportsAnthropicReasoning)
|
||||
}
|
||||
else if (specialToolFormat === 'anthropic-style') {
|
||||
return prepareMessages_anthropic_tools(messages, supportsAnthropicReasoning)
|
||||
}
|
||||
else if (specialToolFormat === 'openai-style') {
|
||||
return prepareMessages_openai_tools(messages)
|
||||
}
|
||||
return llmChatMessages
|
||||
}
|
||||
|
||||
|
||||
// --- CHAT ---
|
||||
|
||||
const prepareMessages = ({
|
||||
messages,
|
||||
systemMessage,
|
||||
aiInstructions,
|
||||
supportsSystemMessage,
|
||||
specialToolFormat,
|
||||
supportsAnthropicReasoning,
|
||||
contextWindow,
|
||||
maxOutputTokens,
|
||||
}: {
|
||||
messages: SimpleLLMMessage[],
|
||||
systemMessage: string,
|
||||
aiInstructions: string,
|
||||
supportsSystemMessage: false | 'system-role' | 'developer-role' | 'separated',
|
||||
specialToolFormat: 'openai-style' | 'anthropic-style' | undefined,
|
||||
supportsAnthropicReasoning: boolean,
|
||||
contextWindow: number,
|
||||
maxOutputTokens: number | null | undefined,
|
||||
}): { messages: LLMChatMessage[], separateSystemMessage: string | undefined } => {
|
||||
maxOutputTokens = maxOutputTokens ?? 4_096 // default to 4096
|
||||
|
||||
// ================ trim ================
|
||||
|
||||
messages = deepClone(messages)
|
||||
messages = messages.map(m => ({ ...m, content: m.role !== 'tool' ? m.content.trim() : m.content }))
|
||||
|
||||
// ================ fit into context ================
|
||||
|
||||
// the higher the weight, the higher the desire to truncate - TRIM HIGHEST WEIGHT MESSAGES
|
||||
const alreadyTrimmedIdxes = new Set<number>()
|
||||
const weight = (message: SimpleLLMMessage, messages: SimpleLLMMessage[], idx: number) => {
|
||||
const base = message.content.length
|
||||
|
||||
let multiplier: number
|
||||
multiplier = 1 + (messages.length - 1 - idx) / messages.length // slow rampdown from 2 to 1 as index increases
|
||||
if (message.role === 'user') {
|
||||
multiplier *= 1
|
||||
}
|
||||
else {
|
||||
multiplier *= 10 // llm tokens are far less valuable than user tokens
|
||||
}
|
||||
// 1st message, last 3 msgs, any already modified message should be low in weight
|
||||
if (idx === 0 || idx >= messages.length - 1 - 3 || alreadyTrimmedIdxes.has(idx)) {
|
||||
multiplier *= .05
|
||||
}
|
||||
return base * multiplier
|
||||
}
|
||||
|
||||
const _findLargestByWeight = (messages: SimpleLLMMessage[]) => {
|
||||
let largestIndex = -1
|
||||
let largestWeight = -Infinity
|
||||
for (let i = 0; i < messages.length; i += 1) {
|
||||
const m = messages[i]
|
||||
const w = weight(m, messages, i)
|
||||
if (w > largestWeight) {
|
||||
largestWeight = w
|
||||
largestIndex = i
|
||||
}
|
||||
}
|
||||
return largestIndex
|
||||
}
|
||||
|
||||
let totalLen = 0
|
||||
for (const m of messages) { totalLen += m.content.length }
|
||||
const charsNeedToTrim = totalLen - (contextWindow - maxOutputTokens) * CHARS_PER_TOKEN
|
||||
|
||||
// <----------------------------------------->
|
||||
// 0 | | |
|
||||
// | contextWindow |
|
||||
// contextWindow - maxOut|putTokens
|
||||
// totalLen
|
||||
let remainingCharsToTrim = charsNeedToTrim
|
||||
let i = 0
|
||||
|
||||
while (remainingCharsToTrim > 0) {
|
||||
i += 1
|
||||
if (i > 100) break
|
||||
|
||||
const trimIdx = _findLargestByWeight(messages)
|
||||
const m = messages[trimIdx]
|
||||
|
||||
// if can finish here, do
|
||||
const numCharsWillTrim = m.content.length - TRIM_TO_LEN
|
||||
if (numCharsWillTrim > remainingCharsToTrim) {
|
||||
m.content = m.content.slice(0, m.content.length - remainingCharsToTrim).trim()
|
||||
break
|
||||
}
|
||||
|
||||
remainingCharsToTrim -= numCharsWillTrim
|
||||
m.content = m.content.substring(0, TRIM_TO_LEN - 3) + '...'
|
||||
alreadyTrimmedIdxes.add(trimIdx)
|
||||
}
|
||||
|
||||
// ================ tools and anthropicReasoning ================
|
||||
const llmMessages: LLMChatMessage[] = prepareMessages_providerSpecific(messages, specialToolFormat, supportsAnthropicReasoning)
|
||||
|
||||
// ================ system message concat ================
|
||||
|
||||
// find system messages and concatenate them
|
||||
const newSystemMessage = aiInstructions ?
|
||||
`${(systemMessage ? `${systemMessage}\n\n` : '')}GUIDELINES\n${aiInstructions}`
|
||||
: systemMessage
|
||||
|
||||
let separateSystemMessageStr: string | undefined = undefined
|
||||
|
||||
// if supports system message
|
||||
if (supportsSystemMessage) {
|
||||
if (supportsSystemMessage === 'separated')
|
||||
separateSystemMessageStr = newSystemMessage
|
||||
else if (supportsSystemMessage === 'system-role')
|
||||
llmMessages.unshift({ role: 'system', content: newSystemMessage }) // add new first message
|
||||
else if (supportsSystemMessage === 'developer-role')
|
||||
llmMessages.unshift({ role: 'developer', content: newSystemMessage }) // add new first message
|
||||
}
|
||||
// if does not support system message
|
||||
else {
|
||||
const newFirstMessage = {
|
||||
role: 'user',
|
||||
content: `<SYSTEM_MESSAGE>\n${newSystemMessage}\n</SYSTEM_MESSAGE>\n${llmMessages[0].content}`
|
||||
} as const
|
||||
llmMessages.splice(0, 1) // delete first message
|
||||
llmMessages.unshift(newFirstMessage) // add new first message
|
||||
}
|
||||
|
||||
|
||||
// ================ no empty message ================
|
||||
for (const currMsg of llmMessages) {
|
||||
if (currMsg.role === 'tool') continue
|
||||
|
||||
// if content is a string, replace string with empty msg
|
||||
if (typeof currMsg.content === 'string')
|
||||
currMsg.content = currMsg.content || EMPTY_MESSAGE
|
||||
else {
|
||||
// if content is an array, replace any empty text entries with empty msg, and make sure there's at least 1 entry
|
||||
for (const c of currMsg.content) {
|
||||
if (c.type === 'text') c.text = c.text || EMPTY_MESSAGE
|
||||
}
|
||||
if (currMsg.content.length === 0) currMsg.content = [{ type: 'text', text: EMPTY_MESSAGE }]
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
messages: llmMessages,
|
||||
separateSystemMessage: separateSystemMessageStr,
|
||||
} as const
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
export interface IConvertToLLMMessageService {
|
||||
readonly _serviceBrand: undefined;
|
||||
prepareLLMSimpleMessages: (opts: { simpleMessages: SimpleLLMMessage[], systemMessage: string, modelSelection: ModelSelection | null, featureName: FeatureName }) => { messages: LLMChatMessage[], separateSystemMessage: string | undefined };
|
||||
prepareLLMChatMessages: (opts: { chatMessages: ChatMessage[], chatMode: ChatMode, modelSelection: ModelSelection | null }) => Promise<{ messages: LLMChatMessage[], separateSystemMessage: string | undefined }>
|
||||
prepareFIMMessage(opts: { messages: LLMFIMMessage, aiInstructions: string, }): { prefix: string, suffix: string, stopTokens: string[] }
|
||||
|
||||
}
|
||||
|
||||
export const IConvertToLLMMessageService = createDecorator<IConvertToLLMMessageService>('ConvertToLLMMessageService');
|
||||
|
||||
|
||||
class ConvertToLLMMessageService extends Disposable implements IConvertToLLMMessageService {
|
||||
_serviceBrand: undefined;
|
||||
|
||||
constructor(
|
||||
@IModelService private readonly modelService: IModelService,
|
||||
@IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService,
|
||||
@IEditorService private readonly editorService: IEditorService,
|
||||
@IDirectoryStrService private readonly directoryStrService: IDirectoryStrService,
|
||||
@ITerminalToolService private readonly terminalToolService: ITerminalToolService,
|
||||
@IVoidSettingsService private readonly voidSettingsService: IVoidSettingsService,
|
||||
) {
|
||||
super()
|
||||
}
|
||||
|
||||
|
||||
// system message
|
||||
private _generateChatMessagesSystemMessage = async (chatMode: ChatMode, specialToolFormat: 'openai-style' | 'anthropic-style' | undefined) => {
|
||||
const workspaceFolders = this.workspaceContextService.getWorkspace().folders.map(f => f.uri.fsPath)
|
||||
|
||||
const openedURIs = this.modelService.getModels().filter(m => m.isAttachedToEditor()).map(m => m.uri.fsPath) || [];
|
||||
const activeURI = this.editorService.activeEditor?.resource?.fsPath;
|
||||
|
||||
const directoryStr = await this.directoryStrService.getAllDirectoriesStr({
|
||||
cutOffMessage: chatMode === 'agent' || chatMode === 'gather' ?
|
||||
`...Directories string cut off, use tools to read more...`
|
||||
: `...Directories string cut off, ask user for more if necessary...`
|
||||
})
|
||||
const includeXMLToolDefinitions = specialToolFormat === undefined
|
||||
|
||||
const runningTerminalIds = this.terminalToolService.listTerminalIds()
|
||||
const systemMessage = chat_systemMessage({ workspaceFolders, openedURIs, directoryStr, activeURI, runningTerminalIds, chatMode, includeXMLToolDefinitions })
|
||||
return systemMessage
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
// --- LLM Chat messages ---
|
||||
|
||||
private _chatMessagesToSimpleMessages(chatMessages: ChatMessage[]): SimpleLLMMessage[] {
|
||||
const simpleLLMMessages: SimpleLLMMessage[] = []
|
||||
|
||||
for (const m of chatMessages) {
|
||||
if (m.role === 'checkpoint') continue
|
||||
if (m.role === 'interrupted_streaming_tool') continue
|
||||
if (m.role === 'assistant') {
|
||||
simpleLLMMessages.push({
|
||||
role: m.role,
|
||||
content: m.displayContent,
|
||||
anthropicReasoning: m.anthropicReasoning,
|
||||
})
|
||||
}
|
||||
else if (m.role === 'tool') {
|
||||
simpleLLMMessages.push({
|
||||
role: m.role,
|
||||
content: m.content,
|
||||
name: m.name,
|
||||
id: m.id,
|
||||
rawParams: m.rawParams,
|
||||
})
|
||||
}
|
||||
else if (m.role === 'user') {
|
||||
simpleLLMMessages.push({
|
||||
role: m.role,
|
||||
content: m.content,
|
||||
})
|
||||
}
|
||||
}
|
||||
return simpleLLMMessages
|
||||
}
|
||||
|
||||
prepareLLMSimpleMessages: IConvertToLLMMessageService['prepareLLMSimpleMessages'] = ({ simpleMessages, systemMessage, modelSelection, featureName }) => {
|
||||
if (modelSelection === null) return { messages: [], separateSystemMessage: undefined }
|
||||
const { providerName, modelName } = modelSelection
|
||||
const {
|
||||
specialToolFormat,
|
||||
contextWindow,
|
||||
supportsSystemMessage,
|
||||
} = getModelCapabilities(providerName, modelName)
|
||||
|
||||
const modelSelectionOptions = this.voidSettingsService.state.optionsOfModelSelection[featureName][modelSelection.providerName]?.[modelSelection.modelName]
|
||||
const aiInstructions = this.voidSettingsService.state.globalSettings.aiInstructions
|
||||
|
||||
const isReasoningEnabled = getIsReasoningEnabledState(featureName, providerName, modelName, modelSelectionOptions)
|
||||
const maxOutputTokens = getMaxOutputTokens(providerName, modelName, { isReasoningEnabled })
|
||||
|
||||
const { messages, separateSystemMessage } = prepareMessages({
|
||||
messages: simpleMessages,
|
||||
systemMessage,
|
||||
aiInstructions,
|
||||
supportsSystemMessage,
|
||||
specialToolFormat,
|
||||
supportsAnthropicReasoning: providerName === 'anthropic',
|
||||
contextWindow,
|
||||
maxOutputTokens,
|
||||
})
|
||||
return { messages, separateSystemMessage };
|
||||
|
||||
}
|
||||
prepareLLMChatMessages: IConvertToLLMMessageService['prepareLLMChatMessages'] = async ({ chatMessages, chatMode, modelSelection }) => {
|
||||
if (modelSelection === null) return { messages: [], separateSystemMessage: undefined }
|
||||
const { providerName, modelName } = modelSelection
|
||||
const {
|
||||
specialToolFormat,
|
||||
contextWindow,
|
||||
supportsSystemMessage,
|
||||
} = getModelCapabilities(providerName, modelName)
|
||||
const systemMessage = await this._generateChatMessagesSystemMessage(chatMode, specialToolFormat)
|
||||
|
||||
const modelSelectionOptions = this.voidSettingsService.state.optionsOfModelSelection['Chat'][modelSelection.providerName]?.[modelSelection.modelName]
|
||||
const aiInstructions = this.voidSettingsService.state.globalSettings.aiInstructions
|
||||
|
||||
const isReasoningEnabled = getIsReasoningEnabledState('Chat', providerName, modelName, modelSelectionOptions)
|
||||
const maxOutputTokens = getMaxOutputTokens(providerName, modelName, { isReasoningEnabled })
|
||||
const llmMessages = this._chatMessagesToSimpleMessages(chatMessages)
|
||||
|
||||
const { messages, separateSystemMessage } = prepareMessages({
|
||||
messages: llmMessages,
|
||||
systemMessage,
|
||||
aiInstructions,
|
||||
supportsSystemMessage,
|
||||
specialToolFormat,
|
||||
supportsAnthropicReasoning: providerName === 'anthropic',
|
||||
contextWindow,
|
||||
maxOutputTokens,
|
||||
})
|
||||
return { messages, separateSystemMessage };
|
||||
|
||||
}
|
||||
|
||||
|
||||
// --- FIM ---
|
||||
|
||||
prepareFIMMessage: IConvertToLLMMessageService['prepareFIMMessage'] = ({ messages, aiInstructions }) => {
|
||||
|
||||
let prefix = `\
|
||||
${!aiInstructions ? '' : `\
|
||||
// Instructions:
|
||||
// Do not output an explanation. Try to avoid outputting comments. Only output the middle code.
|
||||
${aiInstructions.split('\n').map(line => `//${line}`).join('\n')}`}
|
||||
|
||||
${messages.prefix}`
|
||||
|
||||
const suffix = messages.suffix
|
||||
const stopTokens = messages.stopTokens
|
||||
return { prefix, suffix, stopTokens }
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
// pick one and delete the other:
|
||||
registerSingleton(IConvertToLLMMessageService, ConvertToLLMMessageService, InstantiationType.Eager);
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/*
|
||||
Gemini has this, but they're openai-compat so we don't need to implement this
|
||||
gemini request:
|
||||
{ "role": "assistant",
|
||||
"content": null,
|
||||
"function_call": {
|
||||
"name": "get_weather",
|
||||
"arguments": {
|
||||
"latitude": 48.8566,
|
||||
"longitude": 2.3522
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
gemini response:
|
||||
{ "role": "assistant",
|
||||
"function_response": {
|
||||
"name": "get_weather",
|
||||
"response": {
|
||||
"temperature": "15°C",
|
||||
"condition": "Cloudy"
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,429 @@
|
||||
/*--------------------------------------------------------------------------------------
|
||||
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import { URI } from '../../../../base/common/uri.js';
|
||||
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 { IFileService } from '../../../../platform/files/common/files.js';
|
||||
import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js';
|
||||
import { ShallowDirectoryItem, ToolCallParams, ToolResultType } from '../common/toolsServiceTypes.js';
|
||||
import { MAX_CHILDREN_URIs_PAGE } from './toolsService.js';
|
||||
import { IExplorerService } from '../../files/browser/files.js';
|
||||
import { SortOrder } from '../../files/common/files.js';
|
||||
import { ExplorerItem } from '../../files/common/explorerModel.js';
|
||||
import { MAX_DIRSTR_CHARS_TOTAL_BEGINNING, MAX_DIRSTR_CHARS_TOTAL_TOOL } from '../common/prompt/prompts.js';
|
||||
|
||||
|
||||
const MAX_FILES_TOTAL = 300;
|
||||
|
||||
const DEFAULT_MAX_DEPTH = 3;
|
||||
const DEFAULT_MAX_ITEMS_PER_DIR = 3;
|
||||
|
||||
const START_MAX_DEPTH = Infinity;
|
||||
const START_MAX_ITEMS_PER_DIR = Infinity; // Add start value as Infinity
|
||||
|
||||
|
||||
export interface IDirectoryStrService {
|
||||
readonly _serviceBrand: undefined;
|
||||
|
||||
getDirectoryStrTool(uri: URI, options?: { maxItemsPerDir?: number }): Promise<string>
|
||||
getAllDirectoriesStr(opts: { cutOffMessage: string, maxItemsPerDir?: number }): Promise<string>
|
||||
|
||||
}
|
||||
export const IDirectoryStrService = createDecorator<IDirectoryStrService>('voidDirectoryStrService');
|
||||
|
||||
|
||||
|
||||
|
||||
// Check if it's a known filtered type like .git
|
||||
const shouldExcludeDirectory = (item: ExplorerItem) => {
|
||||
if (item.name === '.git' ||
|
||||
item.name === 'node_modules' ||
|
||||
item.name.startsWith('.') ||
|
||||
item.name === 'dist' ||
|
||||
item.name === 'build' ||
|
||||
item.name === 'out' ||
|
||||
item.name === 'bin' ||
|
||||
item.name === 'coverage' ||
|
||||
item.name === '__pycache__' ||
|
||||
item.name === 'env' ||
|
||||
item.name === 'venv' ||
|
||||
item.name === 'tmp' ||
|
||||
item.name === 'temp' ||
|
||||
item.name === 'artifacts' ||
|
||||
item.name === 'target' ||
|
||||
item.name === 'obj' ||
|
||||
item.name === 'vendor' ||
|
||||
item.name === 'logs' ||
|
||||
item.name === 'cache' ||
|
||||
item.name === 'resource' ||
|
||||
item.name === 'resources'
|
||||
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (item.name.match(/\bout\b/)) return true
|
||||
if (item.name.match(/\bbuild\b/)) return true
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// ---------- ONE LAYER DEEP ----------
|
||||
|
||||
export const computeDirectoryTree1Deep = async (
|
||||
fileService: IFileService,
|
||||
rootURI: URI,
|
||||
pageNumber: number = 1,
|
||||
): Promise<ToolResultType['ls_dir']> => {
|
||||
const stat = await fileService.resolve(rootURI, { resolveMetadata: false });
|
||||
if (!stat.isDirectory) {
|
||||
return { children: null, hasNextPage: false, hasPrevPage: false, itemsRemaining: 0 };
|
||||
}
|
||||
|
||||
const nChildren = stat.children?.length ?? 0;
|
||||
|
||||
const fromChildIdx = MAX_CHILDREN_URIs_PAGE * (pageNumber - 1);
|
||||
const toChildIdx = MAX_CHILDREN_URIs_PAGE * pageNumber - 1; // INCLUSIVE
|
||||
const listChildren = stat.children?.slice(fromChildIdx, toChildIdx + 1);
|
||||
|
||||
const children: ShallowDirectoryItem[] = listChildren?.map(child => ({
|
||||
name: child.name,
|
||||
uri: child.resource,
|
||||
isDirectory: child.isDirectory,
|
||||
isSymbolicLink: child.isSymbolicLink
|
||||
})) ?? [];
|
||||
|
||||
const hasNextPage = (nChildren - 1) > toChildIdx;
|
||||
const hasPrevPage = pageNumber > 1;
|
||||
const itemsRemaining = Math.max(0, nChildren - (toChildIdx + 1));
|
||||
|
||||
return {
|
||||
children,
|
||||
hasNextPage,
|
||||
hasPrevPage,
|
||||
itemsRemaining
|
||||
};
|
||||
};
|
||||
|
||||
export const stringifyDirectoryTree1Deep = (params: ToolCallParams['ls_dir'], result: ToolResultType['ls_dir']): string => {
|
||||
if (!result.children) {
|
||||
return `Error: ${params.rootURI} is not a directory`;
|
||||
}
|
||||
|
||||
let output = '';
|
||||
const entries = result.children;
|
||||
|
||||
if (!result.hasPrevPage) { // is first page
|
||||
output += `${params.rootURI.fsPath}\n`;
|
||||
}
|
||||
|
||||
for (let i = 0; i < entries.length; i++) {
|
||||
const entry = entries[i];
|
||||
const isLast = i === entries.length - 1 && !result.hasNextPage;
|
||||
const prefix = isLast ? '└── ' : '├── ';
|
||||
|
||||
output += `${prefix}${entry.name}${entry.isDirectory ? '/' : ''}${entry.isSymbolicLink ? ' (symbolic link)' : ''}\n`;
|
||||
}
|
||||
|
||||
if (result.hasNextPage) {
|
||||
output += `└── (${result.itemsRemaining} results remaining...)\n`;
|
||||
}
|
||||
|
||||
return output;
|
||||
};
|
||||
|
||||
|
||||
// ---------- IN GENERAL ----------
|
||||
|
||||
// Remove the old computeDirectoryTree function and replace with a combined version that handles both computation and rendering
|
||||
const computeAndStringifyDirectoryTree = async (
|
||||
eItem: ExplorerItem,
|
||||
explorerService: IExplorerService,
|
||||
MAX_CHARS: number,
|
||||
fileCount: { count: number } = { count: 0 },
|
||||
options: { maxDepth?: number, currentDepth?: number, maxItemsPerDir?: number } = {}
|
||||
): Promise<{ content: string, wasCutOff: boolean }> => {
|
||||
// Set default values for options
|
||||
const maxDepth = options.maxDepth ?? DEFAULT_MAX_DEPTH;
|
||||
const currentDepth = options.currentDepth ?? 0;
|
||||
const maxItemsPerDir = options.maxItemsPerDir ?? DEFAULT_MAX_ITEMS_PER_DIR;
|
||||
|
||||
// Check if we've reached the max depth
|
||||
if (currentDepth > maxDepth) {
|
||||
return { content: '', wasCutOff: true };
|
||||
}
|
||||
|
||||
// Check if we've reached the file limit
|
||||
if (fileCount.count >= MAX_FILES_TOTAL) {
|
||||
return { content: '', wasCutOff: true };
|
||||
}
|
||||
|
||||
// If we're already exceeding the max characters, return immediately
|
||||
if (MAX_CHARS <= 0) {
|
||||
return { content: '', wasCutOff: true };
|
||||
}
|
||||
|
||||
// Increment file count
|
||||
fileCount.count++;
|
||||
|
||||
// Add the root node first (without tree characters)
|
||||
const nodeLine = `${eItem.name}${eItem.isDirectory ? '/' : ''}${eItem.isSymbolicLink ? ' (symbolic link)' : ''}\n`;
|
||||
|
||||
if (nodeLine.length > MAX_CHARS) {
|
||||
return { content: '', wasCutOff: true };
|
||||
}
|
||||
|
||||
let content = nodeLine;
|
||||
let wasCutOff = false;
|
||||
let remainingChars = MAX_CHARS - nodeLine.length;
|
||||
|
||||
// Check if it's a directory we should skip
|
||||
const isGitIgnoredDirectory = eItem.isDirectory && shouldExcludeDirectory(eItem);
|
||||
|
||||
// Fetch and process children if not a filtered directory
|
||||
if (eItem.isDirectory && !isGitIgnoredDirectory) {
|
||||
// Fetch children with Modified sort order to show recently modified first
|
||||
const eChildren = await eItem.fetchChildren(SortOrder.Modified);
|
||||
|
||||
// Then recursively add all children with proper tree formatting
|
||||
if (eChildren && eChildren.length > 0) {
|
||||
const { childrenContent, childrenCutOff } = await renderChildrenCombined(
|
||||
eChildren,
|
||||
remainingChars,
|
||||
'',
|
||||
explorerService,
|
||||
fileCount,
|
||||
{ maxDepth, currentDepth, maxItemsPerDir } // Pass maxItemsPerDir to the render function
|
||||
);
|
||||
content += childrenContent;
|
||||
wasCutOff = childrenCutOff;
|
||||
}
|
||||
}
|
||||
|
||||
return { content, wasCutOff };
|
||||
};
|
||||
|
||||
// Helper function to render children with proper tree formatting
|
||||
const renderChildrenCombined = async (
|
||||
children: ExplorerItem[],
|
||||
maxChars: number,
|
||||
parentPrefix: string,
|
||||
explorerService: IExplorerService,
|
||||
fileCount: { count: number },
|
||||
options: { maxDepth: number, currentDepth: number, maxItemsPerDir?: number }
|
||||
): Promise<{ childrenContent: string, childrenCutOff: boolean }> => {
|
||||
const { maxDepth, currentDepth } = options; // Remove maxItemsPerDir from destructuring
|
||||
// Get maxItemsPerDir separately and make sure we use it
|
||||
// For first level (currentDepth = 0), always use Infinity regardless of what was passed
|
||||
const maxItemsPerDir = currentDepth === 0 ?
|
||||
Infinity :
|
||||
(options.maxItemsPerDir ?? DEFAULT_MAX_ITEMS_PER_DIR);
|
||||
const nextDepth = currentDepth + 1;
|
||||
|
||||
let childrenContent = '';
|
||||
let childrenCutOff = false;
|
||||
let remainingChars = maxChars;
|
||||
|
||||
// Check if we've reached max depth
|
||||
if (nextDepth > maxDepth) {
|
||||
return { childrenContent: '', childrenCutOff: true };
|
||||
}
|
||||
|
||||
// Apply maxItemsPerDir limit - only process the specified number of items
|
||||
const itemsToProcess = maxItemsPerDir === Infinity ? children : children.slice(0, maxItemsPerDir);
|
||||
const hasMoreItems = children.length > itemsToProcess.length;
|
||||
|
||||
for (let i = 0; i < itemsToProcess.length; i++) {
|
||||
// Check if we've reached the file limit
|
||||
if (fileCount.count >= MAX_FILES_TOTAL) {
|
||||
childrenCutOff = true;
|
||||
break;
|
||||
}
|
||||
|
||||
const child = itemsToProcess[i];
|
||||
const isLast = (i === itemsToProcess.length - 1) && !hasMoreItems;
|
||||
|
||||
// Create the tree branch symbols
|
||||
const branchSymbol = isLast ? '└── ' : '├── ';
|
||||
const childLine = `${parentPrefix}${branchSymbol}${child.name}${child.isDirectory ? '/' : ''}${child.isSymbolicLink ? ' (symbolic link)' : ''}\n`;
|
||||
|
||||
// Check if adding this line would exceed the limit
|
||||
if (childLine.length > remainingChars) {
|
||||
childrenCutOff = true;
|
||||
break;
|
||||
}
|
||||
|
||||
childrenContent += childLine;
|
||||
remainingChars -= childLine.length;
|
||||
fileCount.count++;
|
||||
|
||||
const nextLevelPrefix = parentPrefix + (isLast ? ' ' : '│ ');
|
||||
|
||||
// Skip processing children for git ignored directories
|
||||
const isGitIgnoredDirectory = child.isDirectory && shouldExcludeDirectory(child);
|
||||
|
||||
// Create the prefix for the next level (continuation line or space)
|
||||
if (child.isDirectory && !isGitIgnoredDirectory) {
|
||||
// Fetch children with Modified sort order to show recently modified first
|
||||
const eChildren = await child.fetchChildren(SortOrder.Modified);
|
||||
|
||||
if (eChildren && eChildren.length > 0) {
|
||||
const {
|
||||
childrenContent: grandChildrenContent,
|
||||
childrenCutOff: grandChildrenCutOff
|
||||
} = await renderChildrenCombined(
|
||||
eChildren,
|
||||
remainingChars,
|
||||
nextLevelPrefix,
|
||||
explorerService,
|
||||
fileCount,
|
||||
{ maxDepth, currentDepth: nextDepth, maxItemsPerDir }
|
||||
);
|
||||
|
||||
if (grandChildrenContent.length > 0) {
|
||||
childrenContent += grandChildrenContent;
|
||||
remainingChars -= grandChildrenContent.length;
|
||||
}
|
||||
|
||||
if (grandChildrenCutOff) {
|
||||
childrenCutOff = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add a message if we truncated the items due to maxItemsPerDir
|
||||
if (hasMoreItems) {
|
||||
const remainingCount = children.length - itemsToProcess.length;
|
||||
const truncatedLine = `${parentPrefix}└── (${remainingCount} more items not shown...)\n`;
|
||||
|
||||
if (truncatedLine.length <= remainingChars) {
|
||||
childrenContent += truncatedLine;
|
||||
remainingChars -= truncatedLine.length;
|
||||
}
|
||||
childrenCutOff = true;
|
||||
}
|
||||
|
||||
return { childrenContent, childrenCutOff };
|
||||
};
|
||||
|
||||
|
||||
// ---------------------------------------------------
|
||||
|
||||
|
||||
class DirectoryStrService extends Disposable implements IDirectoryStrService {
|
||||
_serviceBrand: undefined;
|
||||
|
||||
constructor(
|
||||
@IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService,
|
||||
@IExplorerService private readonly explorerService: IExplorerService,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
async getDirectoryStrTool(uri: URI, options?: { maxItemsPerDir?: number }) {
|
||||
const eRoot = this.explorerService.findClosest(uri)
|
||||
if (!eRoot) throw new Error(`There was a problem reading the URI: ${uri.fsPath}.`)
|
||||
|
||||
const maxItemsPerDir = options?.maxItemsPerDir ?? START_MAX_ITEMS_PER_DIR; // Use START_MAX_ITEMS_PER_DIR
|
||||
|
||||
// First try with START_MAX_DEPTH
|
||||
const { content: initialContent, wasCutOff: initialCutOff } = await computeAndStringifyDirectoryTree(
|
||||
eRoot,
|
||||
this.explorerService,
|
||||
MAX_DIRSTR_CHARS_TOTAL_TOOL,
|
||||
{ count: 0 },
|
||||
{ maxDepth: START_MAX_DEPTH, currentDepth: 0, maxItemsPerDir }
|
||||
);
|
||||
|
||||
// If cut off, try again with DEFAULT_MAX_DEPTH and DEFAULT_MAX_ITEMS_PER_DIR
|
||||
let content, wasCutOff;
|
||||
if (initialCutOff) {
|
||||
const result = await computeAndStringifyDirectoryTree(
|
||||
eRoot,
|
||||
this.explorerService,
|
||||
MAX_DIRSTR_CHARS_TOTAL_TOOL,
|
||||
{ count: 0 },
|
||||
{ maxDepth: DEFAULT_MAX_DEPTH, currentDepth: 0, maxItemsPerDir: DEFAULT_MAX_ITEMS_PER_DIR }
|
||||
);
|
||||
content = result.content;
|
||||
wasCutOff = result.wasCutOff;
|
||||
} else {
|
||||
content = initialContent;
|
||||
wasCutOff = initialCutOff;
|
||||
}
|
||||
|
||||
let c = content.substring(0, MAX_DIRSTR_CHARS_TOTAL_TOOL)
|
||||
c = `Directory of ${uri.fsPath}:\n${content}`
|
||||
if (wasCutOff) c = `${c}\n...Result was truncated...`
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
async getAllDirectoriesStr({ cutOffMessage, maxItemsPerDir }: { cutOffMessage: string, maxItemsPerDir?: number }) {
|
||||
let str: string = '';
|
||||
let cutOff = false;
|
||||
const folders = this.workspaceContextService.getWorkspace().folders;
|
||||
if (folders.length === 0)
|
||||
return '(NO WORKSPACE OPEN)';
|
||||
|
||||
// Use START_MAX_ITEMS_PER_DIR if not specified
|
||||
const startMaxItemsPerDir = maxItemsPerDir ?? START_MAX_ITEMS_PER_DIR;
|
||||
|
||||
for (let i = 0; i < folders.length; i += 1) {
|
||||
if (i > 0) str += '\n';
|
||||
|
||||
// this prioritizes filling 1st workspace before any other, etc
|
||||
const f = folders[i];
|
||||
str += `Directory of ${f.uri.fsPath}:\n`;
|
||||
const rootURI = f.uri;
|
||||
|
||||
const eRoot = this.explorerService.findClosestRoot(rootURI);
|
||||
if (!eRoot) continue;
|
||||
|
||||
// First try with START_MAX_DEPTH and startMaxItemsPerDir
|
||||
const { content: initialContent, wasCutOff: initialCutOff } = await computeAndStringifyDirectoryTree(
|
||||
eRoot,
|
||||
this.explorerService,
|
||||
MAX_DIRSTR_CHARS_TOTAL_BEGINNING - str.length,
|
||||
{ count: 0 },
|
||||
{ maxDepth: START_MAX_DEPTH, currentDepth: 0, maxItemsPerDir: startMaxItemsPerDir }
|
||||
);
|
||||
|
||||
// If cut off, try again with DEFAULT_MAX_DEPTH and DEFAULT_MAX_ITEMS_PER_DIR
|
||||
let content, wasCutOff;
|
||||
if (initialCutOff) {
|
||||
const result = await computeAndStringifyDirectoryTree(
|
||||
eRoot,
|
||||
this.explorerService,
|
||||
MAX_DIRSTR_CHARS_TOTAL_BEGINNING - str.length,
|
||||
{ count: 0 },
|
||||
{ maxDepth: DEFAULT_MAX_DEPTH, currentDepth: 0, maxItemsPerDir: DEFAULT_MAX_ITEMS_PER_DIR }
|
||||
);
|
||||
content = result.content;
|
||||
wasCutOff = result.wasCutOff;
|
||||
} else {
|
||||
content = initialContent;
|
||||
wasCutOff = initialCutOff;
|
||||
}
|
||||
|
||||
str += content;
|
||||
if (wasCutOff) {
|
||||
cutOff = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (cutOff) {
|
||||
return `${str}\n${cutOffMessage}`
|
||||
}
|
||||
|
||||
return str
|
||||
}
|
||||
}
|
||||
|
||||
registerSingleton(IDirectoryStrService, DirectoryStrService, InstantiationType.Delayed);
|
||||
@@ -11,7 +11,7 @@ import { ICodeEditor, IOverlayWidget, IViewZone } from '../../../../editor/brows
|
||||
// import { IUndoRedoService } from '../../../../platform/undoRedo/common/undoRedo.js';
|
||||
import { ICodeEditorService } from '../../../../editor/browser/services/codeEditorService.js';
|
||||
// import { throttle } from '../../../../base/common/decorators.js';
|
||||
import { ComputedDiff, findDiffs } from './helpers/findDiffs.js';
|
||||
import { findDiffs } from './helpers/findDiffs.js';
|
||||
import { EndOfLinePreference, IModelDecorationOptions, ITextModel } from '../../../../editor/common/model.js';
|
||||
import { IRange } from '../../../../editor/common/core/range.js';
|
||||
import { registerColor } from '../../../../platform/theme/common/colorUtils.js';
|
||||
@@ -40,13 +40,14 @@ import { ICommandService } from '../../../../platform/commands/common/commands.j
|
||||
import { ILLMMessageService } from '../common/sendLLMMessageService.js';
|
||||
import { LLMChatMessage, OnError, errorDetails } from '../common/sendLLMMessageTypes.js';
|
||||
import { IMetricsService } from '../common/metricsService.js';
|
||||
import { IEditCodeService, AddCtrlKOpts, StartApplyingOpts } from './editCodeServiceInterface.js';
|
||||
import { IEditCodeService, AddCtrlKOpts, StartApplyingOpts, CallBeforeStartApplyingOpts, } from './editCodeServiceInterface.js';
|
||||
import { IVoidSettingsService } from '../common/voidSettingsService.js';
|
||||
import { FeatureName } from '../common/voidSettingsTypes.js';
|
||||
import { IVoidModelService } from '../common/voidModelService.js';
|
||||
import { ITextFileService } from '../../../services/textfile/common/textfiles.js';
|
||||
import { deepClone } from '../../../../base/common/objects.js';
|
||||
import { acceptBg, acceptBorder, buttonFontSize, buttonTextColor, rejectBg, rejectBorder } from '../common/helpers/colors.js';
|
||||
import { DiffArea, Diff, CtrlKZone, VoidFileSnapshot, DiffAreaSnapshotEntry, diffAreaSnapshotKeys, DiffZone, TrackingZone, ComputedDiff } from '../common/editCodeServiceTypes.js';
|
||||
import { IConvertToLLMMessageService } from './convertToLLMMessageService.js';
|
||||
|
||||
const configOfBG = (color: Color) => {
|
||||
return { dark: color, light: color, hcDark: color, hcLight: color, }
|
||||
@@ -71,6 +72,21 @@ registerColor('void.sweepIdxBG', configOfBG(sweepIdxBG), '', true);
|
||||
|
||||
const numLinesOfStr = (str: string) => str.split('\n').length
|
||||
|
||||
|
||||
export const getLengthOfTextPx = ({ tabWidth, spaceWidth, content }: { tabWidth: number, spaceWidth: number, content: string }) => {
|
||||
let lengthOfTextPx = 0;
|
||||
for (const char of content) {
|
||||
if (char === '\t') {
|
||||
lengthOfTextPx += tabWidth
|
||||
} else {
|
||||
lengthOfTextPx += spaceWidth;
|
||||
}
|
||||
}
|
||||
|
||||
return lengthOfTextPx
|
||||
}
|
||||
|
||||
|
||||
const getLeadingWhitespacePx = (editor: ICodeEditor, startLine: number): number => {
|
||||
|
||||
const model = editor.getModel();
|
||||
@@ -94,28 +110,42 @@ const getLeadingWhitespacePx = (editor: ICodeEditor, startLine: number): number
|
||||
const spaceWidth = editor.getOption(EditorOption.fontInfo).spaceWidth;
|
||||
const tabWidth = numSpacesInTab * spaceWidth;
|
||||
|
||||
let paddingLeft = 0;
|
||||
for (const char of leadingWhitespace) {
|
||||
if (char === '\t') {
|
||||
paddingLeft += tabWidth
|
||||
} else if (char === ' ') {
|
||||
paddingLeft += spaceWidth;
|
||||
}
|
||||
}
|
||||
const leftWhitespacePx = getLengthOfTextPx({
|
||||
tabWidth,
|
||||
spaceWidth,
|
||||
content: leadingWhitespace
|
||||
});
|
||||
|
||||
return paddingLeft;
|
||||
|
||||
return leftWhitespacePx;
|
||||
};
|
||||
|
||||
|
||||
// Helper function to remove whitespace except newlines
|
||||
const removeWhitespaceExceptNewlines = (str: string): string => {
|
||||
return str.replace(/[^\S\n]+/g, '');
|
||||
}
|
||||
|
||||
|
||||
|
||||
// finds block.orig in fileContents and return its range in file
|
||||
// startingAtLine is 1-indexed and inclusive
|
||||
const findTextInCode = (text: string, fileContents: string, startingAtLine?: number) => {
|
||||
const idx = fileContents.indexOf(text,
|
||||
startingAtLine !== undefined ?
|
||||
fileContents.split('\n').slice(0, startingAtLine).join('\n').length // num characters in all lines before startingAtLine
|
||||
: 0
|
||||
)
|
||||
const findTextInCode = (text: string, fileContents: string, canFallbackToRemoveWhitespace: boolean, startingAtLine?: number) => {
|
||||
|
||||
const startLineIdx = (fileContents: string) => startingAtLine !== undefined ?
|
||||
fileContents.split('\n').slice(0, startingAtLine).join('\n').length // num characters in all lines before startingAtLine
|
||||
: 0
|
||||
|
||||
// idx = starting index in fileContents
|
||||
let idx = fileContents.indexOf(text, startLineIdx(fileContents))
|
||||
|
||||
// try to find it ignoring all whitespace this time
|
||||
if (idx === -1 && canFallbackToRemoveWhitespace) {
|
||||
text = removeWhitespaceExceptNewlines(text)
|
||||
fileContents = removeWhitespaceExceptNewlines(fileContents)
|
||||
idx = fileContents.indexOf(text, startLineIdx(fileContents));
|
||||
}
|
||||
|
||||
if (idx === -1) return 'Not found' as const
|
||||
const lastIdx = fileContents.lastIndexOf(text)
|
||||
if (lastIdx !== idx) return 'Not unique' as const
|
||||
@@ -127,108 +157,6 @@ const findTextInCode = (text: string, fileContents: string, startingAtLine?: num
|
||||
|
||||
|
||||
|
||||
|
||||
// // TODO diffArea should be removed if we just discovered it has no more diffs in it
|
||||
// for (const diffareaid of this.diffAreasOfURI[uri.fsPath] || []) {
|
||||
// const diffArea = this.diffAreaOfId[diffareaid]
|
||||
// if (Object.keys(diffArea._diffOfId).length === 0 && !diffArea._sweepState.isStreaming) {
|
||||
// const { onFinishEdit } = this._addToHistory(uri)
|
||||
// this._deleteDiffArea(diffArea)
|
||||
// onFinishEdit()
|
||||
// }
|
||||
// }
|
||||
|
||||
|
||||
export type Diff = {
|
||||
diffid: number;
|
||||
diffareaid: number; // the diff area this diff belongs to, "computed"
|
||||
} & ComputedDiff
|
||||
|
||||
|
||||
|
||||
// _ means anything we don't include if we clone it
|
||||
// DiffArea.originalStartLine is the line in originalCode (not the file)
|
||||
|
||||
type CommonZoneProps = {
|
||||
diffareaid: number;
|
||||
startLine: number;
|
||||
endLine: number;
|
||||
|
||||
_URI: URI; // typically we get the URI from model
|
||||
|
||||
}
|
||||
|
||||
type CtrlKZone = {
|
||||
type: 'CtrlKZone';
|
||||
originalCode?: undefined;
|
||||
|
||||
editorId: string; // the editor the input lives on
|
||||
|
||||
_mountInfo: null | {
|
||||
textAreaRef: { current: HTMLTextAreaElement | null }
|
||||
dispose: () => void;
|
||||
refresh: () => void;
|
||||
}
|
||||
|
||||
_linkedStreamingDiffZone: number | null; // diffareaid of the diffZone currently streaming here
|
||||
_removeStylesFns: Set<Function> // these don't remove diffs or this diffArea, only their styles
|
||||
|
||||
} & CommonZoneProps
|
||||
|
||||
|
||||
export type DiffZone = {
|
||||
type: 'DiffZone',
|
||||
originalCode: string;
|
||||
_diffOfId: Record<string, Diff>; // diffid -> diff in this DiffArea
|
||||
_streamState: {
|
||||
isStreaming: true;
|
||||
streamRequestIdRef: { current: string | null };
|
||||
line: number;
|
||||
} | {
|
||||
isStreaming: false;
|
||||
streamRequestIdRef?: undefined;
|
||||
line?: undefined;
|
||||
};
|
||||
editorId?: undefined;
|
||||
linkedStreamingDiffZone?: undefined;
|
||||
_removeStylesFns: Set<Function> // these don't remove diffs or this diffArea, only their styles
|
||||
} & CommonZoneProps
|
||||
|
||||
|
||||
|
||||
type TrackingZone<T> = {
|
||||
type: 'TrackingZone';
|
||||
metadata: T;
|
||||
originalCode?: undefined;
|
||||
editorId?: undefined;
|
||||
_removeStylesFns?: undefined;
|
||||
} & CommonZoneProps
|
||||
|
||||
|
||||
// called DiffArea for historical purposes, we can rename to something like TextRegion if we want
|
||||
export type DiffArea = CtrlKZone | DiffZone | TrackingZone<any>
|
||||
|
||||
const diffAreaSnapshotKeys = [
|
||||
'type',
|
||||
'diffareaid',
|
||||
'originalCode',
|
||||
'startLine',
|
||||
'endLine',
|
||||
'editorId',
|
||||
|
||||
] as const satisfies (keyof DiffArea)[]
|
||||
|
||||
type DiffAreaSnapshot<DiffAreaType extends DiffArea = DiffArea> = Pick<DiffAreaType, typeof diffAreaSnapshotKeys[number]>
|
||||
|
||||
|
||||
|
||||
type HistorySnapshot = {
|
||||
snapshottedDiffAreaOfId: Record<string, DiffAreaSnapshot>;
|
||||
entireFileCode: string;
|
||||
}
|
||||
|
||||
|
||||
|
||||
// line/col is the location, originalCodeStartLine is the start line of the original code being displayed
|
||||
type StreamLocationMutable = { line: number, col: number, addedSplitYet: boolean, originalCodeStartLine: number }
|
||||
|
||||
@@ -243,23 +171,21 @@ class EditCodeService extends Disposable implements IEditCodeService {
|
||||
diffAreaOfId: Record<string, DiffArea> = {}; // diffareaId -> diffArea
|
||||
diffOfId: Record<string, Diff> = {}; // diffid -> diff (redundant with diffArea._diffOfId)
|
||||
|
||||
|
||||
// events
|
||||
|
||||
|
||||
// uri: diffZones // listen on change diffZones
|
||||
private readonly _onDidAddOrDeleteDiffZones = new Emitter<{ uri: URI }>();
|
||||
onDidAddOrDeleteDiffZones = this._onDidAddOrDeleteDiffZones.event;
|
||||
|
||||
// diffZone: [uri], diffs, isStreaming // listen on change diffs, change streaming (uri is const)
|
||||
private readonly _onDidChangeDiffsInDiffZone = new Emitter<{ uri: URI, diffareaid: number }>();
|
||||
private readonly _onDidChangeDiffsInDiffZoneNotStreaming = new Emitter<{ uri: URI, diffareaid: number }>();
|
||||
private readonly _onDidChangeStreamingInDiffZone = new Emitter<{ uri: URI, diffareaid: number }>();
|
||||
onDidChangeDiffsInDiffZone = this._onDidChangeDiffsInDiffZone.event;
|
||||
onDidChangeDiffsInDiffZoneNotStreaming = this._onDidChangeDiffsInDiffZoneNotStreaming.event;
|
||||
onDidChangeStreamingInDiffZone = this._onDidChangeStreamingInDiffZone.event;
|
||||
|
||||
// ctrlKZone: [uri], isStreaming // listen on change streaming
|
||||
private readonly _onDidChangeStreamingInCtrlKZone = new Emitter<{ uri: URI; diffareaid: number }>();
|
||||
onDidChangeStreamingInCtrlKZone = this._onDidChangeStreamingInCtrlKZone.event
|
||||
onDidChangeStreamingInCtrlKZone = this._onDidChangeStreamingInCtrlKZone.event;
|
||||
|
||||
|
||||
constructor(
|
||||
@@ -277,7 +203,7 @@ class EditCodeService extends Disposable implements IEditCodeService {
|
||||
@IVoidSettingsService private readonly _settingsService: IVoidSettingsService,
|
||||
// @IFileService private readonly _fileService: IFileService,
|
||||
@IVoidModelService private readonly _voidModelService: IVoidModelService,
|
||||
@ITextFileService private readonly _textFileService: ITextFileService,
|
||||
@IConvertToLLMMessageService private readonly _convertToLLMMessageService: IConvertToLLMMessageService,
|
||||
) {
|
||||
super();
|
||||
|
||||
@@ -722,106 +648,114 @@ class EditCodeService extends Disposable implements IEditCodeService {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
private _getCurrentVoidFileSnapshot = (uri: URI): VoidFileSnapshot => {
|
||||
const { model } = this._voidModelService.getModel(uri)
|
||||
const snapshottedDiffAreaOfId: Record<string, DiffAreaSnapshotEntry> = {}
|
||||
|
||||
for (const diffareaid in this.diffAreaOfId) {
|
||||
const diffArea = this.diffAreaOfId[diffareaid]
|
||||
|
||||
if (diffArea._URI.fsPath !== uri.fsPath) continue
|
||||
|
||||
snapshottedDiffAreaOfId[diffareaid] = deepClone(
|
||||
Object.fromEntries(diffAreaSnapshotKeys.map(key => [key, diffArea[key]]))
|
||||
) as DiffAreaSnapshotEntry
|
||||
}
|
||||
|
||||
const entireFileCode = model ? model.getValue(EndOfLinePreference.LF) : ''
|
||||
|
||||
// this._noLongerNeedModelReference(uri)
|
||||
return {
|
||||
snapshottedDiffAreaOfId,
|
||||
entireFileCode, // the whole file's code
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private _restoreVoidFileSnapshot = async (uri: URI, snapshot: VoidFileSnapshot) => {
|
||||
// for each diffarea in this uri, stop streaming if currently streaming
|
||||
for (const diffareaid in this.diffAreaOfId) {
|
||||
const diffArea = this.diffAreaOfId[diffareaid]
|
||||
if (diffArea.type === 'DiffZone')
|
||||
this._stopIfStreaming(diffArea)
|
||||
}
|
||||
|
||||
// delete all diffareas on this uri (clearing their styles)
|
||||
this._deleteAllDiffAreas(uri)
|
||||
|
||||
const { snapshottedDiffAreaOfId, entireFileCode: entireModelCode } = deepClone(snapshot) // don't want to destroy the snapshot
|
||||
|
||||
// restore diffAreaOfId and diffAreasOfModelId
|
||||
for (const diffareaid in snapshottedDiffAreaOfId) {
|
||||
|
||||
const snapshottedDiffArea = snapshottedDiffAreaOfId[diffareaid]
|
||||
|
||||
if (snapshottedDiffArea.type === 'DiffZone') {
|
||||
this.diffAreaOfId[diffareaid] = {
|
||||
...snapshottedDiffArea as DiffAreaSnapshotEntry<DiffZone>,
|
||||
type: 'DiffZone',
|
||||
_diffOfId: {},
|
||||
_URI: uri,
|
||||
_streamState: { isStreaming: false }, // when restoring, we will never be streaming
|
||||
_removeStylesFns: new Set(),
|
||||
}
|
||||
}
|
||||
else if (snapshottedDiffArea.type === 'CtrlKZone') {
|
||||
this.diffAreaOfId[diffareaid] = {
|
||||
...snapshottedDiffArea as DiffAreaSnapshotEntry<CtrlKZone>,
|
||||
_URI: uri,
|
||||
_removeStylesFns: new Set<Function>(),
|
||||
_mountInfo: null,
|
||||
_linkedStreamingDiffZone: null, // when restoring, we will never be streaming
|
||||
}
|
||||
}
|
||||
this._addOrInitializeDiffAreaAtURI(uri, diffareaid)
|
||||
}
|
||||
this._onDidAddOrDeleteDiffZones.fire({ uri })
|
||||
|
||||
// restore file content
|
||||
this._writeURIText(uri, entireModelCode,
|
||||
'wholeFileRange',
|
||||
{ shouldRealignDiffAreas: false }
|
||||
)
|
||||
// this._noLongerNeedModelReference(uri)
|
||||
}
|
||||
|
||||
private _addToHistory(uri: URI, opts?: { onWillUndo?: () => void }) {
|
||||
|
||||
const getCurrentSnapshot = (): HistorySnapshot => {
|
||||
|
||||
const { model } = this._voidModelService.getModel(uri)
|
||||
const snapshottedDiffAreaOfId: Record<string, DiffAreaSnapshot> = {}
|
||||
|
||||
for (const diffareaid in this.diffAreaOfId) {
|
||||
const diffArea = this.diffAreaOfId[diffareaid]
|
||||
|
||||
if (diffArea._URI.fsPath !== uri.fsPath) continue
|
||||
|
||||
snapshottedDiffAreaOfId[diffareaid] = deepClone(
|
||||
Object.fromEntries(diffAreaSnapshotKeys.map(key => [key, diffArea[key]]))
|
||||
) as DiffAreaSnapshot
|
||||
}
|
||||
|
||||
const entireFileCode = model ? model.getValue(EndOfLinePreference.LF) : ''
|
||||
|
||||
// this._noLongerNeedModelReference(uri)
|
||||
return {
|
||||
snapshottedDiffAreaOfId,
|
||||
entireFileCode, // the whole file's code
|
||||
}
|
||||
}
|
||||
|
||||
const restoreDiffAreas = async (snapshot: HistorySnapshot) => {
|
||||
|
||||
// for each diffarea in this uri, stop streaming if currently streaming
|
||||
for (const diffareaid in this.diffAreaOfId) {
|
||||
const diffArea = this.diffAreaOfId[diffareaid]
|
||||
if (diffArea.type === 'DiffZone')
|
||||
this._stopIfStreaming(diffArea)
|
||||
}
|
||||
|
||||
// delete all diffareas on this uri (clearing their styles)
|
||||
this._deleteAllDiffAreas(uri)
|
||||
this.diffAreasOfURI[uri.fsPath]?.clear()
|
||||
|
||||
const { snapshottedDiffAreaOfId, entireFileCode: entireModelCode } = deepClone(snapshot) // don't want to destroy the snapshot
|
||||
|
||||
// restore diffAreaOfId and diffAreasOfModelId
|
||||
for (const diffareaid in snapshottedDiffAreaOfId) {
|
||||
|
||||
const snapshottedDiffArea = snapshottedDiffAreaOfId[diffareaid]
|
||||
|
||||
if (snapshottedDiffArea.type === 'DiffZone') {
|
||||
this.diffAreaOfId[diffareaid] = {
|
||||
...snapshottedDiffArea as DiffAreaSnapshot<DiffZone>,
|
||||
type: 'DiffZone',
|
||||
_diffOfId: {},
|
||||
_URI: uri,
|
||||
_streamState: { isStreaming: false }, // when restoring, we will never be streaming
|
||||
_removeStylesFns: new Set(),
|
||||
}
|
||||
}
|
||||
else if (snapshottedDiffArea.type === 'CtrlKZone') {
|
||||
this.diffAreaOfId[diffareaid] = {
|
||||
...snapshottedDiffArea as DiffAreaSnapshot<CtrlKZone>,
|
||||
_URI: uri,
|
||||
_removeStylesFns: new Set<Function>(),
|
||||
_mountInfo: null,
|
||||
_linkedStreamingDiffZone: null, // when restoring, we will never be streaming
|
||||
}
|
||||
}
|
||||
this._addOrInitializeDiffAreaAtURI(uri, diffareaid)
|
||||
}
|
||||
this._onDidAddOrDeleteDiffZones.fire({ uri })
|
||||
|
||||
// restore file content
|
||||
this._writeURIText(uri, entireModelCode,
|
||||
'wholeFileRange',
|
||||
{ shouldRealignDiffAreas: false }
|
||||
)
|
||||
// this._noLongerNeedModelReference(uri)
|
||||
}
|
||||
|
||||
const beforeSnapshot: HistorySnapshot = getCurrentSnapshot()
|
||||
let afterSnapshot: HistorySnapshot | null = null
|
||||
const beforeSnapshot: VoidFileSnapshot = this._getCurrentVoidFileSnapshot(uri)
|
||||
let afterSnapshot: VoidFileSnapshot | null = null
|
||||
|
||||
const elt: IUndoRedoElement = {
|
||||
type: UndoRedoElementType.Resource,
|
||||
resource: uri,
|
||||
label: 'Void Agent',
|
||||
code: 'undoredo.editCode',
|
||||
undo: () => { opts?.onWillUndo?.(); restoreDiffAreas(beforeSnapshot); },
|
||||
redo: () => { if (afterSnapshot) restoreDiffAreas(afterSnapshot) }
|
||||
undo: async () => { opts?.onWillUndo?.(); await this._restoreVoidFileSnapshot(uri, beforeSnapshot) },
|
||||
redo: async () => { if (afterSnapshot) await this._restoreVoidFileSnapshot(uri, afterSnapshot) }
|
||||
}
|
||||
this._undoRedoService.pushElement(elt)
|
||||
|
||||
const onFinishEdit = async () => {
|
||||
afterSnapshot = getCurrentSnapshot()
|
||||
await this._textFileService.save(uri, { // we want [our change] -> [save] so it's all treated as one change.
|
||||
skipSaveParticipants: true // avoid triggering extensions etc (if they reformat the page, it will add another item to the undo stack)
|
||||
})
|
||||
afterSnapshot = this._getCurrentVoidFileSnapshot(uri)
|
||||
await this._voidModelService.saveModel(uri)
|
||||
}
|
||||
return { onFinishEdit }
|
||||
}
|
||||
|
||||
|
||||
public getVoidFileSnapshot(uri: URI) {
|
||||
return this._getCurrentVoidFileSnapshot(uri)
|
||||
}
|
||||
|
||||
|
||||
public restoreVoidFileSnapshot(uri: URI, snapshot: VoidFileSnapshot): void {
|
||||
this._restoreVoidFileSnapshot(uri, snapshot)
|
||||
}
|
||||
|
||||
|
||||
// delete diffOfId and diffArea._diffOfId
|
||||
private _deleteDiff(diff: Diff) {
|
||||
const diffArea = this.diffAreaOfId[diff.diffareaid]
|
||||
@@ -886,6 +820,7 @@ class EditCodeService extends Disposable implements IEditCodeService {
|
||||
else if (diffArea.type === 'CtrlKZone')
|
||||
this._deleteCtrlKZone(diffArea)
|
||||
})
|
||||
this.diffAreasOfURI[uri.fsPath]?.clear()
|
||||
}
|
||||
|
||||
private _addOrInitializeDiffAreaAtURI = (uri: URI, diffareaid: string | number) => {
|
||||
@@ -994,7 +929,7 @@ class EditCodeService extends Disposable implements IEditCodeService {
|
||||
if (diffArea?.type !== 'DiffZone') continue
|
||||
// fire changed diffs (this is the only place Diffs are added)
|
||||
if (!diffArea._streamState.isStreaming) {
|
||||
this._onDidChangeDiffsInDiffZone.fire({ uri, diffareaid: diffArea.diffareaid })
|
||||
this._onDidChangeDiffsInDiffZoneNotStreaming.fire({ uri, diffareaid: diffArea.diffareaid })
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1160,22 +1095,51 @@ class EditCodeService extends Disposable implements IEditCodeService {
|
||||
|
||||
|
||||
|
||||
private _getURIBeforeStartApplying(opts: CallBeforeStartApplyingOpts) {
|
||||
// SR
|
||||
if (opts.from === 'ClickApply') {
|
||||
const uri = this._uriOfGivenURI(opts.uri)
|
||||
if (!uri) return
|
||||
return uri
|
||||
}
|
||||
else if (opts.from === 'QuickEdit') {
|
||||
const { diffareaid } = opts
|
||||
const ctrlKZone = this.diffAreaOfId[diffareaid]
|
||||
if (ctrlKZone?.type !== 'CtrlKZone') return
|
||||
const { _URI: uri } = ctrlKZone
|
||||
return uri
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
public async callBeforeStartApplying(opts: CallBeforeStartApplyingOpts) {
|
||||
const uri = this._getURIBeforeStartApplying(opts)
|
||||
if (!uri) return
|
||||
await this._voidModelService.initializeModel(uri)
|
||||
await this._voidModelService.saveModel(uri) // save the URI
|
||||
}
|
||||
|
||||
|
||||
// the applyDonePromise this returns can reject, and should be caught with .catch
|
||||
public async startApplying(opts: StartApplyingOpts): Promise<[URI, Promise<void>] | null> {
|
||||
public startApplying(opts: StartApplyingOpts): [URI, Promise<void>] | null {
|
||||
let res: [DiffZone, Promise<void>] | undefined = undefined
|
||||
|
||||
if (opts.from === 'QuickEdit') {
|
||||
res = await this._initializeWriteoverStream(opts) // rewrite
|
||||
res = this._initializeWriteoverStream(opts) // rewrite
|
||||
}
|
||||
else if (opts.from === 'ClickApply') {
|
||||
if (this._settingsService.state.globalSettings.enableFastApply) {
|
||||
res = await this._initializeSearchAndReplaceStream(opts) // fast apply
|
||||
const numCharsInFile = this._fileLengthOfGivenURI(opts.uri)
|
||||
if (numCharsInFile === null) return null
|
||||
if (numCharsInFile < 1000) { // slow apply for short files (especially important for empty files)
|
||||
res = this._initializeWriteoverStream(opts)
|
||||
}
|
||||
else {
|
||||
res = this._initializeSearchAndReplaceStream(opts) // fast apply
|
||||
}
|
||||
}
|
||||
else {
|
||||
res = await this._initializeWriteoverStream(opts) // rewrite
|
||||
res = this._initializeWriteoverStream(opts) // rewrite
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1301,19 +1265,21 @@ class EditCodeService extends Disposable implements IEditCodeService {
|
||||
}
|
||||
|
||||
|
||||
private async _initializeWriteoverStream(opts: StartApplyingOpts): Promise<[DiffZone, Promise<void>] | undefined> {
|
||||
private _initializeWriteoverStream(opts: StartApplyingOpts): [DiffZone, Promise<void>] | undefined {
|
||||
|
||||
const { from, } = opts
|
||||
const featureName: FeatureName = opts.from === 'ClickApply' ? 'Apply' : 'Ctrl+K'
|
||||
const modelSelection = this._settingsService.state.modelSelectionOfFeature[featureName]
|
||||
const modelSelectionOptions = modelSelection ? this._settingsService.state.optionsOfModelSelection[featureName][modelSelection.providerName]?.[modelSelection.modelName] : undefined
|
||||
|
||||
|
||||
const uri = this._getURIBeforeStartApplying(opts)
|
||||
if (!uri) return
|
||||
|
||||
let uri: URI
|
||||
let startRange: 'fullFile' | [number, number]
|
||||
|
||||
let ctrlKZoneIfQuickEdit: CtrlKZone | null = null
|
||||
|
||||
if (from === 'ClickApply') {
|
||||
const uri_ = this._getActiveEditorURI()
|
||||
if (!uri_) return
|
||||
uri = uri_
|
||||
startRange = 'fullFile'
|
||||
}
|
||||
else if (from === 'QuickEdit') {
|
||||
@@ -1321,15 +1287,13 @@ class EditCodeService extends Disposable implements IEditCodeService {
|
||||
const ctrlKZone = this.diffAreaOfId[diffareaid]
|
||||
if (ctrlKZone?.type !== 'CtrlKZone') return
|
||||
ctrlKZoneIfQuickEdit = ctrlKZone
|
||||
const { startLine: startLine_, endLine: endLine_, _URI } = ctrlKZone
|
||||
uri = _URI
|
||||
const { startLine: startLine_, endLine: endLine_ } = ctrlKZone
|
||||
startRange = [startLine_, endLine_]
|
||||
}
|
||||
else {
|
||||
throw new Error(`Void: diff.type not recognized on: ${from}`)
|
||||
}
|
||||
|
||||
await this._voidModelService.initializeModel(uri)
|
||||
const { model } = this._voidModelService.getModel(uri)
|
||||
if (!model) return
|
||||
|
||||
@@ -1341,12 +1305,16 @@ class EditCodeService extends Disposable implements IEditCodeService {
|
||||
const originalCode = startRange === 'fullFile' ? originalFileCode : originalFileCode.split('\n').slice((startRange[0] - 1), (startRange[1] - 1) + 1).join('\n')
|
||||
const language = model.getLanguageId()
|
||||
let messages: LLMChatMessage[]
|
||||
let separateSystemMessage: string | undefined
|
||||
if (from === 'ClickApply') {
|
||||
const userContent = rewriteCode_userMessage({ originalCode, applyStr: opts.applyStr, language })
|
||||
messages = [
|
||||
{ role: 'system', content: rewriteCode_systemMessage, },
|
||||
{ role: 'user', content: userContent, }
|
||||
]
|
||||
const { messages: a, separateSystemMessage: b } = this._convertToLLMMessageService.prepareLLMSimpleMessages({
|
||||
systemMessage: rewriteCode_systemMessage,
|
||||
simpleMessages: [{ role: 'user', content: rewriteCode_userMessage({ originalCode, applyStr: opts.applyStr, language }), }],
|
||||
featureName,
|
||||
modelSelection,
|
||||
})
|
||||
messages = a
|
||||
separateSystemMessage = b
|
||||
}
|
||||
else if (from === 'QuickEdit') {
|
||||
if (!ctrlKZoneIfQuickEdit) return
|
||||
@@ -1356,12 +1324,17 @@ class EditCodeService extends Disposable implements IEditCodeService {
|
||||
const startLine = startRange === 'fullFile' ? 1 : startRange[0]
|
||||
const endLine = startRange === 'fullFile' ? model.getLineCount() : startRange[1]
|
||||
const { prefix, suffix } = voidPrefixAndSuffix({ fullFileStr: originalFileCode, startLine, endLine })
|
||||
const userContent = ctrlKStream_userMessage({ selection: originalCode, instructions: instructions, prefix, suffix, isOllamaFIM: false, fimTags: quickEditFIMTags, language })
|
||||
// type: 'messages',
|
||||
messages = [
|
||||
{ role: 'system', content: ctrlKStream_systemMessage({ quickEditFIMTags: quickEditFIMTags }), },
|
||||
{ role: 'user', content: userContent, }
|
||||
]
|
||||
const userContent = ctrlKStream_userMessage({ selection: originalCode, instructions: instructions, prefix, suffix, fimTags: quickEditFIMTags, language })
|
||||
|
||||
const { messages: a, separateSystemMessage: b } = this._convertToLLMMessageService.prepareLLMSimpleMessages({
|
||||
systemMessage: ctrlKStream_systemMessage({ quickEditFIMTags: quickEditFIMTags }),
|
||||
simpleMessages: [{ role: 'user', content: userContent, }],
|
||||
featureName,
|
||||
modelSelection,
|
||||
})
|
||||
messages = a
|
||||
separateSystemMessage = b
|
||||
|
||||
}
|
||||
else { throw new Error(`featureName ${from} is invalid`) }
|
||||
|
||||
@@ -1425,10 +1398,6 @@ class EditCodeService extends Disposable implements IEditCodeService {
|
||||
|
||||
const latestStreamLocationMutable: StreamLocationMutable = { line: diffZone.startLine, addedSplitYet: false, col: 1, originalCodeStartLine: 1 }
|
||||
|
||||
const featureName: FeatureName = opts.from === 'ClickApply' ? 'Apply' : 'Ctrl+K'
|
||||
const modelSelection = this._settingsService.state.modelSelectionOfFeature[featureName]
|
||||
const modelSelectionOptions = modelSelection ? this._settingsService.state.optionsOfModelSelection[modelSelection.providerName]?.[modelSelection.modelName] : undefined
|
||||
|
||||
// allowed to throw errors - this is called inside a promise that handles everything
|
||||
const runWriteover = async () => {
|
||||
let shouldSendAnotherMessage = true
|
||||
@@ -1451,6 +1420,8 @@ class EditCodeService extends Disposable implements IEditCodeService {
|
||||
messages,
|
||||
modelSelection,
|
||||
modelSelectionOptions,
|
||||
separateSystemMessage,
|
||||
chatMode: null, // not chat
|
||||
onText: (params) => {
|
||||
const { fullText: fullText_ } = params
|
||||
const newText_ = fullText_.substring(fullTextSoFar.length, Infinity)
|
||||
@@ -1505,21 +1476,33 @@ class EditCodeService extends Disposable implements IEditCodeService {
|
||||
|
||||
|
||||
|
||||
|
||||
private async _initializeSearchAndReplaceStream(opts: StartApplyingOpts & { from: 'ClickApply' }): Promise<[DiffZone, Promise<void>] | undefined> {
|
||||
const { from, applyStr, uri: givenURI, } = opts
|
||||
let uri: URI
|
||||
|
||||
_uriOfGivenURI(givenURI: URI | 'current') {
|
||||
if (givenURI === 'current') {
|
||||
const uri_ = this._getActiveEditorURI()
|
||||
if (!uri_) return
|
||||
uri = uri_
|
||||
}
|
||||
else {
|
||||
uri = givenURI
|
||||
return uri_
|
||||
}
|
||||
return givenURI
|
||||
}
|
||||
_fileLengthOfGivenURI(givenURI: URI | 'current') {
|
||||
const uri = this._uriOfGivenURI(givenURI)
|
||||
if (!uri) return null
|
||||
const { model } = this._voidModelService.getModel(uri)
|
||||
if (!model) return null
|
||||
const numCharsInFile = model.getValueLength(EndOfLinePreference.LF)
|
||||
return numCharsInFile
|
||||
}
|
||||
|
||||
|
||||
private _initializeSearchAndReplaceStream(opts: StartApplyingOpts & { from: 'ClickApply' }): [DiffZone, Promise<void>] | undefined {
|
||||
const { from, applyStr, } = opts
|
||||
const featureName: FeatureName = 'Apply'
|
||||
const modelSelection = this._settingsService.state.modelSelectionOfFeature[featureName]
|
||||
const modelSelectionOptions = modelSelection ? this._settingsService.state.optionsOfModelSelection[featureName][modelSelection.providerName]?.[modelSelection.modelName] : undefined
|
||||
|
||||
const uri = this._getURIBeforeStartApplying(opts)
|
||||
if (!uri) return
|
||||
|
||||
await this._voidModelService.initializeModel(uri)
|
||||
const { model } = this._voidModelService.getModel(uri)
|
||||
if (!model) return
|
||||
|
||||
@@ -1529,10 +1512,13 @@ class EditCodeService extends Disposable implements IEditCodeService {
|
||||
// build messages - ask LLM to generate search/replace block text
|
||||
const originalFileCode = model.getValue(EndOfLinePreference.LF)
|
||||
const userMessageContent = searchReplace_userMessage({ originalCode: originalFileCode, applyStr: applyStr })
|
||||
const messages: LLMChatMessage[] = [
|
||||
{ role: 'system', content: searchReplace_systemMessage },
|
||||
{ role: 'user', content: userMessageContent },
|
||||
]
|
||||
|
||||
const { messages, separateSystemMessage: separateSystemMessage } = this._convertToLLMMessageService.prepareLLMSimpleMessages({
|
||||
systemMessage: searchReplace_systemMessage,
|
||||
simpleMessages: [{ role: 'user', content: userMessageContent, }],
|
||||
featureName,
|
||||
modelSelection,
|
||||
})
|
||||
|
||||
// if URI is already streaming, return (should never happen, caller is responsible for checking)
|
||||
if (this._uriIsStreaming(uri)) return
|
||||
@@ -1611,7 +1597,7 @@ class EditCodeService extends Disposable implements IEditCodeService {
|
||||
this._notifyError(e)
|
||||
onDone()
|
||||
this._undoHistory(uri)
|
||||
throw e.fullError // throw error h
|
||||
throw e.fullError || new Error(e.message) // throw error h
|
||||
}
|
||||
|
||||
// refresh now in case onText takes a while to get 1st message
|
||||
@@ -1624,11 +1610,7 @@ class EditCodeService extends Disposable implements IEditCodeService {
|
||||
const addedTrackingZoneOfBlockNum: TrackingZone<SearchReplaceDiffAreaMetadata>[] = []
|
||||
diffZone._streamState.line = 1
|
||||
|
||||
const featureName: FeatureName = 'Apply'
|
||||
const modelSelection = this._settingsService.state.modelSelectionOfFeature[featureName]
|
||||
const modelSelectionOptions = modelSelection ? this._settingsService.state.optionsOfModelSelection[modelSelection.providerName]?.[modelSelection.modelName] : undefined
|
||||
|
||||
const N_RETRIES = 5
|
||||
const N_RETRIES = 2
|
||||
|
||||
// allowed to throw errors - this is called inside a promise that handles everything
|
||||
const runSearchReplace = async () => {
|
||||
@@ -1659,6 +1641,8 @@ class EditCodeService extends Disposable implements IEditCodeService {
|
||||
messages,
|
||||
modelSelection,
|
||||
modelSelectionOptions,
|
||||
separateSystemMessage,
|
||||
chatMode: null, // not chat
|
||||
onText: (params) => {
|
||||
const { fullText } = params
|
||||
// blocks are [done done done ... {writingFinal|writingOriginal}]
|
||||
@@ -1674,7 +1658,7 @@ class EditCodeService extends Disposable implements IEditCodeService {
|
||||
// update stream state to the first line of original if some portion of original has been written
|
||||
if (shouldUpdateOrigStreamStyle && block.orig.trim().length >= 20) {
|
||||
const startingAtLine = diffZone._streamState.line ?? 1 // dont go backwards if already have a stream line
|
||||
const originalRange = findTextInCode(block.orig, originalFileCode, startingAtLine)
|
||||
const originalRange = findTextInCode(block.orig, originalFileCode, false, startingAtLine)
|
||||
if (typeof originalRange !== 'string') {
|
||||
const [startLine, _] = convertOriginalRangeToFinalRange(originalRange)
|
||||
diffZone._streamState.line = startLine
|
||||
@@ -1701,7 +1685,7 @@ class EditCodeService extends Disposable implements IEditCodeService {
|
||||
if (!(blockNum in addedTrackingZoneOfBlockNum)) {
|
||||
|
||||
|
||||
const originalBounds = findTextInCode(block.orig, originalFileCode)
|
||||
const originalBounds = findTextInCode(block.orig, originalFileCode, true)
|
||||
// if error
|
||||
if (typeof originalBounds === 'string') {
|
||||
console.log('--------------Error finding text in code:')
|
||||
@@ -1712,7 +1696,7 @@ class EditCodeService extends Disposable implements IEditCodeService {
|
||||
console.log('---------')
|
||||
const content = errContentOfInvalidStr(originalBounds, block.orig, blockNum, blocks)
|
||||
messages.push(
|
||||
{ role: 'assistant', content: fullText, anthropicReasoning: null }, // latest output
|
||||
{ role: 'assistant', content: fullText }, // latest output
|
||||
{ role: 'user', content: content } // user explanation of what's wrong
|
||||
)
|
||||
|
||||
@@ -1740,14 +1724,15 @@ class EditCodeService extends Disposable implements IEditCodeService {
|
||||
return
|
||||
}
|
||||
|
||||
console.log('---------adding-------')
|
||||
console.log('CURRENT TEXT!!!', { current: model?.getValue() })
|
||||
console.log('block', deepClone(block))
|
||||
console.log('origBounds', originalBounds)
|
||||
|
||||
|
||||
const [startLine, endLine] = convertOriginalRangeToFinalRange(originalBounds)
|
||||
console.log('start end', startLine, endLine)
|
||||
|
||||
// console.log('---------adding-------')
|
||||
// console.log('CURRENT TEXT!!!', { current: model?.getValue() })
|
||||
// console.log('block', deepClone(block))
|
||||
// console.log('origBounds', originalBounds)
|
||||
// console.log('start end', startLine, endLine)
|
||||
|
||||
// otherwise if no error, add the position as a diffarea
|
||||
const adding: Omit<TrackingZone<SearchReplaceDiffAreaMetadata>, 'diffareaid'> = {
|
||||
@@ -1804,7 +1789,6 @@ class EditCodeService extends Disposable implements IEditCodeService {
|
||||
onFinalMessage: async (params) => {
|
||||
const { fullText } = params
|
||||
|
||||
console.log('DONE - editCode!', { fullText })
|
||||
|
||||
// 1. wait 500ms and fix lint errors - call lint error workflow
|
||||
// (update react state to say "Fixing errors")
|
||||
@@ -1819,10 +1803,11 @@ class EditCodeService extends Disposable implements IEditCodeService {
|
||||
// IMPORTANT - sort by lineNum
|
||||
addedTrackingZoneOfBlockNum.sort((a, b) => a.metadata.originalBounds[0] - b.metadata.originalBounds[0])
|
||||
|
||||
const { model } = this._voidModelService.getModel(uri)
|
||||
console.log('CURRENT TEXT!!!', { current: model?.getValue() })
|
||||
console.log('addedTrackingZoneOfBlockNum', addedTrackingZoneOfBlockNum)
|
||||
console.log('blocks', deepClone(blocks))
|
||||
// const { model } = this._voidModelService.getModel(uri)
|
||||
// console.log('DONE - editCode!', { fullText })
|
||||
// console.log('CURRENT TEXT!!!', { current: model?.getValue() })
|
||||
// console.log('addedTrackingZoneOfBlockNum', addedTrackingZoneOfBlockNum)
|
||||
// console.log('blocks', deepClone(blocks))
|
||||
|
||||
for (let blockNum = addedTrackingZoneOfBlockNum.length - 1; blockNum >= 0; blockNum -= 1) {
|
||||
const { originalBounds } = addedTrackingZoneOfBlockNum[blockNum].metadata
|
||||
@@ -1917,6 +1902,8 @@ class EditCodeService extends Disposable implements IEditCodeService {
|
||||
|
||||
|
||||
interruptURIStreaming({ uri }: { uri: URI }) {
|
||||
if (!this._uriIsStreaming(uri)) return
|
||||
this._undoHistory(uri)
|
||||
// brute force for now is OK
|
||||
for (const diffareaid of this.diffAreasOfURI[uri.fsPath] || []) {
|
||||
const diffArea = this.diffAreaOfId[diffareaid]
|
||||
@@ -1924,7 +1911,6 @@ class EditCodeService extends Disposable implements IEditCodeService {
|
||||
if (!diffArea._streamState.isStreaming) continue
|
||||
this._stopIfStreaming(diffArea)
|
||||
}
|
||||
this._undoHistory(uri)
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -7,13 +7,20 @@ import { Event } from '../../../../base/common/event.js';
|
||||
import { URI } from '../../../../base/common/uri.js';
|
||||
import { ICodeEditor } from '../../../../editor/browser/editorBrowser.js';
|
||||
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
|
||||
import { Diff, DiffArea } from './editCodeService.js';
|
||||
|
||||
import { Diff, DiffArea, VoidFileSnapshot } from '../common/editCodeServiceTypes.js';
|
||||
|
||||
|
||||
export type StartBehavior = 'accept-conflicts' | 'reject-conflicts' | 'keep-conflicts'
|
||||
|
||||
export type StartApplyingOpts = ({
|
||||
export type CallBeforeStartApplyingOpts = {
|
||||
from: 'QuickEdit';
|
||||
diffareaid: number; // id of the CtrlK area (contains text selection)
|
||||
} | {
|
||||
from: 'ClickApply';
|
||||
uri: 'current' | URI;
|
||||
}
|
||||
|
||||
export type StartApplyingOpts = {
|
||||
from: 'QuickEdit';
|
||||
diffareaid: number; // id of the CtrlK area (contains text selection)
|
||||
startBehavior: StartBehavior;
|
||||
@@ -22,9 +29,7 @@ export type StartApplyingOpts = ({
|
||||
applyStr: string;
|
||||
uri: 'current' | URI;
|
||||
startBehavior: StartBehavior;
|
||||
})
|
||||
|
||||
|
||||
}
|
||||
|
||||
export type AddCtrlKOpts = {
|
||||
startLine: number,
|
||||
@@ -37,7 +42,8 @@ export const IEditCodeService = createDecorator<IEditCodeService>('editCodeServi
|
||||
export interface IEditCodeService {
|
||||
readonly _serviceBrand: undefined;
|
||||
|
||||
startApplying(opts: StartApplyingOpts): Promise<[URI, Promise<void>] | null>;
|
||||
callBeforeStartApplying(opts: CallBeforeStartApplyingOpts): Promise<void>;
|
||||
startApplying(opts: StartApplyingOpts): [URI, Promise<void>] | null;
|
||||
addCtrlKZone(opts: AddCtrlKOpts): number | undefined;
|
||||
removeCtrlKZone(opts: { diffareaid: number }): void;
|
||||
|
||||
@@ -49,7 +55,7 @@ export interface IEditCodeService {
|
||||
|
||||
// events
|
||||
onDidAddOrDeleteDiffZones: Event<{ uri: URI }>;
|
||||
onDidChangeDiffsInDiffZone: Event<{ uri: URI; diffareaid: number }>; // only fires when not streaming!!! streaming would be too much
|
||||
onDidChangeDiffsInDiffZoneNotStreaming: Event<{ uri: URI; diffareaid: number }>; // only fires when not streaming!!! streaming would be too much
|
||||
onDidChangeStreamingInDiffZone: Event<{ uri: URI; diffareaid: number }>;
|
||||
onDidChangeStreamingInCtrlKZone: Event<{ uri: URI; diffareaid: number }>;
|
||||
|
||||
@@ -61,4 +67,6 @@ export interface IEditCodeService {
|
||||
interruptURIStreaming(opts: { uri: URI }): void;
|
||||
|
||||
// testDiffs(): void;
|
||||
getVoidFileSnapshot(uri: URI): VoidFileSnapshot;
|
||||
restoreVoidFileSnapshot(uri: URI, snapshot: VoidFileSnapshot): void;
|
||||
}
|
||||
|
||||
@@ -3,34 +3,9 @@
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import { ComputedDiff } from '../../common/editCodeServiceTypes.js';
|
||||
import { diffLines } from '../react/out/diff/index.js'
|
||||
|
||||
export type ComputedDiff = {
|
||||
type: 'edit';
|
||||
originalCode: string;
|
||||
originalStartLine: number;
|
||||
originalEndLine: number;
|
||||
code: string;
|
||||
startLine: number; // 1-indexed
|
||||
endLine: number;
|
||||
} | {
|
||||
type: 'insertion';
|
||||
// originalCode: string;
|
||||
originalStartLine: number; // insertion starts on column 0 of this
|
||||
// originalEndLine: number;
|
||||
code: string;
|
||||
startLine: number;
|
||||
endLine: number;
|
||||
} | {
|
||||
type: 'deletion';
|
||||
originalCode: string;
|
||||
originalStartLine: number;
|
||||
originalEndLine: number;
|
||||
// code: string;
|
||||
startLine: number; // deletion starts on column 0 of this
|
||||
// endLine: number;
|
||||
}
|
||||
|
||||
export function findDiffs(oldStr: string, newStr: string) {
|
||||
|
||||
// this makes it so the end of the file always ends with a \n (if you don't have this, then diffing E vs E\n gives an "edit". With it, you end up diffing E\n vs E\n\n which now properly gives an insertion)
|
||||
|
||||
@@ -76,93 +76,107 @@
|
||||
opacity: 80%;
|
||||
}
|
||||
|
||||
/* styles for all containers used by void */
|
||||
.void-scope {
|
||||
--scrollbar-vertical-width: 8px;
|
||||
--scrollbar-horizontal-height: 6px;
|
||||
}
|
||||
|
||||
/* Target both void-scope and all its descendants with scrollbars */
|
||||
.void-scope,
|
||||
.void-scope * {
|
||||
scrollbar-width: thin !important;
|
||||
scrollbar-color: var(--void-bg-1) var(--void-bg-3) !important; /* For Firefox */
|
||||
}
|
||||
|
||||
.void-scope::-webkit-scrollbar,
|
||||
.void-scope *::-webkit-scrollbar {
|
||||
width: var(--scrollbar-vertical-width) !important;
|
||||
height: var(--scrollbar-horizontal-height) !important;
|
||||
background-color: var(--void-bg-3) !important;
|
||||
}
|
||||
|
||||
.void-scope::-webkit-scrollbar-thumb,
|
||||
.void-scope *::-webkit-scrollbar-thumb {
|
||||
background-color: var(--void-bg-1) !important;
|
||||
border-radius: 4px !important;
|
||||
border: none !important;
|
||||
-webkit-box-shadow: none !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
.void-scope::-webkit-scrollbar-thumb:hover,
|
||||
.void-scope *::-webkit-scrollbar-thumb:hover {
|
||||
background-color: var(--void-bg-1) !important;
|
||||
filter: brightness(1.1) !important;
|
||||
}
|
||||
|
||||
.void-scope::-webkit-scrollbar-thumb:active,
|
||||
.void-scope *::-webkit-scrollbar-thumb:active {
|
||||
background-color: var(--void-bg-1) !important;
|
||||
filter: brightness(1.2) !important;
|
||||
}
|
||||
|
||||
.void-scope::-webkit-scrollbar-track,
|
||||
.void-scope *::-webkit-scrollbar-track {
|
||||
background-color: var(--void-bg-3) !important;
|
||||
border: none !important;
|
||||
}
|
||||
|
||||
.void-scope::-webkit-scrollbar-corner,
|
||||
.void-scope *::-webkit-scrollbar-corner {
|
||||
background-color: var(--void-bg-3) !important;
|
||||
}
|
||||
|
||||
/* Add void-scrollable-element styles to match */
|
||||
.void-scrollable-element {
|
||||
background-color: var(--vscode-editor-background);
|
||||
--scrollbar-vertical-width: 14px;
|
||||
--scrollbar-horizontal-height: 6px;
|
||||
overflow: auto; /* Ensure scrollbars are shown when needed */
|
||||
}
|
||||
|
||||
.void-scrollable-element,
|
||||
.void-scrollable-element * {
|
||||
scrollbar-width: thin !important; /* For Firefox */
|
||||
scrollbar-color: var(--void-bg-1) var(--void-bg-3) !important; /* For Firefox */
|
||||
}
|
||||
|
||||
.void-scrollable-element::-webkit-scrollbar,
|
||||
.void-scrollable-element *::-webkit-scrollbar {
|
||||
width: 14px !important;
|
||||
height: 4px !important;
|
||||
}
|
||||
|
||||
.void-scrollable-element::-webkit-scrollbar-track,
|
||||
.void-scrollable-element *::-webkit-scrollbar-track {
|
||||
background: transparent !important;
|
||||
width: var(--scrollbar-vertical-width) !important;
|
||||
height: var(--scrollbar-horizontal-height) !important;
|
||||
background-color: var(--void-bg-3) !important;
|
||||
}
|
||||
|
||||
.void-scrollable-element::-webkit-scrollbar-thumb,
|
||||
.void-scrollable-element *::-webkit-scrollbar-thumb {
|
||||
background-color: transparent !important;
|
||||
border-radius: 0px !important;
|
||||
background-color: var(--void-bg-1) !important;
|
||||
border-radius: 4px !important;
|
||||
border: none !important;
|
||||
-webkit-box-shadow: none !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
.void-scrollable-element::-webkit-scrollbar-thumb:hover,
|
||||
.void-scrollable-element *::-webkit-scrollbar-thumb:hover {
|
||||
background-color: var(--vscode-scrollbarSlider-hoverBackground) !important;
|
||||
background-color: var(--void-bg-1) !important;
|
||||
filter: brightness(1.1) !important;
|
||||
}
|
||||
|
||||
.void-scrollable-element::-webkit-scrollbar-thumb:active,
|
||||
.void-scrollable-element *::-webkit-scrollbar-thumb:active {
|
||||
background-color: var(--vscode-scrollbarSlider-activeBackground) !important;
|
||||
background-color: var(--void-bg-1) !important;
|
||||
filter: brightness(1.2) !important;
|
||||
}
|
||||
|
||||
.void-scrollable-element::-webkit-scrollbar-track,
|
||||
.void-scrollable-element *::-webkit-scrollbar-track {
|
||||
background-color: var(--void-bg-3) !important;
|
||||
border: none !important;
|
||||
}
|
||||
|
||||
.void-scrollable-element::-webkit-scrollbar-corner,
|
||||
.void-scrollable-element *::-webkit-scrollbar-corner {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
.void-scrollable-element.show-scrollbar-0::-webkit-scrollbar-thumb,
|
||||
.void-scrollable-element.show-scrollbar-0 *::-webkit-scrollbar-thumb {
|
||||
background-color: color-mix(in srgb, var(--vscode-scrollbarSlider-background) 0%, transparent) !important;
|
||||
}
|
||||
|
||||
.void-scrollable-element.show-scrollbar-1::-webkit-scrollbar-thumb,
|
||||
.void-scrollable-element.show-scrollbar-1 *::-webkit-scrollbar-thumb {
|
||||
background-color: color-mix(in srgb, var(--vscode-scrollbarSlider-background) 10%, transparent) !important;
|
||||
}
|
||||
|
||||
.void-scrollable-element.show-scrollbar-2::-webkit-scrollbar-thumb,
|
||||
.void-scrollable-element.show-scrollbar-2 *::-webkit-scrollbar-thumb {
|
||||
background-color: color-mix(in srgb, var(--vscode-scrollbarSlider-background) 20%, transparent) !important;
|
||||
}
|
||||
|
||||
.void-scrollable-element.show-scrollbar-3::-webkit-scrollbar-thumb,
|
||||
.void-scrollable-element.show-scrollbar-3 *::-webkit-scrollbar-thumb {
|
||||
background-color: color-mix(in srgb, var(--vscode-scrollbarSlider-background) 30%, transparent) !important;
|
||||
}
|
||||
|
||||
.void-scrollable-element.show-scrollbar-4::-webkit-scrollbar-thumb,
|
||||
.void-scrollable-element.show-scrollbar-4 *::-webkit-scrollbar-thumb {
|
||||
background-color: color-mix(in srgb, var(--vscode-scrollbarSlider-background) 40%, transparent) !important;
|
||||
}
|
||||
|
||||
.void-scrollable-element.show-scrollbar-5::-webkit-scrollbar-thumb,
|
||||
.void-scrollable-element.show-scrollbar-5 *::-webkit-scrollbar-thumb {
|
||||
background-color: color-mix(in srgb, var(--vscode-scrollbarSlider-background) 50%, transparent) !important;
|
||||
}
|
||||
|
||||
.void-scrollable-element.show-scrollbar-6::-webkit-scrollbar-thumb,
|
||||
.void-scrollable-element.show-scrollbar-6 *::-webkit-scrollbar-thumb {
|
||||
background-color: color-mix(in srgb, var(--vscode-scrollbarSlider-background) 60%, transparent) !important;
|
||||
}
|
||||
|
||||
.void-scrollable-element.show-scrollbar-7::-webkit-scrollbar-thumb,
|
||||
.void-scrollable-element.show-scrollbar-7 *::-webkit-scrollbar-thumb {
|
||||
background-color: color-mix(in srgb, var(--vscode-scrollbarSlider-background) 70%, transparent) !important;
|
||||
}
|
||||
|
||||
.void-scrollable-element.show-scrollbar-8::-webkit-scrollbar-thumb,
|
||||
.void-scrollable-element.show-scrollbar-8 *::-webkit-scrollbar-thumb {
|
||||
background-color: color-mix(in srgb, var(--vscode-scrollbarSlider-background) 80%, transparent) !important;
|
||||
}
|
||||
|
||||
.void-scrollable-element.show-scrollbar-9::-webkit-scrollbar-thumb,
|
||||
.void-scrollable-element.show-scrollbar-9 *::-webkit-scrollbar-thumb {
|
||||
background-color: color-mix(in srgb, var(--vscode-scrollbarSlider-background) 90%, transparent) !important;
|
||||
}
|
||||
|
||||
.void-scrollable-element.show-scrollbar-10::-webkit-scrollbar-thumb,
|
||||
.void-scrollable-element.show-scrollbar-10 *::-webkit-scrollbar-thumb {
|
||||
background-color: var(--vscode-scrollbarSlider-background) !important;
|
||||
background-color: var(--void-bg-3) !important;
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import * as dom from '../../../../base/browser/dom.js';
|
||||
import { IMetricsService } from '../common/metricsService.js';
|
||||
|
||||
|
||||
|
||||
export interface IMetricsPollService {
|
||||
readonly _serviceBrand: undefined;
|
||||
}
|
||||
|
||||
@@ -74,7 +74,7 @@ function saveStylesFile() {
|
||||
} catch (err) {
|
||||
console.error('[scope-tailwind] Error saving styles.css:', err);
|
||||
}
|
||||
}, 4000);
|
||||
}, 6000);
|
||||
}
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
|
||||
+181
-136
@@ -1,12 +1,17 @@
|
||||
/*--------------------------------------------------------------------------------------
|
||||
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import { useState, useEffect, useCallback } from 'react'
|
||||
import { useAccessor, useCommandBarState, useCommandBarURIListener, useSettingsState } from '../util/services.js'
|
||||
import { usePromise, useRefState } from '../util/helpers.js'
|
||||
import { isFeatureNameDisabled } from '../../../../common/voidSettingsTypes.js'
|
||||
import { URI } from '../../../../../../../base/common/uri.js'
|
||||
import { FileSymlink, LucideIcon, RotateCw } from 'lucide-react'
|
||||
import { FileSymlink, LucideIcon, RotateCw, Terminal } from 'lucide-react'
|
||||
import { Check, X, Square, Copy, Play, } from 'lucide-react'
|
||||
import { getBasename, ListableToolItem, ToolChildrenWrapper } from '../sidebar-tsx/SidebarChat.js'
|
||||
import { ChatMarkdownRender } from './ChatMarkdownRender.js'
|
||||
import { PlacesType, VariantType } from 'react-tooltip'
|
||||
|
||||
enum CopyButtonText {
|
||||
Idle = 'Copy',
|
||||
@@ -16,30 +21,28 @@ enum CopyButtonText {
|
||||
|
||||
|
||||
type IconButtonProps = {
|
||||
onClick: () => void;
|
||||
Icon: LucideIcon
|
||||
disabled?: boolean
|
||||
className?: string
|
||||
}
|
||||
|
||||
export const IconShell1 = ({ onClick, Icon, disabled, className }: IconButtonProps) => (
|
||||
export const IconShell1 = ({ onClick, Icon, disabled, className, ...props }: IconButtonProps & React.ButtonHTMLAttributes<HTMLButtonElement>) => (
|
||||
<button
|
||||
disabled={disabled}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
onClick?.();
|
||||
onClick?.(e);
|
||||
}}
|
||||
// border border-void-border-1 rounded
|
||||
className={`
|
||||
size-[22px]
|
||||
p-[4px]
|
||||
size-[18px]
|
||||
p-[2px]
|
||||
flex items-center justify-center
|
||||
text-sm bg-void-bg-3 text-void-fg-1
|
||||
text-sm bg-void-bg-3 text-void-fg-3
|
||||
hover:brightness-110
|
||||
border border-void-border-1 rounded
|
||||
disabled:opacity-50 disabled:cursor-not-allowed
|
||||
${className}
|
||||
`}
|
||||
{...props}
|
||||
>
|
||||
<Icon />
|
||||
</button>
|
||||
@@ -64,9 +67,9 @@ export const IconShell1 = ({ onClick, Icon, disabled, className }: IconButtonPro
|
||||
// </button>
|
||||
// )
|
||||
|
||||
const COPY_FEEDBACK_TIMEOUT = 1000 // amount of time to say 'Copied!'
|
||||
const COPY_FEEDBACK_TIMEOUT = 1500 // amount of time to say 'Copied!'
|
||||
|
||||
const CopyButton = ({ codeStr }: { codeStr: string }) => {
|
||||
export const CopyButton = ({ codeStr }: { codeStr: string }) => {
|
||||
const accessor = useAccessor()
|
||||
|
||||
const metricsService = accessor.get('IMetricsService')
|
||||
@@ -90,18 +93,14 @@ const CopyButton = ({ codeStr }: { codeStr: string }) => {
|
||||
return <IconShell1
|
||||
Icon={copyButtonText === CopyButtonText.Copied ? Check : copyButtonText === CopyButtonText.Error ? X : Copy}
|
||||
onClick={onCopy}
|
||||
{...tooltipPropsForApplyBlock({ tooltipName: 'Copy' })}
|
||||
/>
|
||||
}
|
||||
|
||||
|
||||
// state persisted for duration of react only
|
||||
// TODO change this to use type `ChatThreads.applyBoxState[applyBoxId]`
|
||||
const applyingURIOfApplyBoxIdRef: { current: { [applyBoxId: string]: URI | undefined } } = { current: {} }
|
||||
|
||||
|
||||
|
||||
|
||||
export const JumpToFileButton = ({ uri }: { uri: URI | 'current' }) => {
|
||||
export const JumpToFileButton = ({ uri, ...props }: { uri: URI | 'current' } & React.ButtonHTMLAttributes<HTMLButtonElement>) => {
|
||||
const accessor = useAccessor()
|
||||
const commandService = accessor.get('ICommandService')
|
||||
|
||||
@@ -111,185 +110,229 @@ export const JumpToFileButton = ({ uri }: { uri: URI | 'current' }) => {
|
||||
onClick={() => {
|
||||
commandService.executeCommand('vscode.open', uri, { preview: true })
|
||||
}}
|
||||
{...tooltipPropsForApplyBlock({ tooltipName: 'Go to file' })}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
|
||||
return jumpToFileButton
|
||||
}
|
||||
|
||||
export const useApplyButtonHTML = ({ codeStr, applyBoxId, uri }: { codeStr: string, applyBoxId: string, uri: URI | 'current' }) => {
|
||||
|
||||
|
||||
export const JumpToTerminalButton = ({ onClick }: { onClick: () => void }) => {
|
||||
return (
|
||||
<IconShell1
|
||||
Icon={Terminal}
|
||||
onClick={onClick}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
// state persisted for duration of react only
|
||||
// TODO change this to use type `ChatThreads.applyBoxState[applyBoxId]`
|
||||
const applyingURIOfApplyBoxIdRef: { current: { [applyBoxId: string]: URI | undefined } } = { current: {} }
|
||||
|
||||
const getUriBeingApplied = (applyBoxId: string) => {
|
||||
return applyingURIOfApplyBoxIdRef.current[applyBoxId] ?? null
|
||||
}
|
||||
|
||||
|
||||
export const useApplyButtonState = ({ applyBoxId, uri }: { applyBoxId: string, uri: URI | 'current' }) => {
|
||||
|
||||
const settingsState = useSettingsState()
|
||||
const isDisabled = !!isFeatureNameDisabled('Apply', settingsState) || !applyBoxId
|
||||
|
||||
const accessor = useAccessor()
|
||||
const editCodeService = accessor.get('IEditCodeService')
|
||||
const voidCommandBarService = accessor.get('IVoidCommandBarService')
|
||||
const metricsService = accessor.get('IMetricsService')
|
||||
|
||||
const [_, rerender] = useState(0)
|
||||
|
||||
const getUriBeingApplied = useCallback(() => {
|
||||
return applyingURIOfApplyBoxIdRef.current[applyBoxId] ?? null
|
||||
}, [applyBoxId])
|
||||
|
||||
const getStreamState = useCallback(() => {
|
||||
const uri = getUriBeingApplied()
|
||||
const uri = getUriBeingApplied(applyBoxId)
|
||||
if (!uri) return 'idle-no-changes'
|
||||
return voidCommandBarService.getStreamState(uri)
|
||||
}, [voidCommandBarService, getUriBeingApplied])
|
||||
}, [voidCommandBarService, applyBoxId])
|
||||
|
||||
// listen for stream updates on this box
|
||||
|
||||
|
||||
useCommandBarURIListener(useCallback((uri_) => {
|
||||
const shouldUpdate = (
|
||||
getUriBeingApplied()?.fsPath === uri_.fsPath
|
||||
getUriBeingApplied(applyBoxId)?.fsPath === uri_.fsPath
|
||||
|| (uri !== 'current' && uri.fsPath === uri_.fsPath)
|
||||
)
|
||||
if (!shouldUpdate) return
|
||||
rerender(c => c + 1)
|
||||
}, [applyBoxId, editCodeService, getUriBeingApplied, uri])
|
||||
if (shouldUpdate) {
|
||||
rerender(c => c + 1)
|
||||
}
|
||||
}, [applyBoxId, uri]))
|
||||
|
||||
const currStreamState = getStreamState()
|
||||
|
||||
|
||||
return {
|
||||
getStreamState,
|
||||
isDisabled,
|
||||
currStreamState,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
type IndicatorColor = 'green' | 'orange' | 'dark' | 'yellow' | null
|
||||
export const StatusIndicator = ({ indicatorColor, title, className, ...props }: { indicatorColor: IndicatorColor, title?: React.ReactNode, className?: string } & React.HTMLAttributes<HTMLDivElement>) => {
|
||||
return (
|
||||
<div className={`flex flex-row text-void-fg-3 text-xs items-center gap-1.5 ${className}`} {...props}>
|
||||
{title && <span className='opacity-80'>{title}</span>}
|
||||
<div
|
||||
className={` size-1.5 rounded-full border
|
||||
${indicatorColor === 'dark' ? 'bg-void-bg-3 border-void-border-1' :
|
||||
indicatorColor === 'orange' ? 'bg-orange-500 border-orange-500 shadow-[0_0_4px_0px_rgba(234,88,12,0.6)]' :
|
||||
indicatorColor === 'green' ? 'bg-green-500 border-green-500 shadow-[0_0_4px_0px_rgba(22,163,74,0.6)]' :
|
||||
indicatorColor === 'yellow' ? 'bg-yellow-500 border-yellow-500 shadow-[0_0_4px_0px_rgba(22,163,74,0.6)]' :
|
||||
'bg-void-border-1 border-void-border-1'
|
||||
}
|
||||
`}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const tooltipPropsForApplyBlock = ({ tooltipName, color = undefined, position = 'top', offset = undefined }: { tooltipName: string, color?: IndicatorColor, position?: PlacesType, offset?: number }) => ({
|
||||
'data-tooltip-id': color === 'orange' ? `void-tooltip-orange` : color === 'green' ? 'void-tooltip-green' : 'void-tooltip',
|
||||
'data-tooltip-place': position as PlacesType,
|
||||
'data-tooltip-content': `${tooltipName}`,
|
||||
'data-tooltip-offset': offset,
|
||||
})
|
||||
|
||||
|
||||
export const StatusIndicatorForApplyButton = ({ applyBoxId, uri }: { applyBoxId: string, uri: URI | 'current' } & React.HTMLAttributes<HTMLDivElement>) => {
|
||||
|
||||
const { currStreamState } = useApplyButtonState({ applyBoxId, uri })
|
||||
|
||||
const color = (
|
||||
currStreamState === 'idle-no-changes' ? 'dark' :
|
||||
currStreamState === 'streaming' ? 'orange' :
|
||||
currStreamState === 'idle-has-changes' ? 'green' :
|
||||
null
|
||||
)
|
||||
|
||||
const tooltipName = (
|
||||
currStreamState === 'idle-no-changes' ? 'Done' :
|
||||
currStreamState === 'streaming' ? 'Applying' :
|
||||
currStreamState === 'idle-has-changes' ? 'Done' : // also 'Done'? 'Applied' looked bad
|
||||
''
|
||||
)
|
||||
|
||||
const statusIndicatorHTML = <StatusIndicator
|
||||
key={currStreamState}
|
||||
className='mx-2'
|
||||
indicatorColor={color}
|
||||
{...tooltipPropsForApplyBlock({ tooltipName, color, position: 'top', offset: 12 })}
|
||||
/>
|
||||
return statusIndicatorHTML
|
||||
}
|
||||
|
||||
|
||||
export const ApplyButtonsHTML = ({ codeStr, applyBoxId, reapplyIcon, uri }: { codeStr: string, applyBoxId: string, reapplyIcon: boolean, uri: URI | 'current' }) => {
|
||||
const accessor = useAccessor()
|
||||
const editCodeService = accessor.get('IEditCodeService')
|
||||
const metricsService = accessor.get('IMetricsService')
|
||||
|
||||
const {
|
||||
currStreamState,
|
||||
isDisabled,
|
||||
getStreamState,
|
||||
} = useApplyButtonState({ applyBoxId, uri })
|
||||
|
||||
const onClickSubmit = useCallback(async () => {
|
||||
if (isDisabled) return
|
||||
if (getStreamState() === 'streaming') return
|
||||
const [newApplyingUri, applyDonePromise] = await editCodeService.startApplying({
|
||||
const opts = {
|
||||
from: 'ClickApply',
|
||||
applyStr: codeStr,
|
||||
uri: uri,
|
||||
startBehavior: 'keep-conflicts',
|
||||
}) ?? []
|
||||
startBehavior: 'reject-conflicts',
|
||||
} as const
|
||||
|
||||
await editCodeService.callBeforeStartApplying(opts)
|
||||
const [newApplyingUri, applyDonePromise] = editCodeService.startApplying(opts) ?? []
|
||||
|
||||
// catch any errors by interrupting the stream
|
||||
applyDonePromise?.catch(e => { if (newApplyingUri) editCodeService.interruptURIStreaming({ uri: newApplyingUri }) })
|
||||
applyDonePromise?.catch(e => {
|
||||
const uri = getUriBeingApplied(applyBoxId)
|
||||
if (uri) editCodeService.interruptURIStreaming({ uri: uri })
|
||||
})
|
||||
|
||||
applyingURIOfApplyBoxIdRef.current[applyBoxId] = newApplyingUri ?? undefined
|
||||
|
||||
rerender(c => c + 1)
|
||||
// rerender(c => c + 1)
|
||||
metricsService.capture('Apply Code', { length: codeStr.length }) // capture the length only
|
||||
}, [isDisabled, getStreamState, editCodeService, codeStr, uri, applyBoxId, metricsService])
|
||||
|
||||
|
||||
const onInterrupt = useCallback(() => {
|
||||
if (getStreamState() !== 'streaming') return
|
||||
const uri = getUriBeingApplied()
|
||||
const uri = getUriBeingApplied(applyBoxId)
|
||||
if (!uri) return
|
||||
|
||||
editCodeService.interruptURIStreaming({ uri })
|
||||
metricsService.capture('Stop Apply', {})
|
||||
}, [getStreamState, getUriBeingApplied, editCodeService, metricsService])
|
||||
}, [getStreamState, applyBoxId, editCodeService, metricsService])
|
||||
|
||||
const onAccept = useCallback(() => {
|
||||
const uri = getUriBeingApplied()
|
||||
const uri = getUriBeingApplied(applyBoxId)
|
||||
if (uri) editCodeService.acceptOrRejectAllDiffAreas({ uri, behavior: 'accept', removeCtrlKs: false })
|
||||
}, [getUriBeingApplied, editCodeService])
|
||||
}, [applyBoxId, editCodeService])
|
||||
|
||||
const onReject = useCallback(() => {
|
||||
const uri = getUriBeingApplied()
|
||||
const uri = getUriBeingApplied(applyBoxId)
|
||||
if (uri) editCodeService.acceptOrRejectAllDiffAreas({ uri, behavior: 'reject', removeCtrlKs: false })
|
||||
}, [getUriBeingApplied, editCodeService])
|
||||
}, [applyBoxId, editCodeService])
|
||||
|
||||
const onReapply = useCallback(() => {
|
||||
onReject()
|
||||
onClickSubmit()
|
||||
}, [onReject, onClickSubmit])
|
||||
// const onReapply = useCallback(() => {
|
||||
// onReject()
|
||||
// onClickSubmit()
|
||||
// }, [onReject, onClickSubmit])
|
||||
|
||||
const currStreamState = getStreamState()
|
||||
|
||||
const copyButton = (
|
||||
<CopyButton codeStr={codeStr} />
|
||||
)
|
||||
|
||||
const playButton = (
|
||||
<IconShell1
|
||||
Icon={Play}
|
||||
onClick={onClickSubmit}
|
||||
/>
|
||||
)
|
||||
|
||||
const stopButton = (
|
||||
<IconShell1
|
||||
Icon={Square}
|
||||
onClick={onInterrupt}
|
||||
/>
|
||||
)
|
||||
|
||||
const reapplyButton = (
|
||||
<IconShell1
|
||||
Icon={RotateCw}
|
||||
onClick={onReapply}
|
||||
/>
|
||||
)
|
||||
|
||||
const acceptButton = (
|
||||
<IconShell1
|
||||
Icon={Check}
|
||||
onClick={onAccept}
|
||||
className="text-green-600"
|
||||
/>
|
||||
)
|
||||
|
||||
const rejectButton = (
|
||||
<IconShell1
|
||||
Icon={X}
|
||||
onClick={onReject}
|
||||
className="text-red-600"
|
||||
/>
|
||||
)
|
||||
|
||||
|
||||
|
||||
let buttonsHTML = <></>
|
||||
|
||||
if (currStreamState === 'streaming') {
|
||||
buttonsHTML = <>
|
||||
<JumpToFileButton uri={uri} />
|
||||
{copyButton}
|
||||
{stopButton}
|
||||
</>
|
||||
return <IconShell1
|
||||
|
||||
Icon={Square}
|
||||
onClick={onInterrupt}
|
||||
|
||||
{...tooltipPropsForApplyBlock({ tooltipName: 'Stop' })}
|
||||
/>
|
||||
}
|
||||
|
||||
if (currStreamState === 'idle-no-changes') {
|
||||
buttonsHTML = <>
|
||||
<JumpToFileButton uri={uri} />
|
||||
{copyButton}
|
||||
{playButton}
|
||||
</>
|
||||
|
||||
return <IconShell1
|
||||
Icon={reapplyIcon ? RotateCw : Play}
|
||||
onClick={onClickSubmit}
|
||||
{...tooltipPropsForApplyBlock({ tooltipName: reapplyIcon ? 'Reapply' : 'Apply' })}
|
||||
/>
|
||||
}
|
||||
|
||||
if (currStreamState === 'idle-has-changes') {
|
||||
buttonsHTML = <>
|
||||
<JumpToFileButton uri={uri} />
|
||||
{reapplyButton}
|
||||
{rejectButton}
|
||||
{acceptButton}
|
||||
return <>
|
||||
{/* <IconShell1
|
||||
Icon={RotateCw}
|
||||
onClick={onReapply}
|
||||
/> */}
|
||||
<IconShell1
|
||||
Icon={X}
|
||||
onClick={onReject}
|
||||
{...tooltipPropsForApplyBlock({ tooltipName: 'Remove' })}
|
||||
/>
|
||||
<IconShell1
|
||||
Icon={Check}
|
||||
onClick={onAccept}
|
||||
{...tooltipPropsForApplyBlock({ tooltipName: 'Keep' })}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
|
||||
const statusIndicatorHTML = <div className='flex flex-row items-center size-4'>
|
||||
<div
|
||||
className={` size-1.5 rounded-full border
|
||||
${currStreamState === 'idle-no-changes' ? 'bg-void-bg-3 border-void-border-1' :
|
||||
currStreamState === 'streaming' ? 'bg-orange-500 border-orange-500 shadow-[0_0_4px_0px_rgba(234,88,12,0.6)]' :
|
||||
currStreamState === 'idle-has-changes' ? 'bg-green-500 border-green-500 shadow-[0_0_4px_0px_rgba(22,163,74,0.6)]' :
|
||||
'bg-void-border-1 border-void-border-1'
|
||||
}`
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
return {
|
||||
statusIndicatorHTML,
|
||||
buttonsHTML,
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
export const BlockCodeApplyWrapper = ({
|
||||
children,
|
||||
initValue,
|
||||
@@ -305,10 +348,10 @@ export const BlockCodeApplyWrapper = ({
|
||||
language: string;
|
||||
uri: URI | 'current',
|
||||
}) => {
|
||||
|
||||
const { statusIndicatorHTML, buttonsHTML } = useApplyButtonHTML({ codeStr: initValue, applyBoxId, uri })
|
||||
const accessor = useAccessor()
|
||||
const commandService = accessor.get('ICommandService')
|
||||
const { currStreamState } = useApplyButtonState({ applyBoxId, uri })
|
||||
|
||||
|
||||
const name = uri !== 'current' ?
|
||||
<ListableToolItem
|
||||
@@ -324,13 +367,15 @@ export const BlockCodeApplyWrapper = ({
|
||||
{/* header */}
|
||||
<div className=" select-none flex justify-between items-center py-1 px-2 border-b border-void-border-3 cursor-default">
|
||||
<div className="flex items-center">
|
||||
{statusIndicatorHTML}
|
||||
<StatusIndicatorForApplyButton uri={uri} applyBoxId={applyBoxId} />
|
||||
<span className="text-[13px] font-light text-void-fg-3">
|
||||
{name}
|
||||
</span>
|
||||
</div>
|
||||
<div className={`${canApply ? '' : 'hidden'} flex items-center gap-1`}>
|
||||
{buttonsHTML}
|
||||
<JumpToFileButton uri={uri} />
|
||||
{currStreamState === 'idle-no-changes' && <CopyButton codeStr={initValue} />}
|
||||
<ApplyButtonsHTML uri={uri} applyBoxId={applyBoxId} codeStr={initValue} reapplyIcon={false} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -57,7 +57,7 @@ const CodespanWithLink = ({ text, rawText, chatMessageLocation }: { text: string
|
||||
const [didComputeCodespanLink, setDidComputeCodespanLink] = useState<boolean>(false)
|
||||
|
||||
let link = undefined
|
||||
if (rawText.endsWith("`")) { // if codespan was completed
|
||||
if (rawText.endsWith('`')) { // if codespan was completed
|
||||
|
||||
// get link from cache
|
||||
link = chatThreadService.getCodespanLink({ codespanStr: text, messageIdx, threadId })
|
||||
@@ -120,11 +120,11 @@ const RenderToken = ({ token, inPTag, codeURI, chatMessageLocation, tokenIdx, ..
|
||||
return null;
|
||||
}
|
||||
|
||||
if (t.type === "space") {
|
||||
if (t.type === 'space') {
|
||||
return <span>{t.raw}</span>
|
||||
}
|
||||
|
||||
if (t.type === "code") {
|
||||
if (t.type === 'code') {
|
||||
const [firstLine, remainingContents] = separateOutFirstLine(t.text)
|
||||
const firstLineIsURI = isValidUri(firstLine) && !codeURI
|
||||
const contents = firstLineIsURI ? (remainingContents?.trimStart() || '') : t.text // exclude first-line URI from contents
|
||||
@@ -152,7 +152,7 @@ const RenderToken = ({ token, inPTag, codeURI, chatMessageLocation, tokenIdx, ..
|
||||
}
|
||||
|
||||
if (options.isApplyEnabled && chatMessageLocation) {
|
||||
const isCodeblockClosed = t.raw.trimEnd().endsWith('```') // user should only be able to Apply when the code has been closed (t.raw ends with "```")
|
||||
const isCodeblockClosed = t.raw.trimEnd().endsWith('```') // user should only be able to Apply when the code has been closed (t.raw ends with '```')
|
||||
|
||||
const applyBoxId = getApplyBoxId({
|
||||
threadId: chatMessageLocation.threadId,
|
||||
@@ -179,7 +179,7 @@ const RenderToken = ({ token, inPTag, codeURI, chatMessageLocation, tokenIdx, ..
|
||||
/>
|
||||
}
|
||||
|
||||
if (t.type === "heading") {
|
||||
if (t.type === 'heading') {
|
||||
|
||||
const HeadingTag = `h${t.depth}` as keyof JSX.IntrinsicElements
|
||||
|
||||
@@ -188,7 +188,7 @@ const RenderToken = ({ token, inPTag, codeURI, chatMessageLocation, tokenIdx, ..
|
||||
</HeadingTag>
|
||||
}
|
||||
|
||||
if (t.type === "table") {
|
||||
if (t.type === 'table') {
|
||||
return (
|
||||
<div>
|
||||
<table>
|
||||
@@ -217,14 +217,14 @@ const RenderToken = ({ token, inPTag, codeURI, chatMessageLocation, tokenIdx, ..
|
||||
)
|
||||
// return (
|
||||
// <div>
|
||||
// <table className={"min-w-full border border-void-bg-2"}>
|
||||
// <table className={'min-w-full border border-void-bg-2'}>
|
||||
// <thead>
|
||||
// <tr className="bg-void-bg-1">
|
||||
// <tr className='bg-void-bg-1'>
|
||||
// {t.header.map((cell: any, index: number) => (
|
||||
// <th
|
||||
// key={index}
|
||||
// className="px-4 py-2 border border-void-bg-2 font-semibold"
|
||||
// style={{ textAlign: t.align[index] || "left" }}
|
||||
// className='px-4 py-2 border border-void-bg-2 font-semibold'
|
||||
// style={{ textAlign: t.align[index] || 'left' }}
|
||||
// >
|
||||
// {cell.raw}
|
||||
// </th>
|
||||
@@ -237,8 +237,8 @@ const RenderToken = ({ token, inPTag, codeURI, chatMessageLocation, tokenIdx, ..
|
||||
// {row.map((cell: any, cellIndex: number) => (
|
||||
// <td
|
||||
// key={cellIndex}
|
||||
// className={"px-4 py-2 border border-void-bg-2"}
|
||||
// style={{ textAlign: t.align[cellIndex] || "left" }}
|
||||
// className={'px-4 py-2 border border-void-bg-2'}
|
||||
// style={{ textAlign: t.align[cellIndex] || 'left' }}
|
||||
// >
|
||||
// {cell.raw}
|
||||
// </td>
|
||||
@@ -251,32 +251,32 @@ const RenderToken = ({ token, inPTag, codeURI, chatMessageLocation, tokenIdx, ..
|
||||
// )
|
||||
}
|
||||
|
||||
if (t.type === "hr") {
|
||||
if (t.type === 'hr') {
|
||||
return <hr />
|
||||
}
|
||||
|
||||
if (t.type === "blockquote") {
|
||||
if (t.type === 'blockquote') {
|
||||
return <blockquote>{t.text}</blockquote>
|
||||
}
|
||||
|
||||
if (t.type === 'list_item') {
|
||||
return <li>
|
||||
<input type="checkbox" checked={t.checked} readOnly />
|
||||
<input type='checkbox' checked={t.checked} readOnly />
|
||||
<span>
|
||||
<ChatMarkdownRender chatMessageLocation={chatMessageLocation} string={t.text} inPTag={true} codeURI={codeURI} {...options} />
|
||||
</span>
|
||||
</li>
|
||||
}
|
||||
|
||||
if (t.type === "list") {
|
||||
const ListTag = t.ordered ? "ol" : "ul"
|
||||
if (t.type === 'list') {
|
||||
const ListTag = t.ordered ? 'ol' : 'ul'
|
||||
|
||||
return (
|
||||
<ListTag start={t.start ? t.start : undefined}>
|
||||
{t.items.map((item, index) => (
|
||||
<li key={index}>
|
||||
{item.task && (
|
||||
<input type="checkbox" checked={item.checked} readOnly />
|
||||
<input type='checkbox' checked={item.checked} readOnly />
|
||||
)}
|
||||
<span>
|
||||
<ChatMarkdownRender chatMessageLocation={chatMessageLocation} string={item.text} inPTag={true} {...options} />
|
||||
@@ -287,7 +287,7 @@ const RenderToken = ({ token, inPTag, codeURI, chatMessageLocation, tokenIdx, ..
|
||||
)
|
||||
}
|
||||
|
||||
if (t.type === "paragraph") {
|
||||
if (t.type === 'paragraph') {
|
||||
const contents = <>
|
||||
{t.tokens.map((token, index) => (
|
||||
<RenderToken key={index}
|
||||
@@ -304,34 +304,28 @@ const RenderToken = ({ token, inPTag, codeURI, chatMessageLocation, tokenIdx, ..
|
||||
return <p>{contents}</p>
|
||||
}
|
||||
|
||||
if (t.type === "html") {
|
||||
const contents = t.raw
|
||||
if (inPTag) return <span className='block'>{contents}</span>
|
||||
return <p>{contents}</p>
|
||||
}
|
||||
|
||||
if (t.type === "text" || t.type === "escape") {
|
||||
if (t.type === 'text' || t.type === 'escape' || t.type === 'html') {
|
||||
return <span>{t.raw}</span>
|
||||
}
|
||||
|
||||
if (t.type === "def") {
|
||||
if (t.type === 'def') {
|
||||
return <></> // Definitions are typically not rendered
|
||||
}
|
||||
|
||||
if (t.type === "link") {
|
||||
if (t.type === 'link') {
|
||||
return (
|
||||
<a
|
||||
onClick={() => { window.open(t.href) }}
|
||||
href={t.href}
|
||||
title={t.title ?? undefined}
|
||||
className='underline cursor-pointer hover:brightness-90 transition-all duration-200'
|
||||
className='underline cursor-pointer hover:brightness-90 transition-all duration-200 text-void-fg-2'
|
||||
>
|
||||
{t.text}
|
||||
</a>
|
||||
)
|
||||
}
|
||||
|
||||
if (t.type === "image") {
|
||||
if (t.type === 'image') {
|
||||
return <img
|
||||
src={t.href}
|
||||
alt={t.text}
|
||||
@@ -340,16 +334,16 @@ const RenderToken = ({ token, inPTag, codeURI, chatMessageLocation, tokenIdx, ..
|
||||
/>
|
||||
}
|
||||
|
||||
if (t.type === "strong") {
|
||||
if (t.type === 'strong') {
|
||||
return <strong>{t.text}</strong>
|
||||
}
|
||||
|
||||
if (t.type === "em") {
|
||||
if (t.type === 'em') {
|
||||
return <em>{t.text}</em>
|
||||
}
|
||||
|
||||
// inline code
|
||||
if (t.type === "codespan") {
|
||||
if (t.type === 'codespan') {
|
||||
|
||||
if (options.isLinkDetectionEnabled && chatMessageLocation) {
|
||||
return <CodespanWithLink
|
||||
@@ -363,18 +357,18 @@ const RenderToken = ({ token, inPTag, codeURI, chatMessageLocation, tokenIdx, ..
|
||||
return <Codespan text={t.text} />
|
||||
}
|
||||
|
||||
if (t.type === "br") {
|
||||
if (t.type === 'br') {
|
||||
return <br />
|
||||
}
|
||||
|
||||
// strikethrough
|
||||
if (t.type === "del") {
|
||||
if (t.type === 'del') {
|
||||
return <del>{t.text}</del>
|
||||
}
|
||||
// default
|
||||
return (
|
||||
<div className="bg-orange-50 rounded-sm overflow-hidden p-2">
|
||||
<span className="text-sm text-orange-500">Unknown token rendered...</span>
|
||||
<div className='bg-orange-50 rounded-sm overflow-hidden p-2'>
|
||||
<span className='text-sm text-orange-500'>Unknown token rendered...</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -10,7 +10,6 @@ import { QuickEditPropsType } from '../../../quickEditActions.js';
|
||||
import { ButtonStop, ButtonSubmit, IconX, VoidChatArea } from '../sidebar-tsx/SidebarChat.js';
|
||||
import { VOID_CTRL_K_ACTION_ID } from '../../../actionIDs.js';
|
||||
import { useRefState } from '../util/helpers.js';
|
||||
import { useScrollbarStyles } from '../util/useScrollbarStyles.js';
|
||||
import { isFeatureNameDisabled } from '../../../../../../../workbench/contrib/void/common/voidSettingsTypes.js';
|
||||
|
||||
export const QuickEditChat = ({
|
||||
@@ -63,11 +62,14 @@ export const QuickEditChat = ({
|
||||
if (isStreamingRef.current) return
|
||||
textAreaFnsRef.current?.disable()
|
||||
|
||||
const [newApplyingUri, applyDonePromise] = await editCodeService.startApplying({
|
||||
const opts = {
|
||||
from: 'QuickEdit',
|
||||
diffareaid,
|
||||
startBehavior: 'keep-conflicts',
|
||||
}) ?? []
|
||||
} as const
|
||||
|
||||
await editCodeService.callBeforeStartApplying(opts)
|
||||
const [newApplyingUri, applyDonePromise] = editCodeService.startApplying(opts) ?? []
|
||||
// catch any errors by interrupting the stream
|
||||
applyDonePromise?.catch(e => { if (newApplyingUri) editCodeService.interruptCtrlKStreaming({ diffareaid }) })
|
||||
|
||||
@@ -86,8 +88,6 @@ export const QuickEditChat = ({
|
||||
editCodeService.removeCtrlKZone({ diffareaid })
|
||||
}, [editCodeService, diffareaid])
|
||||
|
||||
useScrollbarStyles(sizerRef)
|
||||
|
||||
const keybindingString = accessor.get('IKeybindingService').lookupKeybinding(VOID_CTRL_K_ACTION_ID)?.getLabel()
|
||||
|
||||
const chatAreaRef = useRef<HTMLDivElement | null>(null)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+2
-2
@@ -72,7 +72,7 @@ export const SidebarThreadSelector = () => {
|
||||
let firstMsg = null;
|
||||
// let secondMsg = null;
|
||||
|
||||
const firstUserMsgIdx = pastThread.messages.findIndex((msg) => msg.role !== 'tool' && msg.role !== 'tool_request');
|
||||
const firstUserMsgIdx = pastThread.messages.findIndex((msg) => msg.role === 'user');
|
||||
|
||||
if (firstUserMsgIdx !== -1) {
|
||||
// firstMsg = truncate(pastThread.messages[firstMsgIdx].displayContent ?? '');
|
||||
@@ -90,7 +90,7 @@ export const SidebarThreadSelector = () => {
|
||||
// secondMsg = truncate(pastThread.messages[secondMsgIdx].displayContent ?? '');
|
||||
// }
|
||||
|
||||
const numMessages = pastThread.messages.filter((msg) => msg.role !== 'tool_request').length;
|
||||
const numMessages = pastThread.messages.filter((msg) => msg.role === 'assistant' || msg.role === 'user').length;
|
||||
|
||||
return (
|
||||
<li key={pastThread.id}>
|
||||
|
||||
@@ -106,6 +106,7 @@ export const VoidInputBox2 = forwardRef<HTMLTextAreaElement, InputBox2Props>(fun
|
||||
|
||||
return (
|
||||
<textarea
|
||||
autoFocus={false}
|
||||
ref={useCallback((r: HTMLTextAreaElement | null) => {
|
||||
if (fnsRef)
|
||||
fnsRef.current = fns
|
||||
@@ -152,6 +153,42 @@ export const VoidInputBox2 = forwardRef<HTMLTextAreaElement, InputBox2Props>(fun
|
||||
|
||||
})
|
||||
|
||||
|
||||
export const VoidSimpleInputBox = ({ value, onChangeValue, placeholder, className, disabled, passwordBlur, compact, ...inputProps }: {
|
||||
value: string;
|
||||
onChangeValue: (value: string) => void;
|
||||
placeholder: string;
|
||||
className?: string;
|
||||
disabled?: boolean;
|
||||
compact?: boolean;
|
||||
passwordBlur?: boolean;
|
||||
} & React.InputHTMLAttributes<HTMLInputElement>) => {
|
||||
|
||||
return (
|
||||
<input
|
||||
value={value}
|
||||
onChange={(e) => onChangeValue(e.target.value)}
|
||||
placeholder={placeholder}
|
||||
disabled={disabled}
|
||||
// className='max-w-44 w-full border border-void-border-2 bg-void-bg-1 text-void-fg-3 text-root'
|
||||
// className={`w-full resize-none text-void-fg-1 placeholder:text-void-fg-3 px-2 py-1 rounded-sm
|
||||
className={`w-full resize-none bg-void-bg-1 text-void-fg-1 placeholder:text-void-fg-3 border border-void-border-2 focus:border-void-border-1
|
||||
${compact ? 'py-1 px-2' : 'py-2 px-4 '}
|
||||
rounded
|
||||
${disabled ? 'opacity-50 cursor-not-allowed' : ''}
|
||||
${className}`}
|
||||
style={{
|
||||
...passwordBlur && { WebkitTextSecurity: 'disc' },
|
||||
background: asCssVariable(inputBackground),
|
||||
color: asCssVariable(inputForeground)
|
||||
}}
|
||||
{...inputProps}
|
||||
type={undefined} // VS Code is doing some annoyingness that breaks paste if this is defined
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
export const VoidInputBox = ({ onChangeText, onCreateInstance, inputBoxRef, placeholder, isPasswordField, multiline }: {
|
||||
onChangeText: (value: string) => void;
|
||||
styles?: Partial<IInputBoxStyles>,
|
||||
@@ -319,7 +356,7 @@ export const VoidSlider = ({
|
||||
size === 'xs' ? 'h-1' :
|
||||
size === 'sm' ? 'h-1.5' :
|
||||
size === 'sm+' ? 'h-2' : 'h-2.5'
|
||||
} bg-gray-200 dark:bg-gray-700 rounded-full cursor-pointer`}
|
||||
} bg-void-bg-2 rounded-full cursor-pointer`}
|
||||
onClick={handleTrackClick}
|
||||
>
|
||||
{/* Filled part of track */}
|
||||
@@ -328,12 +365,12 @@ export const VoidSlider = ({
|
||||
size === 'xs' ? 'h-1' :
|
||||
size === 'sm' ? 'h-1.5' :
|
||||
size === 'sm+' ? 'h-2' : 'h-2.5'
|
||||
} bg-gray-900 dark:bg-white rounded-full`}
|
||||
} bg-void-fg-1 rounded-full`}
|
||||
style={{ width: `${percentage}%` }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Thumb with sizes matching VoidSwitch */}
|
||||
{/* Thumb */}
|
||||
<div
|
||||
className={`absolute top-1/2 transform -translate-x-1/2 -translate-y-1/2
|
||||
${size === 'xxs' ? 'h-2 w-2' :
|
||||
@@ -341,7 +378,8 @@ export const VoidSlider = ({
|
||||
size === 'sm' ? 'h-3 w-3' :
|
||||
size === 'sm+' ? 'h-3.5 w-3.5' : 'h-4 w-4'
|
||||
}
|
||||
bg-white dark:bg-gray-900 rounded-full shadow-md ${disabled ? 'cursor-not-allowed' : 'cursor-grab active:cursor-grabbing'}`}
|
||||
bg-void-fg-1 rounded-full shadow-md ${disabled ? 'cursor-not-allowed' : 'cursor-grab active:cursor-grabbing'}
|
||||
border border-void-fg-1`}
|
||||
style={{ left: `${percentage}%`, zIndex: 2 }} // Ensure thumb is above the invisible clickable area
|
||||
onMouseDown={(e) => {
|
||||
if (disabled) return;
|
||||
@@ -380,6 +418,7 @@ export const VoidSwitch = ({
|
||||
onChange,
|
||||
size = 'md',
|
||||
disabled = false,
|
||||
...props
|
||||
}: {
|
||||
value: boolean;
|
||||
onChange: (value: boolean) => void;
|
||||
@@ -387,13 +426,13 @@ export const VoidSwitch = ({
|
||||
size?: 'xxs' | 'xs' | 'sm' | 'sm+' | 'md';
|
||||
}) => {
|
||||
return (
|
||||
<label className="inline-flex items-center">
|
||||
<label className="inline-flex items-center" {...props}>
|
||||
<div
|
||||
onClick={() => !disabled && onChange(!value)}
|
||||
className={`
|
||||
cursor-pointer
|
||||
relative inline-flex items-center rounded-full transition-colors duration-200 ease-in-out
|
||||
${value ? 'bg-gray-900 dark:bg-white' : 'bg-gray-200 dark:bg-gray-700'}
|
||||
${value ? 'bg-zinc-900 dark:bg-white' : 'bg-white dark:bg-zinc-600'}
|
||||
${disabled ? 'opacity-25' : ''}
|
||||
${size === 'xxs' ? 'h-3 w-5' : ''}
|
||||
${size === 'xs' ? 'h-4 w-7' : ''}
|
||||
@@ -404,7 +443,7 @@ export const VoidSwitch = ({
|
||||
>
|
||||
<span
|
||||
className={`
|
||||
inline-block transform rounded-full bg-white dark:bg-gray-900 shadow transition-transform duration-200 ease-in-out
|
||||
inline-block transform rounded-full bg-white dark:bg-zinc-900 shadow transition-transform duration-200 ease-in-out
|
||||
${size === 'xxs' ? 'h-2 w-2' : ''}
|
||||
${size === 'xs' ? 'h-2.5 w-2.5' : ''}
|
||||
${size === 'sm' ? 'h-3 w-3' : ''}
|
||||
@@ -603,7 +642,7 @@ export const VoidCustomDropdownBox = <T extends NonNullable<any>>({
|
||||
className="flex items-center h-4 bg-transparent whitespace-nowrap hover:brightness-90 w-full"
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
>
|
||||
<span className={`max-w-[120px] truncate ${arrowTouchesText ? 'mr-1' : ''}`}>
|
||||
<span className={`truncate ${arrowTouchesText ? 'mr-1' : ''}`}>
|
||||
{getOptionDisplayName(selectedOption)}
|
||||
</span>
|
||||
<svg
|
||||
@@ -922,9 +961,9 @@ export const BlockCode = ({ initValue, language, maxHeight, showScrollbars }: Bl
|
||||
}
|
||||
|
||||
|
||||
export const VoidButton = ({ children, disabled, onClick }: { children: React.ReactNode; disabled?: boolean; onClick: () => void }) => {
|
||||
export const VoidButtonBgDarken = ({ children, disabled, onClick, className }: { children: React.ReactNode; disabled?: boolean; onClick: () => void; className?: string }) => {
|
||||
return <button disabled={disabled}
|
||||
className='px-3 py-1 bg-black/10 dark:bg-gray-200/10 rounded-sm overflow-hidden whitespace-nowrap'
|
||||
className={`px-3 py-1 bg-black/10 dark:bg-white/10 rounded-sm overflow-hidden whitespace-nowrap flex items-center justify-center ${className || ''}`}
|
||||
onClick={onClick}
|
||||
>{children}</button>
|
||||
}
|
||||
|
||||
@@ -22,7 +22,6 @@ import { IThemeService } from '../../../../../../../platform/theme/common/themeS
|
||||
import { ILLMMessageService } from '../../../../common/sendLLMMessageService.js';
|
||||
import { IRefreshModelService } from '../../../../../../../workbench/contrib/void/common/refreshModelService.js';
|
||||
import { IVoidSettingsService } from '../../../../../../../workbench/contrib/void/common/voidSettingsService.js';
|
||||
import { IEditCodeService } from '../../../editCodeServiceInterface.js'
|
||||
|
||||
import { ISidebarStateService } from '../../../sidebarStateService.js';
|
||||
import { IInstantiationService } from '../../../../../../../platform/instantiation/common/instantiation.js'
|
||||
@@ -46,6 +45,9 @@ import { ILanguageService } from '../../../../../../../editor/common/languages/l
|
||||
import { IVoidModelService } from '../../../../common/voidModelService.js'
|
||||
import { IWorkspaceContextService } from '../../../../../../../platform/workspace/common/workspace.js'
|
||||
import { IVoidCommandBarService } from '../../../voidCommandBarService.js'
|
||||
import { INativeHostService } from '../../../../../../../platform/native/common/native.js';
|
||||
import { IEditCodeService } from '../../../editCodeServiceInterface.js'
|
||||
import { IToolsService } from '../../../toolsService.js'
|
||||
|
||||
|
||||
// normally to do this you'd use a useEffect that calls .onDidChangeState(), but useEffect mounts too late and misses initial state changes
|
||||
@@ -213,6 +215,8 @@ const getReactAccessor = (accessor: ServicesAccessor) => {
|
||||
IWorkspaceContextService: accessor.get(IWorkspaceContextService),
|
||||
|
||||
IVoidCommandBarService: accessor.get(IVoidCommandBarService),
|
||||
INativeHostService: accessor.get(INativeHostService),
|
||||
IToolsService: accessor.get(IToolsService),
|
||||
|
||||
} as const
|
||||
return reactAccessor
|
||||
@@ -348,9 +352,9 @@ export const useCommandBarURIListener = (listener: (uri: URI) => void) => {
|
||||
export const useCommandBarState = () => {
|
||||
const accessor = useAccessor()
|
||||
const commandBarService = accessor.get('IVoidCommandBarService')
|
||||
const [s, ss] = useState({ state: commandBarService.stateOfURI, sortedURIs: commandBarService.sortedURIs });
|
||||
const [s, ss] = useState({ stateOfURI: commandBarService.stateOfURI, sortedURIs: commandBarService.sortedURIs });
|
||||
const listener = useCallback(() => {
|
||||
ss({ state: commandBarService.stateOfURI, sortedURIs: commandBarService.sortedURIs });
|
||||
ss({ stateOfURI: commandBarService.stateOfURI, sortedURIs: commandBarService.sortedURIs });
|
||||
}, [commandBarService])
|
||||
useCommandBarURIListener(listener)
|
||||
|
||||
|
||||
@@ -1,128 +1,130 @@
|
||||
import { useEffect } from 'react';
|
||||
// Get rid of this as it was causing lag
|
||||
|
||||
export const useScrollbarStyles = (containerRef: React.MutableRefObject<HTMLDivElement | null>) => {
|
||||
useEffect(() => {
|
||||
if (!containerRef.current) return;
|
||||
// import { useEffect } from 'react';
|
||||
|
||||
// Create selector for specific overflow classes
|
||||
const overflowSelector = [
|
||||
'[class*="overflow-auto"]',
|
||||
'[class*="overflow-x-auto"]',
|
||||
'[class*="overflow-y-auto"]'
|
||||
].join(',');
|
||||
// export const useScrollbarStyles = (containerRef: React.RefObject<HTMLDivElement | null>) => {
|
||||
// useEffect(() => {
|
||||
// if (!containerRef.current) return;
|
||||
|
||||
// Function to initialize scrollbar styles for elements
|
||||
const initializeScrollbarStyles = () => {
|
||||
// Get all matching elements within the container, including the container itself
|
||||
const scrollElements = [
|
||||
...(containerRef.current?.matches(overflowSelector) ? [containerRef.current] : []),
|
||||
...Array.from(containerRef.current?.querySelectorAll(overflowSelector) || [])
|
||||
];
|
||||
// // Create selector for specific overflow classes
|
||||
// const overflowSelector = [
|
||||
// '[class*="overflow-auto"]',
|
||||
// '[class*="overflow-x-auto"]',
|
||||
// '[class*="overflow-y-auto"]'
|
||||
// ].join(',');
|
||||
|
||||
// Apply basic styling to all elements
|
||||
scrollElements.forEach(element => {
|
||||
element.classList.add('void-scrollable-element');
|
||||
});
|
||||
// // Function to initialize scrollbar styles for elements
|
||||
// const initializeScrollbarStyles = () => {
|
||||
// // Get all matching elements within the container, including the container itself
|
||||
// const scrollElements = [
|
||||
// ...(containerRef.current?.matches(overflowSelector) ? [containerRef.current] : []),
|
||||
// ...Array.from(containerRef.current?.querySelectorAll(overflowSelector) || [])
|
||||
// ];
|
||||
|
||||
// Only initialize fade effects for elements that haven't been initialized yet
|
||||
scrollElements.forEach(element => {
|
||||
if (!(element as any).__scrollbarCleanup) {
|
||||
let fadeTimeout: NodeJS.Timeout | null = null;
|
||||
let fadeInterval: NodeJS.Timeout | null = null;
|
||||
// // Apply basic styling to all elements
|
||||
// scrollElements.forEach(element => {
|
||||
// element.classList.add('void-scrollable-element');
|
||||
// });
|
||||
|
||||
const fadeIn = () => {
|
||||
if (fadeInterval) clearInterval(fadeInterval);
|
||||
// // Only initialize fade effects for elements that haven't been initialized yet
|
||||
// scrollElements.forEach(element => {
|
||||
// if (!(element as any).__scrollbarCleanup) {
|
||||
// let fadeTimeout: NodeJS.Timeout | null = null;
|
||||
// let fadeInterval: NodeJS.Timeout | null = null;
|
||||
|
||||
let step = 0;
|
||||
fadeInterval = setInterval(() => {
|
||||
if (step <= 10) {
|
||||
element.classList.remove(`show-scrollbar-${step - 1}`);
|
||||
element.classList.add(`show-scrollbar-${step}`);
|
||||
step++;
|
||||
} else {
|
||||
clearInterval(fadeInterval!);
|
||||
}
|
||||
}, 10);
|
||||
};
|
||||
// const fadeIn = () => {
|
||||
// if (fadeInterval) clearInterval(fadeInterval);
|
||||
|
||||
const fadeOut = () => {
|
||||
if (fadeInterval) clearInterval(fadeInterval);
|
||||
// let step = 0;
|
||||
// fadeInterval = setInterval(() => {
|
||||
// if (step <= 10) {
|
||||
// element.classList.remove(`show-scrollbar-${step - 1}`);
|
||||
// element.classList.add(`show-scrollbar-${step}`);
|
||||
// step++;
|
||||
// } else {
|
||||
// clearInterval(fadeInterval!);
|
||||
// }
|
||||
// }, 10);
|
||||
// };
|
||||
|
||||
let step = 10;
|
||||
fadeInterval = setInterval(() => {
|
||||
if (step >= 0) {
|
||||
element.classList.remove(`show-scrollbar-${step + 1}`);
|
||||
element.classList.add(`show-scrollbar-${step}`);
|
||||
step--;
|
||||
} else {
|
||||
clearInterval(fadeInterval!);
|
||||
}
|
||||
}, 60);
|
||||
};
|
||||
// const fadeOut = () => {
|
||||
// if (fadeInterval) clearInterval(fadeInterval);
|
||||
|
||||
const onMouseEnter = () => {
|
||||
if (fadeTimeout) clearTimeout(fadeTimeout);
|
||||
if (fadeInterval) clearInterval(fadeInterval);
|
||||
fadeIn();
|
||||
};
|
||||
// let step = 10;
|
||||
// fadeInterval = setInterval(() => {
|
||||
// if (step >= 0) {
|
||||
// element.classList.remove(`show-scrollbar-${step + 1}`);
|
||||
// element.classList.add(`show-scrollbar-${step}`);
|
||||
// step--;
|
||||
// } else {
|
||||
// clearInterval(fadeInterval!);
|
||||
// }
|
||||
// }, 60);
|
||||
// };
|
||||
|
||||
const onMouseLeave = () => {
|
||||
if (fadeTimeout) clearTimeout(fadeTimeout);
|
||||
fadeTimeout = setTimeout(() => {
|
||||
fadeOut();
|
||||
}, 10);
|
||||
};
|
||||
// const onMouseEnter = () => {
|
||||
// if (fadeTimeout) clearTimeout(fadeTimeout);
|
||||
// if (fadeInterval) clearInterval(fadeInterval);
|
||||
// fadeIn();
|
||||
// };
|
||||
|
||||
element.addEventListener('mouseenter', onMouseEnter);
|
||||
element.addEventListener('mouseleave', onMouseLeave);
|
||||
// const onMouseLeave = () => {
|
||||
// if (fadeTimeout) clearTimeout(fadeTimeout);
|
||||
// fadeTimeout = setTimeout(() => {
|
||||
// fadeOut();
|
||||
// }, 10);
|
||||
// };
|
||||
|
||||
// Store cleanup function
|
||||
const cleanup = () => {
|
||||
element.removeEventListener('mouseenter', onMouseEnter);
|
||||
element.removeEventListener('mouseleave', onMouseLeave);
|
||||
if (fadeTimeout) clearTimeout(fadeTimeout);
|
||||
if (fadeInterval) clearInterval(fadeInterval);
|
||||
element.classList.remove('void-scrollable-element');
|
||||
// Remove any remaining show-scrollbar classes
|
||||
for (let i = 0; i <= 10; i++) {
|
||||
element.classList.remove(`show-scrollbar-${i}`);
|
||||
}
|
||||
};
|
||||
// element.addEventListener('mouseenter', onMouseEnter);
|
||||
// element.addEventListener('mouseleave', onMouseLeave);
|
||||
|
||||
// Store the cleanup function on the element for later use
|
||||
(element as any).__scrollbarCleanup = cleanup;
|
||||
}
|
||||
});
|
||||
};
|
||||
// // Store cleanup function
|
||||
// const cleanup = () => {
|
||||
// element.removeEventListener('mouseenter', onMouseEnter);
|
||||
// element.removeEventListener('mouseleave', onMouseLeave);
|
||||
// if (fadeTimeout) clearTimeout(fadeTimeout);
|
||||
// if (fadeInterval) clearInterval(fadeInterval);
|
||||
// element.classList.remove('void-scrollable-element');
|
||||
// // Remove any remaining show-scrollbar classes
|
||||
// for (let i = 0; i <= 10; i++) {
|
||||
// element.classList.remove(`show-scrollbar-${i}`);
|
||||
// }
|
||||
// };
|
||||
|
||||
// Initialize for the first time
|
||||
initializeScrollbarStyles();
|
||||
// // Store the cleanup function on the element for later use
|
||||
// (element as any).__scrollbarCleanup = cleanup;
|
||||
// }
|
||||
// });
|
||||
// };
|
||||
|
||||
// Set up mutation observer to do the same
|
||||
const observer = new MutationObserver(() => {
|
||||
initializeScrollbarStyles();
|
||||
});
|
||||
// // Initialize for the first time
|
||||
// initializeScrollbarStyles();
|
||||
|
||||
// Start observing the container for child changes
|
||||
observer.observe(containerRef.current, {
|
||||
childList: true,
|
||||
subtree: true
|
||||
});
|
||||
// // Set up mutation observer to do the same
|
||||
// const observer = new MutationObserver(() => {
|
||||
// initializeScrollbarStyles();
|
||||
// });
|
||||
|
||||
return () => {
|
||||
observer.disconnect();
|
||||
// Your existing cleanup code...
|
||||
if (containerRef.current) {
|
||||
const scrollElements = [
|
||||
...(containerRef.current.matches(overflowSelector) ? [containerRef.current] : []),
|
||||
...Array.from(containerRef.current.querySelectorAll(overflowSelector))
|
||||
];
|
||||
scrollElements.forEach(element => {
|
||||
if ((element as any).__scrollbarCleanup) {
|
||||
(element as any).__scrollbarCleanup();
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
}, [containerRef]);
|
||||
};
|
||||
// // Start observing the container for child changes
|
||||
// observer.observe(containerRef.current, {
|
||||
// childList: true,
|
||||
// subtree: true
|
||||
// });
|
||||
|
||||
// return () => {
|
||||
// observer.disconnect();
|
||||
// // Your existing cleanup code...
|
||||
// if (containerRef.current) {
|
||||
// const scrollElements = [
|
||||
// ...(containerRef.current.matches(overflowSelector) ? [containerRef.current] : []),
|
||||
// ...Array.from(containerRef.current.querySelectorAll(overflowSelector))
|
||||
// ];
|
||||
// scrollElements.forEach(element => {
|
||||
// if ((element as any).__scrollbarCleanup) {
|
||||
// (element as any).__scrollbarCleanup();
|
||||
// }
|
||||
// });
|
||||
// }
|
||||
// };
|
||||
// }, [containerRef]);
|
||||
// };
|
||||
|
||||
+46
-36
@@ -9,8 +9,9 @@ import { useAccessor, useCommandBarState, useIsDark } from '../util/services.js'
|
||||
import '../styles.css'
|
||||
import { useCallback, useEffect, useState, useRef } from 'react';
|
||||
import { ScrollType } from '../../../../../../../editor/common/editorCommon.js';
|
||||
import { acceptAllBg, acceptBorder, buttonFontSize, buttonTextColor, rejectAllBg, rejectBorder } from '../../../../common/helpers/colors.js';
|
||||
import { acceptAllBg, acceptBorder, buttonFontSize, buttonTextColor, rejectBg, rejectBorder } from '../../../../common/helpers/colors.js';
|
||||
import { VoidCommandBarProps } from '../../../voidCommandBarService.js';
|
||||
import { AcceptAllButtonWrapper, RejectAllButtonWrapper } from '../sidebar-tsx/SidebarChat.js';
|
||||
|
||||
export const VoidCommandBarMain = ({ uri, editor }: VoidCommandBarProps) => {
|
||||
const isDark = useIsDark()
|
||||
@@ -39,7 +40,7 @@ const VoidCommandBar = ({ uri, editor }: VoidCommandBarProps) => {
|
||||
const commandService = accessor.get('ICommandService')
|
||||
const commandBarService = accessor.get('IVoidCommandBarService')
|
||||
const voidModelService = accessor.get('IVoidModelService')
|
||||
const { state: commandBarState, sortedURIs: sortedCommandBarURIs } = useCommandBarState()
|
||||
const { stateOfURI: commandBarState, sortedURIs: sortedCommandBarURIs } = useCommandBarState()
|
||||
|
||||
|
||||
// useEffect(() => {
|
||||
@@ -97,7 +98,7 @@ const VoidCommandBar = ({ uri, editor }: VoidCommandBarProps) => {
|
||||
if (diffid === undefined) return
|
||||
const diff = editCodeService.diffOfId[diffid]
|
||||
if (!diff) return
|
||||
editor.revealLineNearTop(diff.startLine, ScrollType.Immediate)
|
||||
editor.revealLineNearTop(diff.startLine - 1, ScrollType.Immediate)
|
||||
commandBarService.setDiffIdx(uri, idx)
|
||||
}
|
||||
const getNextUriIdx = (step: 1 | -1) => {
|
||||
@@ -211,38 +212,47 @@ const VoidCommandBar = ({ uri, editor }: VoidCommandBarProps) => {
|
||||
|
||||
if (!isADiffZoneInAnyFile) return null
|
||||
|
||||
const acceptAllButton = <button
|
||||
className='text-nowrap'
|
||||
// const acceptAllButton = <button
|
||||
// className='text-nowrap'
|
||||
// onClick={onAcceptAll}
|
||||
// style={{
|
||||
// backgroundColor: acceptAllBg,
|
||||
// border: acceptBorder,
|
||||
// color: buttonTextColor,
|
||||
// fontSize: buttonFontSize,
|
||||
// padding: '2px 4px',
|
||||
// borderRadius: '6px',
|
||||
// cursor: 'pointer'
|
||||
// }}
|
||||
// >
|
||||
// Accept File
|
||||
// </button>
|
||||
|
||||
// const rejectAllButton = <button
|
||||
// className='text-nowrap'
|
||||
// onClick={onRejectAll}
|
||||
// style={{
|
||||
// backgroundColor: rejectBg,
|
||||
// border: rejectBorder,
|
||||
// color: 'white',
|
||||
// fontSize: buttonFontSize,
|
||||
// padding: '2px 4px',
|
||||
// borderRadius: '6px',
|
||||
// cursor: 'pointer'
|
||||
// }}
|
||||
// >
|
||||
// Reject File
|
||||
// </button>
|
||||
|
||||
const acceptAllButton = <AcceptAllButtonWrapper
|
||||
text={'Keep Changes'}
|
||||
onClick={onAcceptAll}
|
||||
style={{
|
||||
backgroundColor: acceptAllBg,
|
||||
border: acceptBorder,
|
||||
color: buttonTextColor,
|
||||
fontSize: buttonFontSize,
|
||||
padding: '2px 4px',
|
||||
borderRadius: '6px',
|
||||
cursor: 'pointer'
|
||||
}}
|
||||
>
|
||||
Accept File
|
||||
</button>
|
||||
/>
|
||||
|
||||
|
||||
const rejectAllButton = <button
|
||||
className='text-nowrap'
|
||||
const rejectAllButton = <RejectAllButtonWrapper
|
||||
text={'Reject All'}
|
||||
onClick={onRejectAll}
|
||||
style={{
|
||||
backgroundColor: rejectAllBg,
|
||||
border: rejectBorder,
|
||||
color: 'white',
|
||||
fontSize: buttonFontSize,
|
||||
padding: '2px 4px',
|
||||
borderRadius: '6px',
|
||||
cursor: 'pointer'
|
||||
}}
|
||||
>
|
||||
Reject File
|
||||
</button>
|
||||
/>
|
||||
|
||||
const acceptRejectAllButtons = <div className="flex items-center gap-1 text-sm">
|
||||
{acceptAllButton}
|
||||
@@ -271,9 +281,9 @@ const VoidCommandBar = ({ uri, editor }: VoidCommandBarProps) => {
|
||||
<div className="flex flex-col gap-1">
|
||||
{/* Changes in file */}
|
||||
<div className={`${!isADiffZoneInThisFile ? 'hidden' : ''} flex items-center ${upDownDisabled ? 'opacity-50' : ''}`}>
|
||||
{downButton}
|
||||
{upButton}
|
||||
<span className="min-w-16 px-2 text-xs">
|
||||
{downButton}
|
||||
<span className="min-w-16 px-2 text-xs leading-[1]">
|
||||
{isADiffInThisFile ?
|
||||
`Diff ${(currDiffIdx ?? 0) + 1} of ${sortedDiffIds.length}`
|
||||
: streamState === 'streaming' ?
|
||||
@@ -289,7 +299,7 @@ const VoidCommandBar = ({ uri, editor }: VoidCommandBarProps) => {
|
||||
{/* <div className="w-px h-3 bg-void-border-3 mx-0.5 shadow-sm"></div> */}
|
||||
{rightButton}
|
||||
{/* <div className="w-px h-3 bg-void-border-3 mx-0.5 shadow-sm"></div> */}
|
||||
<span className="min-w-16 px-2 text-xs">
|
||||
<span className="min-w-16 px-2 text-xs leading-[1]">
|
||||
{currFileIdx !== null ?
|
||||
`File ${currFileIdx + 1} of ${sortedCommandBarURIs.length}`
|
||||
: `${sortedCommandBarURIs.length} file${sortedCommandBarURIs.length === 1 ? '' : 's'} changed`
|
||||
@@ -299,7 +309,7 @@ const VoidCommandBar = ({ uri, editor }: VoidCommandBarProps) => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
return <div className={`flex flex-col items-center gap-y-2 mx-2 pointer-events-auto`}>
|
||||
return <div className={`flex flex-col items-center gap-y-2 pointer-events-auto`}>
|
||||
{showAcceptRejectAll && acceptRejectAllButtons}
|
||||
{leftRightUpDownButtons}
|
||||
|
||||
+170
@@ -0,0 +1,170 @@
|
||||
/*--------------------------------------------------------------------------------------
|
||||
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
|
||||
import { useAccessor, useActiveURI, useIsDark, useSettingsState } from '../util/services.js';
|
||||
|
||||
import '../styles.css'
|
||||
import { VOID_CTRL_K_ACTION_ID, VOID_CTRL_L_ACTION_ID } from '../../../actionIDs.js';
|
||||
import { Circle, MoreVertical } from 'lucide-react';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { VoidSelectionHelperProps } from '../../../../../../contrib/void/browser/voidSelectionHelperWidget.js';
|
||||
import { VOID_OPEN_SETTINGS_ACTION_ID } from '../../../voidSettingsPane.js';
|
||||
|
||||
|
||||
export const VoidSelectionHelperMain = (props: VoidSelectionHelperProps) => {
|
||||
|
||||
const isDark = useIsDark()
|
||||
|
||||
return <div
|
||||
className={`@@void-scope ${isDark ? 'dark' : ''}`}
|
||||
>
|
||||
<VoidSelectionHelper {...props} />
|
||||
</div>
|
||||
}
|
||||
|
||||
|
||||
|
||||
const VoidSelectionHelper = ({ rerenderKey }: VoidSelectionHelperProps) => {
|
||||
|
||||
|
||||
const accessor = useAccessor()
|
||||
const keybindingService = accessor.get('IKeybindingService')
|
||||
const commandService = accessor.get('ICommandService')
|
||||
|
||||
const ctrlLKeybind = keybindingService.lookupKeybinding(VOID_CTRL_L_ACTION_ID)
|
||||
const ctrlKKeybind = keybindingService.lookupKeybinding(VOID_CTRL_K_ACTION_ID)
|
||||
|
||||
const dividerHTML = <div className='w-[0.5px] bg-void-border-3'></div>
|
||||
|
||||
const [reactRerenderCount, setReactRerenderKey] = useState(rerenderKey)
|
||||
const [clickState, setClickState] = useState<'init' | 'clickedOption' | 'clickedMore'>('init')
|
||||
|
||||
useEffect(() => {
|
||||
const disposable = commandService.onWillExecuteCommand(e => {
|
||||
if (e.commandId === VOID_CTRL_L_ACTION_ID || e.commandId === VOID_CTRL_K_ACTION_ID) {
|
||||
setClickState('clickedOption')
|
||||
}
|
||||
});
|
||||
|
||||
return () => {
|
||||
disposable.dispose();
|
||||
};
|
||||
}, [commandService, setClickState]);
|
||||
|
||||
|
||||
// rerender when the key changes
|
||||
if (reactRerenderCount !== rerenderKey) {
|
||||
setReactRerenderKey(rerenderKey)
|
||||
setClickState('init')
|
||||
}
|
||||
// useEffect(() => {
|
||||
// }, [rerenderKey, reactRerenderCount, setReactRerenderKey, setClickState])
|
||||
|
||||
// if the user selected an option, close
|
||||
|
||||
|
||||
if (clickState === 'clickedOption') {
|
||||
return null
|
||||
}
|
||||
|
||||
const defaultHTML = <>
|
||||
{ctrlLKeybind &&
|
||||
<div
|
||||
className='
|
||||
flex items-center px-2 py-1.5
|
||||
cursor-pointer
|
||||
'
|
||||
onClick={() => {
|
||||
commandService.executeCommand(VOID_CTRL_L_ACTION_ID)
|
||||
setClickState('clickedOption');
|
||||
}}
|
||||
>
|
||||
<span>Add to Chat</span>
|
||||
<span className='ml-1 px-1 rounded bg-[var(--vscode-keybindingLabel-background)] text-[var(--vscode-keybindingLabel-foreground)] border border-[var(--vscode-keybindingLabel-border)]'>
|
||||
{ctrlLKeybind.getLabel()}
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
{ctrlLKeybind && ctrlKKeybind &&
|
||||
dividerHTML
|
||||
}
|
||||
{ctrlKKeybind &&
|
||||
<div
|
||||
className='
|
||||
flex items-center px-2 py-1.5
|
||||
cursor-pointer
|
||||
'
|
||||
onClick={() => {
|
||||
commandService.executeCommand(VOID_CTRL_K_ACTION_ID)
|
||||
setClickState('clickedOption');
|
||||
}}
|
||||
>
|
||||
<span className='ml-1'>Edit Inline</span>
|
||||
<span className='ml-1 px-1 rounded bg-[var(--vscode-keybindingLabel-background)] text-[var(--vscode-keybindingLabel-foreground)] border border-[var(--vscode-keybindingLabel-border)]'>
|
||||
{ctrlKKeybind.getLabel()}
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
|
||||
{dividerHTML}
|
||||
|
||||
<div
|
||||
className='
|
||||
flex items-center px-0.5
|
||||
cursor-pointer
|
||||
'
|
||||
onClick={() => {
|
||||
setClickState('clickedMore');
|
||||
}}
|
||||
>
|
||||
<MoreVertical className="w-4" />
|
||||
</div>
|
||||
</>
|
||||
|
||||
|
||||
const moreOptionsHTML = <>
|
||||
<div
|
||||
className='
|
||||
flex items-center px-2 py-1.5
|
||||
cursor-pointer
|
||||
'
|
||||
onClick={() => {
|
||||
commandService.executeCommand(VOID_OPEN_SETTINGS_ACTION_ID);
|
||||
setClickState('clickedOption');
|
||||
}}
|
||||
>
|
||||
Disable Suggestions?
|
||||
</div>
|
||||
|
||||
{dividerHTML}
|
||||
|
||||
<div
|
||||
className='
|
||||
flex items-center px-0.5
|
||||
cursor-pointer
|
||||
'
|
||||
onClick={() => {
|
||||
setClickState('init');
|
||||
}}
|
||||
>
|
||||
<MoreVertical className="w-4" />
|
||||
</div>
|
||||
</>
|
||||
|
||||
return <div className='
|
||||
pointer-events-auto select-none
|
||||
z-[1000]
|
||||
rounded-sm shadow-md flex flex-nowrap text-nowrap
|
||||
border border-void-border-3 bg-void-bg-2
|
||||
transition-all duration-200
|
||||
'>
|
||||
{clickState === 'init' ? defaultHTML
|
||||
: clickState === 'clickedMore' ? moreOptionsHTML
|
||||
: <></>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
+4
@@ -5,5 +5,9 @@
|
||||
|
||||
import { mountFnGenerator } from '../util/mountFnGenerator.js'
|
||||
import { VoidCommandBarMain } from './VoidCommandBar.js'
|
||||
import { VoidSelectionHelperMain } from './VoidSelectionHelper.js'
|
||||
|
||||
export const mountVoidCommandBar = mountFnGenerator(VoidCommandBarMain)
|
||||
|
||||
export const mountVoidSelectionHelper = mountFnGenerator(VoidSelectionHelperMain)
|
||||
|
||||
@@ -0,0 +1,747 @@
|
||||
/*--------------------------------------------------------------------------------------
|
||||
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useAccessor, useIsDark, useSettingsState } from '../util/services.js';
|
||||
import { Check, ExternalLink, X } from 'lucide-react';
|
||||
import { displayInfoOfProviderName, ProviderName, providerNames, refreshableProviderNames } from '../../../../common/voidSettingsTypes.js';
|
||||
import { getModelCapabilities, ollamaRecommendedModels } from '../../../../common/modelCapabilities.js';
|
||||
import { ChatMarkdownRender } from '../markdown/ChatMarkdownRender.js';
|
||||
import { AddModelInputBox, AnimatedCheckmarkButton, ollamaSetupInstructions, OneClickSwitchButton, SettingsForProvider } from '../void-settings-tsx/Settings.js';
|
||||
|
||||
const OVERRIDE_VALUE = true
|
||||
|
||||
export const VoidOnboarding = () => {
|
||||
|
||||
const voidSettingsState = useSettingsState()
|
||||
const isOnboardingComplete = voidSettingsState.globalSettings.isOnboardingComplete || OVERRIDE_VALUE
|
||||
|
||||
const isDark = useIsDark()
|
||||
|
||||
return (
|
||||
<div className={`@@void-scope ${isDark ? 'dark' : ''}`}>
|
||||
<div
|
||||
className={`
|
||||
hidden
|
||||
bg-void-bg-3 fixed top-0 right-0 bottom-0 left-0 width-full h-full z-[99999]
|
||||
transition-all duration-1000 ${isOnboardingComplete ? 'opacity-0 pointer-events-none' : 'opacity-100 pointer-events-auto'}
|
||||
`}
|
||||
>
|
||||
<VoidOnboardingContent />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
const FADE_DURATION_MS = 2000
|
||||
|
||||
|
||||
const FadeIn = ({ children, className, delayMs = 0, ...props }: { children: React.ReactNode, delayMs?: number, className?: string } & React.HTMLAttributes<HTMLDivElement>) => {
|
||||
|
||||
const [opacity, setOpacity] = useState(0)
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
const timeout = setTimeout(() => {
|
||||
setOpacity(1)
|
||||
}, delayMs)
|
||||
|
||||
return () => clearTimeout(timeout)
|
||||
}, [setOpacity, delayMs])
|
||||
|
||||
|
||||
return (
|
||||
<div className={className} style={{ opacity, transition: `opacity ${FADE_DURATION_MS}ms ease-in-out` }} {...props}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Onboarding
|
||||
// OnboardingPage
|
||||
// title:
|
||||
// div
|
||||
// "Welcome to Void"
|
||||
// image
|
||||
// content:<></>
|
||||
// title
|
||||
// content
|
||||
// prev/next
|
||||
|
||||
// OnboardingPage
|
||||
// title:
|
||||
// div
|
||||
// "How would you like to use Void?"
|
||||
// content:
|
||||
// ModelQuestionContent
|
||||
// |
|
||||
// div
|
||||
// "I want to:"
|
||||
// div
|
||||
// "Use the smartest models"
|
||||
// "Keep my data fully private"
|
||||
// "Save money"
|
||||
// "I don't know"
|
||||
// | div
|
||||
// | div
|
||||
// "We recommend using "
|
||||
// "Set API"
|
||||
// | div
|
||||
// ""
|
||||
// | div
|
||||
//
|
||||
// title
|
||||
// content
|
||||
// prev/next
|
||||
//
|
||||
// OnboardingPage
|
||||
// title
|
||||
// content
|
||||
// prev/next
|
||||
|
||||
|
||||
const NextButton = ({ onClick, ...props }: { onClick: () => void } & React.ButtonHTMLAttributes<HTMLButtonElement>) => {
|
||||
return (
|
||||
<button
|
||||
onClick={onClick}
|
||||
className="px-6 py-2 bg-[#0e70c0] enabled:hover:bg-[#1177cb] disabled:opacity-50 disabled:cursor-not-allowed rounded text-white duration-300 transition-all"
|
||||
{...props.disabled && {
|
||||
'data-tooltip-id': 'void-tooltip',
|
||||
'data-tooltip-content': 'Disabled (Please enter all required fields or choose another Provider)',
|
||||
'data-tooltip-place': 'top',
|
||||
}}
|
||||
{...props}
|
||||
>
|
||||
Next
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
const SkipButton = ({ onClick, ...props }: { onClick: () => void } & React.ButtonHTMLAttributes<HTMLButtonElement>) => {
|
||||
return (
|
||||
<button
|
||||
onClick={onClick}
|
||||
className="px-6 py-2 rounded bg-void-bg-2 hover:bg-void-bg-3 text-void-fg-2 duration-300 transition-all"
|
||||
{...props}
|
||||
>
|
||||
Skip
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
const PreviousButton = ({ onClick, ...props }: { onClick: () => void } & React.ButtonHTMLAttributes<HTMLButtonElement>) => {
|
||||
return (
|
||||
<button
|
||||
onClick={onClick}
|
||||
className="px-6 py-2 rounded text-void-fg-3 opacity-80 hover:brightness-110 duration-300 transition-all"
|
||||
{...props}
|
||||
>
|
||||
Back
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
|
||||
const OnboardingPageShell = ({ top, bottom, content, hasMaxWidth = true, className = '', }: {
|
||||
top?: React.ReactNode,
|
||||
bottom?: React.ReactNode,
|
||||
content?: React.ReactNode,
|
||||
hasMaxWidth?: boolean,
|
||||
className?: string,
|
||||
}) => {
|
||||
return (
|
||||
<div className={`min-h-full flex flex-col gap-4 w-full mx-auto ${hasMaxWidth ? 'max-w-[600px]' : ''} ${className}`}>
|
||||
<FadeIn className='w-full pt-16'>{top}</FadeIn>
|
||||
<FadeIn className='w-full my-auto'>{content}</FadeIn>
|
||||
<div className='w-full pb-8'>{bottom}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const OllamaDownloadOrRemoveModelButton = ({ modelName, isModelInstalled, sizeGb }: { modelName: string, isModelInstalled: boolean, sizeGb: number | false | 'not-known' }) => {
|
||||
|
||||
|
||||
// for now just link to the ollama download page
|
||||
return <a
|
||||
href={`https://ollama.com/library/${modelName}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center text-void-fg-2 hover:text-void-fg-1"
|
||||
>
|
||||
<ExternalLink className="w-3.5 h-3.5" />
|
||||
</a>
|
||||
|
||||
// if (isModelInstalled) {
|
||||
// return <div className="flex items-center">
|
||||
|
||||
// <span className="flex items-center">Uninstall</span>
|
||||
|
||||
// <IconShell1
|
||||
// className="ml-1"
|
||||
// Icon={Trash}
|
||||
// onClick={() => {
|
||||
|
||||
// setIsModelInstalling(false);
|
||||
// }}
|
||||
// />
|
||||
|
||||
// </div>
|
||||
// }
|
||||
|
||||
|
||||
|
||||
// else if (isModelInstalling) {
|
||||
// return <div className="flex items-center">
|
||||
|
||||
// <span className="flex items-center">{`Download? ${typeof sizeGb === 'number' ? `(${sizeGb} Gb)` : ''}`}</span>
|
||||
|
||||
// <IconShell1
|
||||
// className="ml-1"
|
||||
// Icon={Square}
|
||||
// onClick={() => {
|
||||
// // abort()
|
||||
|
||||
// // TODO!!!!!!!!!!! don't do this
|
||||
// setIsModelInstalling(false);
|
||||
// }}
|
||||
// />
|
||||
|
||||
// </div>
|
||||
// }
|
||||
|
||||
|
||||
// else if (!isModelInstalled) {
|
||||
|
||||
// return <div className="flex items-center">
|
||||
|
||||
// <span className="flex items-center">Download ({sizeGb} Gb)</span>
|
||||
|
||||
// <IconShell1
|
||||
// className="ml-1"
|
||||
// Icon={Download}
|
||||
// onClick={() => {
|
||||
// // this is a check for whether the model was installed:
|
||||
|
||||
// if (isModelInstalling) return
|
||||
|
||||
|
||||
// // TODO!!!!!! don't do this
|
||||
|
||||
|
||||
// // install(modelname), callback = setIsModelInstalling(false);
|
||||
|
||||
// setIsModelInstalling(true);
|
||||
// }}
|
||||
// />
|
||||
|
||||
// </div>
|
||||
|
||||
// }
|
||||
|
||||
// return <></>
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
const YesNoText = ({ val }: { val: boolean | null }) => {
|
||||
|
||||
return <div
|
||||
className={
|
||||
val === true ? "text text-green-500"
|
||||
: val === false ? 'text-red-500'
|
||||
: "text text-yellow-500"
|
||||
}
|
||||
>
|
||||
{
|
||||
val === true ? "Yes"
|
||||
: val === false ? 'No'
|
||||
: "Yes*"
|
||||
}
|
||||
</div>
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
const abbreviateNumber = (num: number): string => {
|
||||
if (num >= 1000000) {
|
||||
// For millions
|
||||
return Math.floor(num / 1000000) + 'M';
|
||||
} else if (num >= 1000) {
|
||||
// For thousands
|
||||
return Math.floor(num / 1000) + 'K';
|
||||
} else {
|
||||
// For numbers less than 1000
|
||||
return num.toString();
|
||||
}
|
||||
}
|
||||
|
||||
const TableOfModelsForProvider = ({ providerName }: { providerName: ProviderName }) => {
|
||||
|
||||
const accessor = useAccessor()
|
||||
const voidSettingsService = accessor.get('IVoidSettingsService')
|
||||
const voidSettingsState = useSettingsState()
|
||||
const isDetectableLocally = (refreshableProviderNames as ProviderName[]).includes(providerName)
|
||||
// const providerCapabilities = getProviderCapabilities(providerName)
|
||||
|
||||
|
||||
// info used to show the table
|
||||
const infoOfModelName: Record<string, { showAsDefault: boolean, isDownloaded: boolean }> = {}
|
||||
|
||||
voidSettingsState.settingsOfProvider[providerName].models.forEach(m => {
|
||||
infoOfModelName[m.modelName] = {
|
||||
showAsDefault: m.isDefault,
|
||||
isDownloaded: true
|
||||
}
|
||||
})
|
||||
|
||||
// special case columns for ollama; show recommended models as default
|
||||
if (providerName === 'ollama') {
|
||||
for (const modelName of ollamaRecommendedModels) {
|
||||
if (modelName in infoOfModelName) continue
|
||||
infoOfModelName[modelName] = {
|
||||
...infoOfModelName[modelName],
|
||||
showAsDefault: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return <table className="table-fixed border-collapse mb-6 bg-void-bg-2 text-sm mx-auto select-text">
|
||||
<thead>
|
||||
<tr className="border-b border-void-border-1 text-nowrap text-ellipsis">
|
||||
<th className="text-left py-2 px-3 font-normal text-void-fg-3 min-w-[200px]">Models Offered</th>
|
||||
<th className="text-left py-2 px-3 font-normal text-void-fg-3 min-w-[10%]">Cost/M</th>
|
||||
<th className="text-left py-2 px-3 font-normal text-void-fg-3 min-w-[10%]">Context</th>
|
||||
<th className="text-left py-2 px-3 font-normal text-void-fg-3 min-w-[10%]">Chat</th>
|
||||
<th className="text-left py-2 px-3 font-normal text-void-fg-3 min-w-[10%]">Agent</th>
|
||||
<th className="text-left py-2 px-3 font-normal text-void-fg-3 min-w-[10%]">Autotab</th>
|
||||
{/* <th className="text-left py-2 px-3 font-normal text-void-fg-3 min-w-[10%]">Reasoning</th> */}
|
||||
{isDetectableLocally && <th className="text-left py-2 px-3 font-normal text-void-fg-3 min-w-[10%]">Detected</th>}
|
||||
{providerName === 'ollama' && <th className="text-left py-2 px-3 font-normal text-void-fg-3">Download</th>}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{Object.keys(infoOfModelName).map(modelName => {
|
||||
const { showAsDefault, isDownloaded } = infoOfModelName[modelName]
|
||||
|
||||
|
||||
const capabilities = getModelCapabilities(providerName, modelName)
|
||||
const {
|
||||
downloadable,
|
||||
cost,
|
||||
supportsFIM,
|
||||
reasoningCapabilities,
|
||||
contextWindow,
|
||||
|
||||
isUnrecognizedModel,
|
||||
maxOutputTokens,
|
||||
supportsSystemMessage,
|
||||
} = capabilities
|
||||
|
||||
// TODO update this when tools work
|
||||
const supportsTools = !!!((capabilities as unknown as any).supportsTools)
|
||||
|
||||
const removeModelButton = <button
|
||||
className="absolute -left-1 top-1/2 transform -translate-y-1/2 -translate-x-full text-void-fg-3 hover:text-void-fg-1 text-xs"
|
||||
onClick={() => voidSettingsService.deleteModel(providerName, modelName)}
|
||||
>
|
||||
<X className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<tr key={modelName} className="border-b border-void-border-1 hover:bg-void-bg-3/50">
|
||||
<td className="py-2 px-3 relative">
|
||||
{!showAsDefault && removeModelButton}
|
||||
{modelName}
|
||||
</td>
|
||||
<td className="py-2 px-3">${cost.output ?? ''}</td>
|
||||
<td className="py-2 px-3">{contextWindow ? abbreviateNumber(contextWindow) : ''}</td>
|
||||
<td className="py-2 px-3"><YesNoText val={true} /></td>
|
||||
<td className="py-2 px-3"><YesNoText val={!!supportsTools} /></td>
|
||||
<td className="py-2 px-3"><YesNoText val={!!supportsFIM} /></td>
|
||||
{/* <td className="py-2 px-3"><YesNoText val={!!reasoningCapabilities} /></td> */}
|
||||
{isDetectableLocally && <td className="py-2 px-3">{!!isDownloaded ? <Check className="w-4 h-4" /> : <></>}</td>}
|
||||
{providerName === 'ollama' && <th className="py-2 px-3">
|
||||
<OllamaDownloadOrRemoveModelButton modelName={modelName} isModelInstalled={infoOfModelName[modelName].isDownloaded} sizeGb={downloadable && downloadable.sizeGb} />
|
||||
</th>}
|
||||
|
||||
</tr>
|
||||
)
|
||||
})}
|
||||
<tr className="hover:bg-void-bg-3/50">
|
||||
<td className="py-2 px-3 text-void-accent">
|
||||
<AddModelInputBox
|
||||
key={providerName}
|
||||
providerName={providerName}
|
||||
compact={true} />
|
||||
</td>
|
||||
<td colSpan={4}></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
}
|
||||
|
||||
|
||||
|
||||
type WantToUseOption = 'smart' | 'private' | 'cheap' | 'all'
|
||||
|
||||
const VoidOnboardingContent = () => {
|
||||
|
||||
|
||||
const accessor = useAccessor()
|
||||
const voidSettingsService = accessor.get('IVoidSettingsService')
|
||||
|
||||
const voidSettingsState = useSettingsState()
|
||||
|
||||
const [pageIndex, setPageIndex] = useState(0)
|
||||
|
||||
|
||||
// page 1 state
|
||||
const [wantToUseOption, setWantToUseOption] = useState<WantToUseOption>('smart')
|
||||
|
||||
// page 2 state
|
||||
const [selectedProviderName, setSelectedProviderName] = useState<ProviderName | null>(null)
|
||||
|
||||
const providerNamesOfWantToUseOption: { [wantToUseOption in WantToUseOption]: ProviderName[] } = {
|
||||
smart: ['anthropic', 'openAI', 'gemini', 'openRouter'],
|
||||
private: ['ollama', 'vLLM', 'openAICompatible'],
|
||||
cheap: ['gemini', 'deepseek', 'openRouter', 'ollama', 'vLLM'],
|
||||
all: providerNames,
|
||||
// TODO allow user to redo onboarding
|
||||
}
|
||||
|
||||
|
||||
const didFillInProviderSettings = selectedProviderName && voidSettingsState.settingsOfProvider[selectedProviderName]._didFillInProviderSettings
|
||||
const isApiKeyLongEnoughIfApiKeyExists = selectedProviderName && voidSettingsState.settingsOfProvider[selectedProviderName].apiKey ? voidSettingsState.settingsOfProvider[selectedProviderName].apiKey.length > 15 : true
|
||||
const isAtLeastOneModel = selectedProviderName && voidSettingsState.settingsOfProvider[selectedProviderName].models.length >= 1
|
||||
|
||||
const didFillInSelectedProviderSettings = !!(didFillInProviderSettings && isApiKeyLongEnoughIfApiKeyExists && isAtLeastOneModel)
|
||||
|
||||
const prevAndNextButtons = <div className="max-w-[600px] w-full mx-auto flex flex-col items-end">
|
||||
<div className="flex items-center gap-4">
|
||||
<PreviousButton
|
||||
onClick={() => { setPageIndex(pageIndex - 1) }}
|
||||
/>
|
||||
<NextButton
|
||||
onClick={() => { setPageIndex(pageIndex + 1) }}
|
||||
disabled={pageIndex === 2 && !didFillInSelectedProviderSettings}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
// cannot be md
|
||||
const basicDescOfWantToUseOption: { [wantToUseOption in WantToUseOption]: string } = {
|
||||
smart: "Models with the best performance on benchmarks.",
|
||||
private: "Fully private and hosted on your computer/network.",
|
||||
cheap: "Free and low-cost options.",
|
||||
all: "",
|
||||
}
|
||||
|
||||
// can be md
|
||||
const detailedDescOfWantToUseOption: { [wantToUseOption in WantToUseOption]: string } = {
|
||||
smart: "Most intelligent and best for agent mode.",
|
||||
private: "Private-hosted so your data never leaves your computer or network. [Email us](mailto:founders@voideditor.com) for help setting up at your company.",
|
||||
cheap: "Great deals like Gemini 2.5 Pro or self-host a model with Ollama or vLLM for free.",
|
||||
all: "",
|
||||
}
|
||||
|
||||
// set the selected provider name appropriately
|
||||
useEffect(() => {
|
||||
if (wantToUseOption && providerNamesOfWantToUseOption[wantToUseOption].length > 0) {
|
||||
setSelectedProviderName(providerNamesOfWantToUseOption[wantToUseOption][0]);
|
||||
} else {
|
||||
setSelectedProviderName(null);
|
||||
}
|
||||
}, [wantToUseOption]);
|
||||
|
||||
// set wantToUseOption to smart when page changes
|
||||
useEffect(() => {
|
||||
setWantToUseOption(wantToUseOption);
|
||||
}, [pageIndex]);
|
||||
|
||||
|
||||
// reset the page to page 0 if the user redos onboarding
|
||||
useEffect(() => {
|
||||
if (!voidSettingsState.globalSettings.isOnboardingComplete) {
|
||||
setPageIndex(0)
|
||||
}
|
||||
}, [setPageIndex, voidSettingsState.globalSettings.isOnboardingComplete])
|
||||
|
||||
|
||||
// TODO add a description next to the skip button saying (you can always restart the onboarding in Settings)
|
||||
const contentOfIdx: { [pageIndex: number]: React.ReactNode } = {
|
||||
// 0: <OnboardingPageShell
|
||||
// top={
|
||||
// <div className='bg-green-600 h-6 w-32' />
|
||||
// }
|
||||
// content={
|
||||
// <div className='bg-red-600 h-[10000px] w-32' />
|
||||
// }
|
||||
// bottom={
|
||||
// <div className='bg-blue-600 h-6 w-32' />
|
||||
// }
|
||||
// />,
|
||||
0: <OnboardingPageShell
|
||||
top={
|
||||
<div className="text-5xl font-light text-center">Welcome to Void</div>
|
||||
}
|
||||
content={
|
||||
<FadeIn
|
||||
delayMs={500}
|
||||
className="text-center"
|
||||
onClick={() => { setPageIndex(pageIndex + 1) }}
|
||||
>
|
||||
Get Started
|
||||
</FadeIn>
|
||||
}
|
||||
bottom={
|
||||
''
|
||||
}
|
||||
/>,
|
||||
1: <OnboardingPageShell
|
||||
hasMaxWidth={false}
|
||||
top={
|
||||
<FadeIn className='flex flex-col items-center'>
|
||||
<div className="text-5xl font-light text-center">AI Preferences</div>
|
||||
|
||||
<div className="mt-[10%] text-base text-void-fg-2 mb-8 text-center">What are you looking for in an AI model?</div>
|
||||
|
||||
<div className="flex justify-center w-full md:flex-nowrap md:max-w-[80%] max-w-[90%] gap-4">
|
||||
<div
|
||||
onClick={() => { setWantToUseOption('smart'); setPageIndex(pageIndex + 1); }}
|
||||
className="w-full max-w-[250px] h-full relative p-6 aspect-[8/7] border border-void-border-1 rounded-md group flex flex-col items-center justify-center cursor-pointer"
|
||||
>
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-[#0e70c0]/15 via-[#0e70c0]/5 to-transparent dark:from-[#0e70c0]/20 dark:via-[#0e70c0]/10 dark:to-[#0e70c0]/5 transition-opacity duration-300 ease-in-out opacity-100"></div>
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-[#0e70c0]/25 via-[#0e70c0]/10 to-[#0e70c0]/5 dark:from-[#0e70c0]/30 dark:via-[#0e70c0]/15 dark:to-[#0e70c0]/5 transition-opacity duration-300 ease-in-out opacity-0 group-hover:opacity-100"></div>
|
||||
<span className="text-5xl mb-4 relative z-10">🧠</span>
|
||||
<h3 className="text-xl font-medium mb-3 relative z-10">Intelligence</h3>
|
||||
<p className="text-center text-root text-void-fg-2 relative z-10">{basicDescOfWantToUseOption['smart']}</p>
|
||||
</div>
|
||||
|
||||
<div
|
||||
onClick={() => { setWantToUseOption('private'); setPageIndex(pageIndex + 1); }}
|
||||
className="w-full max-w-[250px] h-full relative p-6 aspect-[8/7] border border-void-border-1 rounded-md group flex flex-col items-center justify-center cursor-pointer"
|
||||
>
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-[#0e70c0]/15 via-[#0e70c0]/5 to-transparent dark:from-[#0e70c0]/20 dark:via-[#0e70c0]/10 dark:to-[#0e70c0]/5 transition-opacity duration-300 ease-in-out opacity-100"></div>
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-[#0e70c0]/25 via-[#0e70c0]/10 to-[#0e70c0]/5 dark:from-[#0e70c0]/30 dark:via-[#0e70c0]/15 dark:to-[#0e70c0]/5 transition-opacity duration-300 ease-in-out opacity-0 group-hover:opacity-100"></div>
|
||||
<span className="text-5xl mb-4 relative z-10">🔒</span>
|
||||
<h3 className="text-xl font-medium mb-3 relative z-10">Privacy</h3>
|
||||
<p className="text-center text-root text-void-fg-2 relative z-10">{basicDescOfWantToUseOption['private']}</p>
|
||||
</div>
|
||||
|
||||
<div
|
||||
onClick={() => { setWantToUseOption('cheap'); setPageIndex(pageIndex + 1); }}
|
||||
className="w-full max-w-[250px] h-full relative p-6 aspect-[8/7] border border-void-border-1 rounded-md group flex flex-col items-center justify-center cursor-pointer"
|
||||
>
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-[#0e70c0]/15 via-[#0e70c0]/5 to-transparent dark:from-[#0e70c0]/20 dark:via-[#0e70c0]/10 dark:to-[#0e70c0]/5 transition-opacity duration-300 ease-in-out opacity-100"></div>
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-[#0e70c0]/25 via-[#0e70c0]/10 to-[#0e70c0]/5 dark:from-[#0e70c0]/30 dark:via-[#0e70c0]/15 dark:to-[#0e70c0]/5 transition-opacity duration-300 ease-in-out opacity-0 group-hover:opacity-100"></div>
|
||||
<span className="text-5xl mb-4 relative z-10">💵</span>
|
||||
<h3 className="text-xl font-medium mb-3 relative z-10">Affordability</h3>
|
||||
<p className="text-center text-root text-void-fg-2 relative z-10">{basicDescOfWantToUseOption['cheap']}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</FadeIn>
|
||||
}
|
||||
content={<></>}
|
||||
/>,
|
||||
2: <OnboardingPageShell
|
||||
top={
|
||||
<div className='flex flex-col items-center'>
|
||||
{/* Title */}
|
||||
<div className="text-5xl font-light text-center">Choose a Provider</div>
|
||||
|
||||
{/* Preference Selector */}
|
||||
<div className="mt-6 mb-6 mx-auto flex items-center overflow-hidden bg-zinc-700/5 dark:bg-zinc-300/5 rounded-md">
|
||||
<button
|
||||
onClick={() => {
|
||||
setWantToUseOption('smart');
|
||||
}}
|
||||
className={`py-1 px-2 text-xs cursor-pointer whitespace-nowrap rounded-sm transition-colors
|
||||
${wantToUseOption === 'smart'
|
||||
? 'bg-zinc-700/10 dark:bg-zinc-300/10 text-white font-medium'
|
||||
: 'text-void-fg-3 hover:text-void-fg-2'
|
||||
}
|
||||
`}
|
||||
>
|
||||
Intelligent
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
setWantToUseOption('private');
|
||||
}}
|
||||
className={`py-1 px-2 text-xs cursor-pointer whitespace-nowrap rounded-sm transition-colors
|
||||
${wantToUseOption === 'private'
|
||||
? 'bg-zinc-700/10 dark:bg-zinc-300/10 text-white font-medium'
|
||||
: 'text-void-fg-3 hover:text-void-fg-2'
|
||||
}
|
||||
`}
|
||||
>
|
||||
Private
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
setWantToUseOption('cheap');
|
||||
}}
|
||||
className={`py-1 px-2 text-xs cursor-pointer whitespace-nowrap rounded-sm transition-colors
|
||||
${wantToUseOption === 'cheap'
|
||||
? 'bg-zinc-700/10 dark:bg-zinc-300/10 text-white font-medium'
|
||||
: 'text-void-fg-3 hover:text-void-fg-2'
|
||||
}
|
||||
`}
|
||||
>
|
||||
Low-Cost
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
setWantToUseOption('all')
|
||||
}}
|
||||
className={`py-1 px-2 text-xs cursor-pointer whitespace-nowrap rounded-sm transition-colors
|
||||
${wantToUseOption === 'all'
|
||||
? 'bg-zinc-700/10 dark:bg-zinc-300/10 text-white font-medium'
|
||||
: 'text-void-fg-3 hover:text-void-fg-2'
|
||||
}
|
||||
`}
|
||||
>
|
||||
All
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
||||
{/* Provider Buttons */}
|
||||
<div
|
||||
key={wantToUseOption}
|
||||
className="mb-2 flex flex-wrap items-center w-full"
|
||||
>
|
||||
|
||||
{(wantToUseOption === 'all' ? providerNames : providerNamesOfWantToUseOption[wantToUseOption]).map((providerName) => {
|
||||
const isSelected = selectedProviderName === providerName
|
||||
|
||||
return (
|
||||
<button
|
||||
key={providerName}
|
||||
onClick={() => setSelectedProviderName(providerName)}
|
||||
className={`py-[2px] px-2 mx-0.5 my-0.5 text-xs font-medium cursor-pointer relative rounded-full transition-colors duration-150 border
|
||||
${isSelected ? 'bg-[#0e70c0] text-white shadow-sm border-[#0e70c0]/80' : 'bg-[#0e70c0]/10 text-void-fg-3 hover:bg-[#0e70c0]/30 border-[#0e70c0]/20'}
|
||||
`}
|
||||
>
|
||||
{displayInfoOfProviderName(providerName).title}
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
|
||||
</div>
|
||||
|
||||
{/* Description */}
|
||||
<div className="text-left self-start text-sm text-void-fg-3 px-2 py-1">
|
||||
<ChatMarkdownRender string={detailedDescOfWantToUseOption[wantToUseOption]} chatMessageLocation={undefined} />
|
||||
</div>
|
||||
|
||||
|
||||
{/* ModelsTable and ProviderFields */}
|
||||
{selectedProviderName && <div className='mt-4'>
|
||||
|
||||
|
||||
{/* Models Table */}
|
||||
<TableOfModelsForProvider providerName={selectedProviderName} />
|
||||
|
||||
|
||||
{/* Add provider section - simplified styling */}
|
||||
<div className='mb-5 mt-8'>
|
||||
<div className=''>
|
||||
Add {displayInfoOfProviderName(selectedProviderName).title}
|
||||
|
||||
|
||||
{selectedProviderName === 'ollama' ? ollamaSetupInstructions : ''}
|
||||
|
||||
</div>
|
||||
|
||||
{selectedProviderName &&
|
||||
<SettingsForProvider providerName={selectedProviderName} showProviderTitle={false} showProviderSuggestions={false} />
|
||||
}
|
||||
|
||||
{/* Button and status indicators */}
|
||||
{!didFillInProviderSettings ? <p className="text-xs text-void-fg-3 mt-2">Please fill in all fields to continue</p>
|
||||
: !isAtLeastOneModel ? <p className="text-xs text-void-fg-3 mt-2">Please add a model to continue</p>
|
||||
: !isApiKeyLongEnoughIfApiKeyExists ? <p className="text-xs text-void-fg-3 mt-2">Please enter a valid API key</p>
|
||||
: <div className="mt-2"><AnimatedCheckmarkButton text='Added' /></div>}
|
||||
</div>
|
||||
|
||||
</div>}
|
||||
|
||||
</div>
|
||||
|
||||
}
|
||||
|
||||
bottom={
|
||||
prevAndNextButtons
|
||||
}
|
||||
|
||||
/>,
|
||||
|
||||
// 2.5: <div className="max-w-[600px] w-full h-full text-left mx-auto flex flex-col items-center justify-between">
|
||||
// <FadeIn>
|
||||
// <div className="text-5xl font-light mb-6 mt-12 text-center">Autocomplete</div>
|
||||
|
||||
// <div className="text-center flex flex-col gap-4 w-full max-w-md mx-auto">
|
||||
// <h4 className="text-void-fg-3 mb-2">Void offers free autocomplete with locally hosted models</h4>
|
||||
// <h4 className="text-void-fg-3 mb-2">[have buttons for Ollama install Qwen2.5coder3b and memory requirements] </h4>
|
||||
|
||||
// </div>
|
||||
// </FadeIn>
|
||||
|
||||
// {prevAndNextButtons}
|
||||
// </div>,
|
||||
3: <OnboardingPageShell
|
||||
top={
|
||||
<div>
|
||||
<div className="text-5xl font-light text-center">Settings and Themes</div>
|
||||
|
||||
<div className="mt-8 text-center flex flex-col items-center gap-4 w-full max-w-md mx-auto">
|
||||
<h4 className="text-void-fg-3 mb-4">Transfer your settings from an existing editor?</h4>
|
||||
<OneClickSwitchButton className='w-full px-4 py-2' fromEditor="VS Code" />
|
||||
<OneClickSwitchButton className='w-full px-4 py-2' fromEditor="Cursor" />
|
||||
<OneClickSwitchButton className='w-full px-4 py-2' fromEditor="Windsurf" />
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
bottom={prevAndNextButtons}
|
||||
/>,
|
||||
4: <OnboardingPageShell
|
||||
top={
|
||||
<div className="text-5xl font-light text-center">Jump in</div>
|
||||
}
|
||||
content={
|
||||
<div
|
||||
className="text-center"
|
||||
onClick={() => {
|
||||
// TODO make a fadeout effect
|
||||
voidSettingsService.setGlobalSetting('isOnboardingComplete', true)
|
||||
}}
|
||||
|
||||
>
|
||||
Enter the Void
|
||||
</div>
|
||||
}
|
||||
bottom={
|
||||
<PreviousButton
|
||||
onClick={() => { setPageIndex(pageIndex - 1) }}
|
||||
/>
|
||||
}
|
||||
/>,
|
||||
}
|
||||
|
||||
|
||||
return <div key={pageIndex} className="w-full h-full text-left mx-auto overflow-y-auto flex flex-col items-center justify-around">
|
||||
{contentOfIdx[pageIndex]}
|
||||
</div>
|
||||
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
/*--------------------------------------------------------------------------------------
|
||||
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import { mountFnGenerator } from '../util/mountFnGenerator.js'
|
||||
import { VoidOnboarding } from './VoidOnboarding.js'
|
||||
|
||||
export const mountVoidOnboarding = mountFnGenerator(VoidOnboarding)
|
||||
@@ -3,26 +3,24 @@
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { InputBox } from '../../../../../../../base/browser/ui/inputbox/inputBox.js'
|
||||
import { ProviderName, SettingName, displayInfoOfSettingName, providerNames, VoidModelInfo, globalSettingNames, customSettingNamesOfProvider, RefreshableProviderName, refreshableProviderNames, displayInfoOfProviderName, defaultProviderSettings, nonlocalProviderNames, localProviderNames, GlobalSettingName, featureNames, displayInfoOfFeatureName, isProviderNameDisabled, FeatureName } from '../../../../common/voidSettingsTypes.js'
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { ProviderName, SettingName, displayInfoOfSettingName, providerNames, VoidStatefulModelInfo, customSettingNamesOfProvider, RefreshableProviderName, refreshableProviderNames, displayInfoOfProviderName, nonlocalProviderNames, localProviderNames, GlobalSettingName, featureNames, displayInfoOfFeatureName, isProviderNameDisabled, FeatureName, hasDownloadButtonsOnModelsProviderNames } from '../../../../common/voidSettingsTypes.js'
|
||||
import ErrorBoundary from '../sidebar-tsx/ErrorBoundary.js'
|
||||
import { VoidButton, VoidCheckBox, VoidCustomDropdownBox, VoidInputBox, VoidInputBox2, VoidSwitch } from '../util/inputs.js'
|
||||
import { VoidButtonBgDarken, VoidCustomDropdownBox, VoidInputBox2, VoidSimpleInputBox, VoidSwitch } from '../util/inputs.js'
|
||||
import { useAccessor, useIsDark, useRefreshModelListener, useRefreshModelState, useSettingsState } from '../util/services.js'
|
||||
import { X, RefreshCw, Loader2, Check, MoveRight } from 'lucide-react'
|
||||
import { useScrollbarStyles } from '../util/useScrollbarStyles.js'
|
||||
import { isWindows, isLinux, isMacintosh } from '../../../../../../../base/common/platform.js'
|
||||
import { X, RefreshCw, Loader2, Check, } from 'lucide-react'
|
||||
import { URI } from '../../../../../../../base/common/uri.js'
|
||||
import { env } from '../../../../../../../base/common/process.js'
|
||||
import { ModelDropdown } from './ModelDropdown.js'
|
||||
import { ChatMarkdownRender } from '../markdown/ChatMarkdownRender.js'
|
||||
import { WarningBox } from './WarningBox.js'
|
||||
import { os } from '../../../../common/helpers/systemInfo.js'
|
||||
import { IconX } from '../sidebar-tsx/SidebarChat.js'
|
||||
import { IconLoading } from '../sidebar-tsx/SidebarChat.js'
|
||||
|
||||
|
||||
const ButtonLeftTextRightOption = ({ text, leftButton }: { text: string, leftButton?: React.ReactNode }) => {
|
||||
|
||||
return <div className='flex items-center text-void-fg-3 px-3 py-0.5 rounded-sm overflow-hidden gap-2 hover:bg-black/10 dark:hover:bg-gray-300/10'>
|
||||
return <div className='flex items-center text-void-fg-3 px-3 py-0.5 rounded-sm overflow-hidden gap-2'>
|
||||
{leftButton ? leftButton : null}
|
||||
<span>
|
||||
{text}
|
||||
@@ -98,58 +96,136 @@ const RefreshableModels = () => {
|
||||
|
||||
|
||||
|
||||
const AddModelMenu = ({ onSubmit, onClose }: { onSubmit: () => void, onClose: () => void }) => {
|
||||
export const AnimatedCheckmarkButton = ({ text, className }: { text?: string, className?: string }) => {
|
||||
const [dashOffset, setDashOffset] = useState(40);
|
||||
|
||||
useEffect(() => {
|
||||
const startTime = performance.now();
|
||||
const duration = 500; // 500ms animation
|
||||
|
||||
const animate = (currentTime: number) => {
|
||||
const elapsed = currentTime - startTime;
|
||||
const progress = Math.min(elapsed / duration, 1);
|
||||
const newOffset = 40 - (progress * 40);
|
||||
|
||||
setDashOffset(newOffset);
|
||||
|
||||
if (progress < 1) {
|
||||
requestAnimationFrame(animate);
|
||||
}
|
||||
};
|
||||
|
||||
const animationId = requestAnimationFrame(animate);
|
||||
return () => cancelAnimationFrame(animationId);
|
||||
}, []);
|
||||
|
||||
return <div
|
||||
className={`flex items-center gap-1.5 w-fit
|
||||
${className ? className : `px-2 py-0.5 text-xs text-white bg-[#0e70c0] rounded-sm`}
|
||||
`}
|
||||
>
|
||||
<svg className="size-4" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M5 13l4 4L19 7"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
style={{
|
||||
strokeDasharray: 40,
|
||||
strokeDashoffset: dashOffset
|
||||
}}
|
||||
/>
|
||||
</svg>
|
||||
{text}
|
||||
</div>
|
||||
}
|
||||
|
||||
|
||||
const AddButton = ({ disabled, text = 'Add', ...props }: { disabled?: boolean, text?: React.ReactNode } & React.ButtonHTMLAttributes<HTMLButtonElement>) => {
|
||||
|
||||
return <button
|
||||
disabled={disabled}
|
||||
className={`bg-[#0e70c0] px-3 py-1 text-white rounded-sm ${!disabled ? 'hover:bg-[#1177cb] cursor-pointer' : 'opacity-50 cursor-not-allowed bg-opacity-70'}`}
|
||||
{...props}
|
||||
>{text}</button>
|
||||
|
||||
}
|
||||
|
||||
|
||||
// shows a providerName dropdown if no `providerName` is given
|
||||
export const AddModelInputBox = ({ providerName: permanentProviderName, className, compact }: { providerName?: ProviderName, className?: string, compact?: boolean }) => {
|
||||
|
||||
const accessor = useAccessor()
|
||||
const settingsStateService = accessor.get('IVoidSettingsService')
|
||||
|
||||
const settingsState = useSettingsState()
|
||||
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
const [showCheckmark, setShowCheckmark] = useState(false)
|
||||
|
||||
// const providerNameRef = useRef<ProviderName | null>(null)
|
||||
const [providerName, setProviderName] = useState<ProviderName | null>(null)
|
||||
const [userChosenProviderName, setUserChosenProviderName] = useState<ProviderName>('anthropic')
|
||||
|
||||
const modelNameRef = useRef<HTMLTextAreaElement | null>(null)
|
||||
const providerName = permanentProviderName ?? userChosenProviderName;
|
||||
|
||||
const [modelName, setModelName] = useState<string>('')
|
||||
const [errorString, setErrorString] = useState('')
|
||||
|
||||
const numModels = settingsState.settingsOfProvider[providerName].models.length
|
||||
|
||||
if (showCheckmark) {
|
||||
return <AnimatedCheckmarkButton text='Added' className={`bg-[#0e70c0] text-white px-3 py-1 rounded-sm ${className}`} />
|
||||
}
|
||||
|
||||
if (!isOpen) {
|
||||
return <div
|
||||
className={`text-void-fg-4 flex flex-nowrap text-nowrap items-center hover:brightness-110 cursor-pointer ${className}`}
|
||||
onClick={() => setIsOpen(true)}
|
||||
|
||||
>
|
||||
<div>
|
||||
{numModels > 0 ? `Add a different model?` : `Add a model`}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
|
||||
return <>
|
||||
<div className='flex items-center gap-4'>
|
||||
<form className={`flex items-center gap-2 ${className}`}>
|
||||
|
||||
{/* provider */}
|
||||
<VoidCustomDropdownBox
|
||||
options={providerNames}
|
||||
selectedOption={providerName}
|
||||
onChangeOption={(pn) => setProviderName(pn)}
|
||||
getOptionDisplayName={(pn) => pn ? displayInfoOfProviderName(pn).title : 'Provider Name'}
|
||||
getOptionDropdownName={(pn) => pn ? displayInfoOfProviderName(pn).title : 'Provider Name'}
|
||||
getOptionsEqual={(a, b) => a === b}
|
||||
className={`max-w-44 w-full border border-void-border-2 bg-void-bg-1 text-void-fg-3 text-root
|
||||
py-[4px] px-[6px]
|
||||
`}
|
||||
arrowTouchesText={false}
|
||||
/>
|
||||
{/* <_VoidSelectBox
|
||||
onCreateInstance={useCallback(() => { providerNameRef.current = providerOptions[0].value }, [providerOptions])} // initialize state
|
||||
onChangeSelection={useCallback((providerName: ProviderName) => { providerNameRef.current = providerName }, [])}
|
||||
options={providerOptions}
|
||||
/> */}
|
||||
{/* X button
|
||||
<button onClick={() => { setIsOpen(false) }} className='text-void-fg-4'><X className='size-4' /></button> */}
|
||||
|
||||
{/* model */}
|
||||
<div className='max-w-44 w-full border border-void-border-2 bg-void-bg-1 text-void-fg-3 text-root'>
|
||||
<VoidInputBox2
|
||||
placeholder='Model Name'
|
||||
className='mt-[2px] px-[6px] h-full w-full'
|
||||
ref={modelNameRef}
|
||||
multiline={false}
|
||||
{/* provider input */}
|
||||
{!permanentProviderName &&
|
||||
<VoidCustomDropdownBox
|
||||
options={providerNames}
|
||||
selectedOption={providerName}
|
||||
onChangeOption={(pn) => setUserChosenProviderName(pn)}
|
||||
getOptionDisplayName={(pn) => pn ? displayInfoOfProviderName(pn).title : 'Provider Name'}
|
||||
getOptionDropdownName={(pn) => pn ? displayInfoOfProviderName(pn).title : 'Provider Name'}
|
||||
getOptionsEqual={(a, b) => a === b}
|
||||
// className={`max-w-44 w-full border border-void-border-2 bg-void-bg-1 text-void-fg-3 text-root py-[4px] px-[6px]`}
|
||||
className={`max-w-32 mx-2 w-full resize-none bg-void-bg-1 text-void-fg-1 placeholder:text-void-fg-3 border border-void-border-2 focus:border-void-border-1 py-1 px-2 rounded`}
|
||||
arrowTouchesText={false}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
|
||||
{/* button */}
|
||||
<VoidButton
|
||||
onClick={() => {
|
||||
const modelName = modelNameRef.current?.value
|
||||
{/* model input */}
|
||||
<VoidSimpleInputBox
|
||||
value={modelName}
|
||||
onChangeValue={setModelName}
|
||||
placeholder='Model Name'
|
||||
compact={compact}
|
||||
className={'max-w-32'}
|
||||
/>
|
||||
|
||||
{/* add button */}
|
||||
<AddButton
|
||||
type='submit'
|
||||
disabled={!modelName}
|
||||
onClick={(e) => {
|
||||
if (providerName === null) {
|
||||
setErrorString('Please select a provider.')
|
||||
return
|
||||
@@ -160,17 +236,24 @@ const AddModelMenu = ({ onSubmit, onClose }: { onSubmit: () => void, onClose: ()
|
||||
}
|
||||
// if model already exists here
|
||||
if (settingsState.settingsOfProvider[providerName].models.find(m => m.modelName === modelName)) {
|
||||
setErrorString(`This model already exists under ${providerName}.`)
|
||||
// setErrorString(`This model already exists under ${providerName}.`)
|
||||
setErrorString(`This model already exists.`)
|
||||
return
|
||||
}
|
||||
|
||||
settingsStateService.addModel(providerName, modelName)
|
||||
onSubmit()
|
||||
setShowCheckmark(true)
|
||||
setTimeout(() => {
|
||||
setShowCheckmark(false)
|
||||
setIsOpen(false)
|
||||
}, 1500)
|
||||
setErrorString('')
|
||||
setModelName('')
|
||||
}}
|
||||
>Add model</VoidButton>
|
||||
/>
|
||||
|
||||
<button onClick={onClose} className='ml-auto'><X className='size-4' /></button>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
|
||||
{!errorString ? null : <div className='text-red-500 truncate whitespace-nowrap mt-1'>
|
||||
{errorString}
|
||||
@@ -180,17 +263,6 @@ const AddModelMenu = ({ onSubmit, onClose }: { onSubmit: () => void, onClose: ()
|
||||
|
||||
}
|
||||
|
||||
const AddModelMenuFull = () => {
|
||||
const [open, setOpen] = useState(false)
|
||||
|
||||
return <div className='hover:bg-black/10 dark:hover:bg-gray-300/10 py-1 my-4 px-3 rounded-sm overflow-hidden'>
|
||||
{open ?
|
||||
<AddModelMenu onSubmit={() => setOpen(false)} onClose={() => setOpen(false)} />
|
||||
: <VoidButton onClick={() => setOpen(true)}>Add Model</VoidButton>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
|
||||
export const ModelDump = () => {
|
||||
|
||||
@@ -200,7 +272,7 @@ export const ModelDump = () => {
|
||||
const settingsState = useSettingsState()
|
||||
|
||||
// a dump of all the enabled providers' models
|
||||
const modelDump: (VoidModelInfo & { providerName: ProviderName, providerEnabled: boolean })[] = []
|
||||
const modelDump: (VoidStatefulModelInfo & { providerName: ProviderName, providerEnabled: boolean })[] = []
|
||||
for (let providerName of providerNames) {
|
||||
const providerSettings = settingsState.settingsOfProvider[providerName]
|
||||
// if (!providerSettings.enabled) continue
|
||||
@@ -218,7 +290,16 @@ export const ModelDump = () => {
|
||||
|
||||
const isNewProviderName = (i > 0 ? modelDump[i - 1] : undefined)?.providerName !== providerName
|
||||
|
||||
const providerTitle = displayInfoOfProviderName(providerName).title
|
||||
|
||||
const disabled = !providerEnabled
|
||||
const value = disabled ? false : !isHidden
|
||||
|
||||
const tooltipName = (
|
||||
disabled ? `Add ${providerTitle} to enable`
|
||||
: value === true ? 'Enabled'
|
||||
: 'Disabled'
|
||||
)
|
||||
|
||||
return <div key={`${modelName}${providerName}`}
|
||||
className={`flex items-center justify-between gap-4 hover:bg-black/10 dark:hover:bg-gray-300/10 py-1 px-3 rounded-sm overflow-hidden cursor-default truncate
|
||||
@@ -226,18 +307,28 @@ export const ModelDump = () => {
|
||||
>
|
||||
{/* left part is width:full */}
|
||||
<div className={`flex-grow flex items-center gap-4`}>
|
||||
<span className='w-full max-w-32'>{isNewProviderName ? displayInfoOfProviderName(providerName).title : ''}</span>
|
||||
<span className='w-full max-w-32'>{isNewProviderName ? providerTitle : ''}</span>
|
||||
<span className='w-fit truncate'>{modelName}</span>
|
||||
</div>
|
||||
{/* right part is anything that fits */}
|
||||
<div className='flex items-center gap-4'>
|
||||
<div className='flex items-center gap-4'
|
||||
// data-tooltip-id='void-tooltip'
|
||||
// data-tooltip-place='top'
|
||||
// data-tooltip-content={disabled ? `${displayInfoOfProviderName(providerName).title} is disabled`
|
||||
// : (isHidden ? `'${modelName}' won't appear in dropdowns` : ``)
|
||||
// }
|
||||
>
|
||||
<span className='opacity-50 truncate'>{isAutodetected ? '(detected locally)' : isDefault ? '' : '(custom model)'}</span>
|
||||
|
||||
<VoidSwitch
|
||||
value={disabled ? false : !isHidden}
|
||||
value={value}
|
||||
onChange={() => { settingsStateService.toggleModelHidden(providerName, modelName) }}
|
||||
disabled={disabled}
|
||||
size='sm'
|
||||
|
||||
data-tooltip-id='void-tooltip'
|
||||
data-tooltip-place='right'
|
||||
data-tooltip-content={tooltipName}
|
||||
/>
|
||||
|
||||
<div className={`w-5 flex items-center justify-center`}>
|
||||
@@ -255,45 +346,29 @@ export const ModelDump = () => {
|
||||
|
||||
const ProviderSetting = ({ providerName, settingName }: { providerName: ProviderName, settingName: SettingName }) => {
|
||||
|
||||
|
||||
// const { title: providerTitle, } = displayInfoOfProviderName(providerName)
|
||||
|
||||
const { title: settingTitle, placeholder, isPasswordField, subTextMd } = displayInfoOfSettingName(providerName, settingName)
|
||||
|
||||
const accessor = useAccessor()
|
||||
const voidSettingsService = accessor.get('IVoidSettingsService')
|
||||
const settingsState = useSettingsState()
|
||||
|
||||
let weChangedTextRef = false
|
||||
const settingValue = settingsState.settingsOfProvider[providerName][settingName] as string // this should always be a string in this component
|
||||
if (typeof settingValue !== 'string') {
|
||||
console.log('Error: Provider setting had a non-string value.')
|
||||
return
|
||||
}
|
||||
|
||||
return <ErrorBoundary>
|
||||
<div className='my-1'>
|
||||
<VoidInputBox
|
||||
// placeholder={`${providerTitle} ${settingTitle} (${placeholder})`}
|
||||
placeholder={`${settingTitle} (${placeholder})`}
|
||||
|
||||
onChangeText={useCallback((newVal) => {
|
||||
if (weChangedTextRef) return
|
||||
<VoidSimpleInputBox
|
||||
value={settingValue}
|
||||
onChangeValue={useCallback((newVal) => {
|
||||
voidSettingsService.setSettingOfProvider(providerName, settingName, newVal)
|
||||
}, [voidSettingsService, providerName, settingName])}
|
||||
|
||||
// we are responsible for setting the initial value. always sync the instance whenever there's a change to state.
|
||||
onCreateInstance={useCallback((instance: InputBox) => {
|
||||
const syncInstance = () => {
|
||||
const settingsAtProvider = voidSettingsService.state.settingsOfProvider[providerName];
|
||||
const stateVal = settingsAtProvider[settingName as SettingName]
|
||||
|
||||
// console.log('SYNCING TO', providerName, settingName, stateVal)
|
||||
weChangedTextRef = true
|
||||
instance.value = stateVal as string
|
||||
weChangedTextRef = false
|
||||
}
|
||||
|
||||
syncInstance()
|
||||
const disposable = voidSettingsService.onDidChangeState(syncInstance)
|
||||
return [disposable]
|
||||
}, [voidSettingsService, providerName, settingName])}
|
||||
multiline={false}
|
||||
isPasswordField={isPasswordField}
|
||||
// placeholder={`${providerTitle} ${settingTitle} (${placeholder})`}
|
||||
placeholder={`${settingTitle} (${placeholder})`}
|
||||
passwordBlur={isPasswordField}
|
||||
compact={true}
|
||||
/>
|
||||
{subTextMd === undefined ? null : <div className='py-1 px-3 opacity-50 text-sm'>
|
||||
<ChatMarkdownRender string={subTextMd} chatMessageLocation={undefined} />
|
||||
@@ -303,7 +378,52 @@ const ProviderSetting = ({ providerName, settingName }: { providerName: Provider
|
||||
</ErrorBoundary>
|
||||
}
|
||||
|
||||
const SettingsForProvider = ({ providerName }: { providerName: ProviderName }) => {
|
||||
// const OldSettingsForProvider = ({ providerName, showProviderTitle }: { providerName: ProviderName, showProviderTitle: boolean }) => {
|
||||
// const voidSettingsState = useSettingsState()
|
||||
|
||||
// const needsModel = isProviderNameDisabled(providerName, voidSettingsState) === 'addModel'
|
||||
|
||||
// // const accessor = useAccessor()
|
||||
// // const voidSettingsService = accessor.get('IVoidSettingsService')
|
||||
|
||||
// // const { enabled } = voidSettingsState.settingsOfProvider[providerName]
|
||||
// const settingNames = customSettingNamesOfProvider(providerName)
|
||||
|
||||
// const { title: providerTitle } = displayInfoOfProviderName(providerName)
|
||||
|
||||
// return <div className='my-4'>
|
||||
|
||||
// <div className='flex items-center w-full gap-4'>
|
||||
// {showProviderTitle && <h3 className='text-xl truncate'>{providerTitle}</h3>}
|
||||
|
||||
// {/* enable provider switch */}
|
||||
// {/* <VoidSwitch
|
||||
// value={!!enabled}
|
||||
// onChange={
|
||||
// useCallback(() => {
|
||||
// const enabledRef = voidSettingsService.state.settingsOfProvider[providerName].enabled
|
||||
// voidSettingsService.setSettingOfProvider(providerName, 'enabled', !enabledRef)
|
||||
// }, [voidSettingsService, providerName])}
|
||||
// size='sm+'
|
||||
// /> */}
|
||||
// </div>
|
||||
|
||||
// <div className='px-0'>
|
||||
// {/* settings besides models (e.g. api key) */}
|
||||
// {settingNames.map((settingName, i) => {
|
||||
// return <ProviderSetting key={settingName} providerName={providerName} settingName={settingName} />
|
||||
// })}
|
||||
|
||||
// {needsModel ?
|
||||
// providerName === 'ollama' ?
|
||||
// <WarningBox text={`Please install an Ollama model. We'll auto-detect it.`} />
|
||||
// : <WarningBox text={`Please add a model for ${providerTitle} (Models section).`} />
|
||||
// : null}
|
||||
// </div>
|
||||
// </div >
|
||||
// }
|
||||
|
||||
export const SettingsForProvider = ({ providerName, showProviderTitle, showProviderSuggestions }: { providerName: ProviderName, showProviderTitle: boolean, showProviderSuggestions: boolean }) => {
|
||||
const voidSettingsState = useSettingsState()
|
||||
|
||||
const needsModel = isProviderNameDisabled(providerName, voidSettingsState) === 'addModel'
|
||||
@@ -316,10 +436,10 @@ const SettingsForProvider = ({ providerName }: { providerName: ProviderName }) =
|
||||
|
||||
const { title: providerTitle } = displayInfoOfProviderName(providerName)
|
||||
|
||||
return <div className='my-4'>
|
||||
return <div>
|
||||
|
||||
<div className='flex items-center w-full gap-4'>
|
||||
<h3 className='text-xl truncate'>{providerTitle}</h3>
|
||||
{showProviderTitle && <h3 className='text-xl truncate'>{providerTitle}</h3>}
|
||||
|
||||
{/* enable provider switch */}
|
||||
{/* <VoidSwitch
|
||||
@@ -339,7 +459,7 @@ const SettingsForProvider = ({ providerName }: { providerName: ProviderName }) =
|
||||
return <ProviderSetting key={settingName} providerName={providerName} settingName={settingName} />
|
||||
})}
|
||||
|
||||
{needsModel ?
|
||||
{showProviderSuggestions && needsModel ?
|
||||
providerName === 'ollama' ?
|
||||
<WarningBox text={`Please install an Ollama model. We'll auto-detect it.`} />
|
||||
: <WarningBox text={`Please add a model for ${providerTitle} (Models section).`} />
|
||||
@@ -352,7 +472,7 @@ const SettingsForProvider = ({ providerName }: { providerName: ProviderName }) =
|
||||
export const VoidProviderSettings = ({ providerNames }: { providerNames: ProviderName[] }) => {
|
||||
return <>
|
||||
{providerNames.map(providerName =>
|
||||
<SettingsForProvider key={providerName} providerName={providerName} />
|
||||
<SettingsForProvider key={providerName} providerName={providerName} showProviderTitle={true} showProviderSuggestions={true} />
|
||||
)}
|
||||
</>
|
||||
}
|
||||
@@ -425,6 +545,29 @@ const FastApplyMethodDropdown = () => {
|
||||
}
|
||||
|
||||
|
||||
export const ollamaSetupInstructions = <div className='prose-p:my-0 prose-ol:list-decimal prose-p:py-0 prose-ol:my-0 prose-ol:py-0 prose-span:my-0 prose-span:py-0 text-void-fg-3 text-sm list-decimal select-text'>
|
||||
<div className=''><ChatMarkdownRender string={`Ollama Setup Instructions`} chatMessageLocation={undefined} /></div>
|
||||
<div className=' pl-6'><ChatMarkdownRender string={`1. Download [Ollama](https://ollama.com/download).`} chatMessageLocation={undefined} /></div>
|
||||
<div className=' pl-6'><ChatMarkdownRender string={`2. Open your terminal.`} chatMessageLocation={undefined} /></div>
|
||||
<div className=' pl-6'><ChatMarkdownRender string={`3. Run \`ollama pull your_model\` to install a model.`} chatMessageLocation={undefined} /></div>
|
||||
<div className=' pl-6'><ChatMarkdownRender string={`Void automatically detects locally running models and enables them.`} chatMessageLocation={undefined} /></div>
|
||||
</div>
|
||||
|
||||
|
||||
const RedoOnboardingButton = ({ className }: { className?: string }) => {
|
||||
const accessor = useAccessor()
|
||||
const voidSettingsService = accessor.get('IVoidSettingsService')
|
||||
return <div
|
||||
className={`text-void-fg-4 flex flex-nowrap text-nowrap items-center hover:brightness-110 cursor-pointer ${className}`}
|
||||
onClick={() => { voidSettingsService.setGlobalSetting('isOnboardingComplete', false) }}
|
||||
>
|
||||
See onboarding screen?
|
||||
</div>
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
export const FeaturesTab = () => {
|
||||
const voidSettingsState = useSettingsState()
|
||||
const accessor = useAccessor()
|
||||
@@ -435,7 +578,8 @@ export const FeaturesTab = () => {
|
||||
<h2 className={`text-3xl mb-2`}>Models</h2>
|
||||
<ErrorBoundary>
|
||||
<ModelDump />
|
||||
<AddModelMenuFull />
|
||||
<AddModelInputBox className='mt-4' compact />
|
||||
<RedoOnboardingButton className='mt-2 mb-4' />
|
||||
<AutoDetectLocalModelsToggle />
|
||||
<RefreshableModels />
|
||||
</ErrorBoundary>
|
||||
@@ -446,13 +590,9 @@ export const FeaturesTab = () => {
|
||||
{/* <h3 className={`opacity-50 mb-2`}>{`Instructions:`}</h3> */}
|
||||
{/* <h3 className={`mb-2`}>{`Void can access any model that you host locally. We automatically detect your local models by default.`}</h3> */}
|
||||
<h3 className={`text-void-fg-3 mb-2`}>{`Void can access any model that you host locally. We automatically detect your local models by default.`}</h3>
|
||||
<div className='pl-4 prose-ol:list-decimal opacity-80'>
|
||||
<span className={`text-sm mb-2`}><ChatMarkdownRender string={`1. Download [Ollama](https://ollama.com/download).`} chatMessageLocation={undefined} /></span>
|
||||
<span className={`text-sm mb-2`}><ChatMarkdownRender string={`2. Open your terminal.`} chatMessageLocation={undefined} /></span>
|
||||
<span className={`text-sm mb-2 select-text`}><ChatMarkdownRender string={`3. Run \`ollama run llama3.1:8b\`. This installs Meta's llama3.1 model which is best for chat and inline edits. Requires 5GB of memory.`} chatMessageLocation={undefined} /></span>
|
||||
<span className={`text-sm mb-2 select-text`}><ChatMarkdownRender string={`4. Run \`ollama run qwen2.5-coder:1.5b\`. This installs a faster autocomplete model. Requires 1GB of memory.`} chatMessageLocation={undefined} /></span>
|
||||
<span className={`text-sm mb-2`}><ChatMarkdownRender string={`Void automatically detects locally running models and enables them.`} chatMessageLocation={undefined} /></span>
|
||||
{/* TODO we should create UI for downloading models without user going into terminal */}
|
||||
|
||||
<div className='opacity-80 mb-4'>
|
||||
{ollamaSetupInstructions}
|
||||
</div>
|
||||
|
||||
<ErrorBoundary>
|
||||
@@ -475,7 +615,19 @@ export const FeaturesTab = () => {
|
||||
{/* FIM */}
|
||||
<div className='w-full'>
|
||||
<h4 className={`text-base`}>{displayInfoOfFeatureName('Autocomplete')}</h4>
|
||||
<div className='text-sm italic text-void-fg-3 mt-1 mb-4'>Experimental. Only works with models that support FIM.</div>
|
||||
<div className='text-sm italic text-void-fg-3 mt-1 mb-4'>
|
||||
<span>
|
||||
Experimental. Only works with FIM models.
|
||||
</span>
|
||||
<span
|
||||
className='hover:brightness-110'
|
||||
data-tooltip-id='void-tooltip'
|
||||
data-tooltip-content='We recommend using qwen2.5-coder:1.5b with Ollama.'
|
||||
data-tooltip-class-name='void-max-w-[20px]'
|
||||
>
|
||||
*
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className='my-2'>
|
||||
{/* Enable Switch */}
|
||||
@@ -547,12 +699,39 @@ export const FeaturesTab = () => {
|
||||
/>
|
||||
<span className='text-void-fg-3 text-xs pointer-events-none'>{voidSettingsState.globalSettings.autoApprove ? 'Auto-approve' : 'Auto-approve'}</span>
|
||||
</div>
|
||||
|
||||
{/* Tool Lint Errors Switch */}
|
||||
<div className='flex items-center gap-x-2 my-2'>
|
||||
<VoidSwitch
|
||||
size='xs'
|
||||
value={voidSettingsState.globalSettings.includeToolLintErrors}
|
||||
onChange={(newVal) => voidSettingsService.setGlobalSetting('includeToolLintErrors', newVal)}
|
||||
/>
|
||||
<span className='text-void-fg-3 text-xs pointer-events-none'>{voidSettingsState.globalSettings.includeToolLintErrors ? 'Fix lint errors' : `Don't fix lint errors`}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div className='w-full'>
|
||||
<h4 className={`text-base`}>Editor</h4>
|
||||
<div className='text-sm italic text-void-fg-3 mt-1 mb-4'>{`Settings that control the visibility of suggestions and widgets in the code editor.`}</div>
|
||||
|
||||
<div className='my-2'>
|
||||
{/* Auto Accept Switch */}
|
||||
<div className='flex items-center gap-x-2 my-2'>
|
||||
<VoidSwitch
|
||||
size='xs'
|
||||
value={voidSettingsState.globalSettings.showInlineSuggestions}
|
||||
onChange={(newVal) => voidSettingsService.setGlobalSetting('showInlineSuggestions', newVal)}
|
||||
/>
|
||||
<span className='text-void-fg-3 text-xs pointer-events-none'>{voidSettingsState.globalSettings.showInlineSuggestions ? 'Show suggestions on select' : 'Show suggestions on select'}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
@@ -564,41 +743,91 @@ export const FeaturesTab = () => {
|
||||
}
|
||||
|
||||
|
||||
|
||||
type TransferEditorType = 'VS Code' | 'Cursor' | 'Windsurf'
|
||||
// https://github.com/VSCodium/vscodium/blob/master/docs/index.md#migrating-from-visual-studio-code-to-vscodium
|
||||
// https://code.visualstudio.com/docs/editor/extension-marketplace#_where-are-extensions-installed
|
||||
type TransferFilesInfo = { from: URI, to: URI }[]
|
||||
const transferTheseFilesOfOS = (os: 'mac' | 'windows' | 'linux' | null): TransferFilesInfo => {
|
||||
const transferTheseFilesOfOS = (os: 'mac' | 'windows' | 'linux' | null, fromEditor: TransferEditorType = 'VS Code'): TransferFilesInfo => {
|
||||
if (os === null)
|
||||
throw new Error(`One-click switch is not possible in this environment.`)
|
||||
if (os === 'mac') {
|
||||
const homeDir = env['HOME']
|
||||
if (!homeDir) throw new Error(`$HOME not found`)
|
||||
return [{
|
||||
from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, 'Library', 'Application Support', 'Code', 'User', 'settings.json'),
|
||||
to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, 'Library', 'Application Support', 'Void', 'User', 'settings.json'),
|
||||
}, {
|
||||
from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, 'Library', 'Application Support', 'Code', 'User', 'keybindings.json'),
|
||||
to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, 'Library', 'Application Support', 'Void', 'User', 'keybindings.json'),
|
||||
}, {
|
||||
from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.vscode', 'extensions'),
|
||||
to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.void-editor', 'extensions'),
|
||||
}]
|
||||
|
||||
if (fromEditor === 'VS Code') {
|
||||
return [{
|
||||
from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, 'Library', 'Application Support', 'Code', 'User', 'settings.json'),
|
||||
to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, 'Library', 'Application Support', 'Void', 'User', 'settings.json'),
|
||||
}, {
|
||||
from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, 'Library', 'Application Support', 'Code', 'User', 'keybindings.json'),
|
||||
to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, 'Library', 'Application Support', 'Void', 'User', 'keybindings.json'),
|
||||
}, {
|
||||
from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.vscode', 'extensions'),
|
||||
to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.void-editor', 'extensions'),
|
||||
}]
|
||||
} else if (fromEditor === 'Cursor') {
|
||||
return [{
|
||||
from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, 'Library', 'Application Support', 'Cursor', 'User', 'settings.json'),
|
||||
to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, 'Library', 'Application Support', 'Void', 'User', 'settings.json'),
|
||||
}, {
|
||||
from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, 'Library', 'Application Support', 'Cursor', 'User', 'keybindings.json'),
|
||||
to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, 'Library', 'Application Support', 'Void', 'User', 'keybindings.json'),
|
||||
}, {
|
||||
from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.cursor', 'extensions'),
|
||||
to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.void-editor', 'extensions'),
|
||||
}]
|
||||
} else if (fromEditor === 'Windsurf') {
|
||||
return [{
|
||||
from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, 'Library', 'Application Support', 'Windsurf', 'User', 'settings.json'),
|
||||
to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, 'Library', 'Application Support', 'Void', 'User', 'settings.json'),
|
||||
}, {
|
||||
from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, 'Library', 'Application Support', 'Windsurf', 'User', 'keybindings.json'),
|
||||
to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, 'Library', 'Application Support', 'Void', 'User', 'keybindings.json'),
|
||||
}, {
|
||||
from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.windsurf', 'extensions'),
|
||||
to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.void-editor', 'extensions'),
|
||||
}]
|
||||
}
|
||||
}
|
||||
|
||||
if (os === 'linux') {
|
||||
const homeDir = env['HOME']
|
||||
if (!homeDir) throw new Error(`variable for $HOME location not found`)
|
||||
return [{
|
||||
from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.config', 'Code', 'User', 'settings.json'),
|
||||
to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.config', 'Void', 'User', 'settings.json'),
|
||||
}, {
|
||||
from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.config', 'Code', 'User', 'keybindings.json'),
|
||||
to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.config', 'Void', 'User', 'keybindings.json'),
|
||||
}, {
|
||||
from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.vscode', 'extensions'),
|
||||
to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.void-editor', 'extensions'),
|
||||
}]
|
||||
|
||||
if (fromEditor === 'VS Code') {
|
||||
return [{
|
||||
from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.config', 'Code', 'User', 'settings.json'),
|
||||
to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.config', 'Void', 'User', 'settings.json'),
|
||||
}, {
|
||||
from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.config', 'Code', 'User', 'keybindings.json'),
|
||||
to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.config', 'Void', 'User', 'keybindings.json'),
|
||||
}, {
|
||||
from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.vscode', 'extensions'),
|
||||
to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.void-editor', 'extensions'),
|
||||
}]
|
||||
} else if (fromEditor === 'Cursor') {
|
||||
return [{
|
||||
from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.config', 'Cursor', 'User', 'settings.json'),
|
||||
to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.config', 'Void', 'User', 'settings.json'),
|
||||
}, {
|
||||
from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.config', 'Cursor', 'User', 'keybindings.json'),
|
||||
to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.config', 'Void', 'User', 'keybindings.json'),
|
||||
}, {
|
||||
from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.cursor', 'extensions'),
|
||||
to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.void-editor', 'extensions'),
|
||||
}]
|
||||
} else if (fromEditor === 'Windsurf') {
|
||||
return [{
|
||||
from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.config', 'Windsurf', 'User', 'settings.json'),
|
||||
to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.config', 'Void', 'User', 'settings.json'),
|
||||
}, {
|
||||
from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.config', 'Windsurf', 'User', 'keybindings.json'),
|
||||
to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.config', 'Void', 'User', 'keybindings.json'),
|
||||
}, {
|
||||
from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.windsurf', 'extensions'),
|
||||
to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.void-editor', 'extensions'),
|
||||
}]
|
||||
}
|
||||
}
|
||||
|
||||
if (os === 'windows') {
|
||||
@@ -607,73 +836,115 @@ const transferTheseFilesOfOS = (os: 'mac' | 'windows' | 'linux' | null): Transfe
|
||||
const userprofile = env['USERPROFILE']
|
||||
if (!userprofile) throw new Error(`variable for %USERPROFILE% location not found`)
|
||||
|
||||
return [{
|
||||
from: URI.joinPath(URI.from({ scheme: 'file' }), appdata, 'Code', 'User', 'settings.json'),
|
||||
to: URI.joinPath(URI.from({ scheme: 'file' }), appdata, 'Void', 'User', 'settings.json'),
|
||||
}, {
|
||||
from: URI.joinPath(URI.from({ scheme: 'file' }), appdata, 'Code', 'User', 'keybindings.json'),
|
||||
to: URI.joinPath(URI.from({ scheme: 'file' }), appdata, 'Void', 'User', 'keybindings.json'),
|
||||
}, {
|
||||
from: URI.joinPath(URI.from({ scheme: 'file' }), userprofile, '.vscode', 'extensions'),
|
||||
to: URI.joinPath(URI.from({ scheme: 'file' }), userprofile, '.void-editor', 'extensions'),
|
||||
}]
|
||||
if (fromEditor === 'VS Code') {
|
||||
return [{
|
||||
from: URI.joinPath(URI.from({ scheme: 'file' }), appdata, 'Code', 'User', 'settings.json'),
|
||||
to: URI.joinPath(URI.from({ scheme: 'file' }), appdata, 'Void', 'User', 'settings.json'),
|
||||
}, {
|
||||
from: URI.joinPath(URI.from({ scheme: 'file' }), appdata, 'Code', 'User', 'keybindings.json'),
|
||||
to: URI.joinPath(URI.from({ scheme: 'file' }), appdata, 'Void', 'User', 'keybindings.json'),
|
||||
}, {
|
||||
from: URI.joinPath(URI.from({ scheme: 'file' }), userprofile, '.vscode', 'extensions'),
|
||||
to: URI.joinPath(URI.from({ scheme: 'file' }), userprofile, '.void-editor', 'extensions'),
|
||||
}]
|
||||
} else if (fromEditor === 'Cursor') {
|
||||
return [{
|
||||
from: URI.joinPath(URI.from({ scheme: 'file' }), appdata, 'Cursor', 'User', 'settings.json'),
|
||||
to: URI.joinPath(URI.from({ scheme: 'file' }), appdata, 'Void', 'User', 'settings.json'),
|
||||
}, {
|
||||
from: URI.joinPath(URI.from({ scheme: 'file' }), appdata, 'Cursor', 'User', 'keybindings.json'),
|
||||
to: URI.joinPath(URI.from({ scheme: 'file' }), appdata, 'Void', 'User', 'keybindings.json'),
|
||||
}, {
|
||||
from: URI.joinPath(URI.from({ scheme: 'file' }), userprofile, '.cursor', 'extensions'),
|
||||
to: URI.joinPath(URI.from({ scheme: 'file' }), userprofile, '.void-editor', 'extensions'),
|
||||
}]
|
||||
} else if (fromEditor === 'Windsurf') {
|
||||
return [{
|
||||
from: URI.joinPath(URI.from({ scheme: 'file' }), appdata, 'Windsurf', 'User', 'settings.json'),
|
||||
to: URI.joinPath(URI.from({ scheme: 'file' }), appdata, 'Void', 'User', 'settings.json'),
|
||||
}, {
|
||||
from: URI.joinPath(URI.from({ scheme: 'file' }), appdata, 'Windsurf', 'User', 'keybindings.json'),
|
||||
to: URI.joinPath(URI.from({ scheme: 'file' }), appdata, 'Void', 'User', 'keybindings.json'),
|
||||
}, {
|
||||
from: URI.joinPath(URI.from({ scheme: 'file' }), userprofile, '.windsurf', 'extensions'),
|
||||
to: URI.joinPath(URI.from({ scheme: 'file' }), userprofile, '.void-editor', 'extensions'),
|
||||
}]
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(`os '${os}' not recognized`)
|
||||
throw new Error(`os '${os}' not recognized or editor type '${fromEditor}' not supported for this OS`)
|
||||
}
|
||||
|
||||
|
||||
let transferTheseFiles: TransferFilesInfo = []
|
||||
let transferError: string | null = null
|
||||
|
||||
try { transferTheseFiles = transferTheseFilesOfOS(os) }
|
||||
catch (e) { transferError = e + '' }
|
||||
|
||||
const OneClickSwitchButton = () => {
|
||||
export const OneClickSwitchButton = ({ fromEditor = 'VS Code', className = '' }: { fromEditor?: TransferEditorType, className?: string }) => {
|
||||
const accessor = useAccessor()
|
||||
const fileService = accessor.get('IFileService')
|
||||
|
||||
const [state, setState] = useState<{ type: 'done', error?: string } | { type: | 'loading' | 'justfinished' }>({ type: 'done' })
|
||||
const [transferState, setTransferState] = useState<{ type: 'done', error?: string } | { type: | 'loading' | 'justfinished' }>({ type: 'done' })
|
||||
|
||||
let transferTheseFiles: TransferFilesInfo = [];
|
||||
let editorError: string | null = null;
|
||||
|
||||
try {
|
||||
transferTheseFiles = transferTheseFilesOfOS(os, fromEditor)
|
||||
} catch (e) {
|
||||
editorError = e + ''
|
||||
}
|
||||
|
||||
if (transferTheseFiles.length === 0)
|
||||
return <>
|
||||
<WarningBox text={transferError ?? `One-click switch not available.`} />
|
||||
<WarningBox text={editorError ?? `Transfer from ${fromEditor} not available.`} />
|
||||
</>
|
||||
|
||||
|
||||
|
||||
const onClick = async () => {
|
||||
if (transferState.type !== 'done') return
|
||||
|
||||
if (state.type !== 'done') return
|
||||
|
||||
setState({ type: 'loading' })
|
||||
setTransferState({ type: 'loading' })
|
||||
|
||||
let errAcc = ''
|
||||
for (let { from, to } of transferTheseFiles) {
|
||||
console.log('transferring', from, to)
|
||||
// not sure if this can fail, just wrapping it with try/catch for now
|
||||
try { await fileService.copy(from, to, true) }
|
||||
catch (e) { errAcc += e + '\n' }
|
||||
// Check if the source file exists before attempting to copy
|
||||
try {
|
||||
const exists = await fileService.exists(from)
|
||||
if (exists) {
|
||||
// Ensure the destination directory exists
|
||||
const toParent = URI.joinPath(to, '..')
|
||||
const toParentExists = await fileService.exists(toParent)
|
||||
if (!toParentExists) {
|
||||
await fileService.createFolder(toParent)
|
||||
}
|
||||
await fileService.copy(from, to, true)
|
||||
} else {
|
||||
console.log(`Skipping file that doesn't exist: ${from.toString()}`)
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
console.error('Error copying file:', e)
|
||||
errAcc += `Error copying ${from.toString()}: ${e}\n`
|
||||
}
|
||||
}
|
||||
|
||||
// Even if some files were missing, consider it a success if no actual errors occurred
|
||||
const hadError = !!errAcc
|
||||
if (hadError) {
|
||||
setState({ type: 'done', error: errAcc })
|
||||
setTransferState({ type: 'done', error: errAcc })
|
||||
}
|
||||
else {
|
||||
setState({ type: 'justfinished' })
|
||||
setTimeout(() => { setState({ type: 'done' }); }, 3000)
|
||||
setTransferState({ type: 'justfinished' })
|
||||
setTimeout(() => { setTransferState({ type: 'done' }); }, 3000)
|
||||
}
|
||||
}
|
||||
|
||||
return <>
|
||||
<VoidButton disabled={state.type !== 'done'} onClick={onClick}>
|
||||
{state.type === 'done' ? 'Transfer my Settings'
|
||||
: state.type === 'loading' ? 'Transferring...'
|
||||
: state.type === 'justfinished' ? 'Success!'
|
||||
<VoidButtonBgDarken className={`max-w-48 p-4 ${className}`} disabled={transferState.type !== 'done'} onClick={onClick}>
|
||||
{transferState.type === 'done' ? `Transfer from ${fromEditor}`
|
||||
: transferState.type === 'loading' ? <span className='text-nowrap flex flex-nowrap'>Transferring<IconLoading /></span>
|
||||
: transferState.type === 'justfinished' ? <AnimatedCheckmarkButton text='Settings Transferred' className='bg-none' />
|
||||
: null
|
||||
}
|
||||
</VoidButton>
|
||||
{state.type === 'done' && state.error ? <WarningBox text={state.error} /> : null}
|
||||
</VoidButtonBgDarken>
|
||||
{transferState.type === 'done' && transferState.error ? <WarningBox text={transferState.error} /> : null}
|
||||
</>
|
||||
}
|
||||
|
||||
@@ -681,43 +952,53 @@ const OneClickSwitchButton = () => {
|
||||
const GeneralTab = () => {
|
||||
const accessor = useAccessor()
|
||||
const commandService = accessor.get('ICommandService')
|
||||
const environmentService = accessor.get('IEnvironmentService')
|
||||
const nativeHostService = accessor.get('INativeHostService')
|
||||
|
||||
return <>
|
||||
|
||||
|
||||
<div className=''>
|
||||
<h2 className={`text-3xl mb-2`}>One-Click Switch</h2>
|
||||
<h4 className={`text-void-fg-3 mb-2`}>{`Transfer your settings from VS Code to Void in one click.`}</h4>
|
||||
<OneClickSwitchButton />
|
||||
<h4 className={`text-void-fg-3 mb-4`}>{`Transfer your settings from another editor to Void in one click.`}</h4>
|
||||
|
||||
<div className='flex flex-col gap-4'>
|
||||
<OneClickSwitchButton className='w-48' fromEditor="VS Code" />
|
||||
<OneClickSwitchButton className='w-48' fromEditor="Cursor" />
|
||||
<OneClickSwitchButton className='w-48' fromEditor="Windsurf" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div className='mt-12'>
|
||||
<h2 className={`text-3xl mb-2`}>Built-in Settings</h2>
|
||||
<h4 className={`text-void-fg-3 mb-2`}>{`IDE settings, keyboard settings, and theme customization.`}</h4>
|
||||
<h4 className={`text-void-fg-3 mb-4`}>{`IDE settings, keyboard settings, and theme customization.`}</h4>
|
||||
|
||||
<div className='my-4'>
|
||||
<VoidButton onClick={() => { commandService.executeCommand('workbench.action.openSettings') }}>
|
||||
<VoidButtonBgDarken className='px-4 py-2' onClick={() => { commandService.executeCommand('workbench.action.openSettings') }}>
|
||||
General Settings
|
||||
</VoidButton>
|
||||
</VoidButtonBgDarken>
|
||||
</div>
|
||||
<div className='my-4'>
|
||||
<VoidButton onClick={() => { commandService.executeCommand('workbench.action.openGlobalKeybindings') }}>
|
||||
<VoidButtonBgDarken className='px-4 py-2' onClick={() => { commandService.executeCommand('workbench.action.openGlobalKeybindings') }}>
|
||||
Keyboard Settings
|
||||
</VoidButton>
|
||||
</VoidButtonBgDarken>
|
||||
</div>
|
||||
<div className='my-4'>
|
||||
<VoidButton onClick={() => { commandService.executeCommand('workbench.action.selectTheme') }}>
|
||||
<VoidButtonBgDarken className='px-4 py-2' onClick={() => { commandService.executeCommand('workbench.action.selectTheme') }}>
|
||||
Theme Settings
|
||||
</VoidButton>
|
||||
</VoidButtonBgDarken>
|
||||
</div>
|
||||
<div className='my-4'>
|
||||
<VoidButtonBgDarken className='px-4 py-2' onClick={() => { nativeHostService.showItemInFolder(environmentService.logsHome.fsPath) }}>
|
||||
Open Logs
|
||||
</VoidButtonBgDarken>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div className='mt-12'>
|
||||
<div className='mt-12 max-w-[600px]'>
|
||||
<h2 className={`text-3xl mb-2`}>AI Instructions</h2>
|
||||
<h4 className={`text-void-fg-3 mb-2`}>{`Instructions to include on all AI requests.`}</h4>
|
||||
<h4 className={`text-void-fg-3 mb-4`}>{`Instructions to include on all AI requests.`}</h4>
|
||||
<AIInstructionsBox />
|
||||
</div>
|
||||
|
||||
@@ -732,11 +1013,8 @@ export const Settings = () => {
|
||||
|
||||
const [tab, setTab] = useState<TabName>('models')
|
||||
|
||||
const containerRef = useRef<HTMLDivElement | null>(null)
|
||||
useScrollbarStyles(containerRef)
|
||||
|
||||
return <div className={`@@void-scope ${isDark ? 'dark' : ''}`} style={{ height: '100%', width: '100%' }}>
|
||||
<div ref={containerRef} className='overflow-y-auto w-full h-full px-10 py-10 select-none'>
|
||||
<div className='overflow-y-auto w-full h-full px-10 py-10 select-none'>
|
||||
|
||||
<div className='max-w-5xl mx-auto'>
|
||||
|
||||
@@ -780,3 +1058,4 @@ export const Settings = () => {
|
||||
|
||||
</div>
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
/*--------------------------------------------------------------------------------------
|
||||
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Tooltip } from 'react-tooltip';
|
||||
import 'react-tooltip/dist/react-tooltip.css';
|
||||
import { useIsDark } from '../util/services.js';
|
||||
|
||||
/**
|
||||
* Creates a configured global tooltip component with consistent styling
|
||||
* To use:
|
||||
* 1. Mount a Tooltip with some id eg id='void-tooltip'
|
||||
* 2. Add data-tooltip-id="void-tooltip" and data-tooltip-content="Your tooltip text" to any element
|
||||
*/
|
||||
export const VoidTooltip = () => {
|
||||
|
||||
|
||||
const isDark = useIsDark()
|
||||
|
||||
return (
|
||||
|
||||
// use native colors so we don't have to worry about @@void-scope styles
|
||||
// --void-bg-1: var(--vscode-input-background);
|
||||
// --void-bg-1-alt: var(--vscode-badge-background);
|
||||
// --void-bg-2: var(--vscode-sideBar-background);
|
||||
// --void-bg-2-alt: color-mix(in srgb, var(--vscode-sideBar-background) 30%, var(--vscode-editor-background) 70%);
|
||||
// --void-bg-3: var(--vscode-editor-background);
|
||||
|
||||
// --void-fg-0: color-mix(in srgb, var(--vscode-tab-activeForeground) 90%, black 10%);
|
||||
// --void-fg-1: var(--vscode-editor-foreground);
|
||||
// --void-fg-2: var(--vscode-input-foreground);
|
||||
// --void-fg-3: var(--vscode-input-placeholderForeground);
|
||||
// /* --void-fg-4: var(--vscode-tab-inactiveForeground); */
|
||||
// --void-fg-4: var(--vscode-list-deemphasizedForeground);
|
||||
|
||||
// --void-warning: var(--vscode-charts-yellow);
|
||||
|
||||
// --void-border-1: var(--vscode-commandCenter-activeBorder);
|
||||
// --void-border-2: var(--vscode-commandCenter-border);
|
||||
// --void-border-3: var(--vscode-commandCenter-inactiveBorder);
|
||||
// --void-border-4: var(--vscode-editorGroup-border);
|
||||
|
||||
<>
|
||||
<style>
|
||||
{`
|
||||
#void-tooltip, #void-tooltip-orange, #void-tooltip-green {
|
||||
font-size: 12px;
|
||||
padding: 0px 8px;
|
||||
border-radius: 6px;
|
||||
z-index: 999999;
|
||||
}
|
||||
|
||||
#void-tooltip {
|
||||
background-color: var(--vscode-editor-background);
|
||||
color: var(--vscode-input-foreground);
|
||||
}
|
||||
|
||||
#void-tooltip-orange {
|
||||
background-color: #F6762A;
|
||||
color: white;
|
||||
}
|
||||
|
||||
#void-tooltip-green {
|
||||
background-color: #228B22;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.react-tooltip-arrow {
|
||||
z-index: -1 !important; /* Keep arrow behind content (somehow this isnt done automatically) */
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
|
||||
|
||||
<Tooltip
|
||||
id="void-tooltip"
|
||||
// border='1px solid var(--vscode-editorGroup-border)'
|
||||
border='1px solid rgba(100,100,100,.2)'
|
||||
opacity={1}
|
||||
delayShow={50}
|
||||
/>
|
||||
<Tooltip
|
||||
id="void-tooltip-orange"
|
||||
border='1px solid rgba(200,200,200,.3)'
|
||||
opacity={1}
|
||||
delayShow={50}
|
||||
/>
|
||||
<Tooltip
|
||||
id="void-tooltip-green"
|
||||
border='1px solid rgba(200,200,200,.3)'
|
||||
opacity={1}
|
||||
delayShow={50}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,9 @@
|
||||
/*--------------------------------------------------------------------------------------
|
||||
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import { mountFnGenerator } from '../util/mountFnGenerator.js'
|
||||
import { VoidTooltip } from './VoidTooltip.js'
|
||||
|
||||
export const mountVoidTooltip = mountFnGenerator(VoidTooltip)
|
||||
@@ -7,9 +7,11 @@ import { defineConfig } from 'tsup'
|
||||
|
||||
export default defineConfig({
|
||||
entry: [
|
||||
'./src2/void-command-bar-tsx/index.tsx',
|
||||
'./src2/void-editor-widgets-tsx/index.tsx',
|
||||
'./src2/sidebar-tsx/index.tsx',
|
||||
'./src2/void-settings-tsx/index.tsx',
|
||||
'./src2/void-tooltip/index.tsx',
|
||||
'./src2/void-onboarding/index.tsx',
|
||||
'./src2/quick-edit-tsx/index.tsx',
|
||||
'./src2/diff/index.tsx',
|
||||
],
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
/*--------------------------------------------------------------------------------------
|
||||
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Emitter, Event } from '../../../../base/common/event.js';
|
||||
import { Disposable } from '../../../../base/common/lifecycle.js';
|
||||
import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js';
|
||||
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
|
||||
|
||||
|
||||
|
||||
export interface ISearchReplaceService {
|
||||
readonly _serviceBrand: undefined;
|
||||
}
|
||||
|
||||
export const ISearchReplaceService = createDecorator<ISearchReplaceService>('SearchReplaceCacheService');
|
||||
export class SearchReplaceService extends Disposable implements ISearchReplaceService {
|
||||
_serviceBrand: undefined;
|
||||
|
||||
private readonly _onDidChangeState = new Emitter<void>();
|
||||
readonly onDidChangeState: Event<void> = this._onDidChangeState.event;
|
||||
|
||||
constructor(
|
||||
// @ILLMMessageService private readonly llmMessageService: ILLMMessageService,
|
||||
) {
|
||||
super()
|
||||
}
|
||||
|
||||
// send(params: ServiceSendLLMMessageParams & { onText: (p: { newText: string, fullText: string }) => { retry: boolean } }) {
|
||||
// this.llmMessageService.sendLLMMessage({
|
||||
// ...params as ServiceSendLLMMessageParams,
|
||||
// onText: (p) => {
|
||||
// const { retry } = params.onText(p)
|
||||
// if (retry) {
|
||||
|
||||
// }
|
||||
// }
|
||||
// })
|
||||
// }
|
||||
|
||||
}
|
||||
|
||||
registerSingleton(ISearchReplaceService, SearchReplaceService, InstantiationType.Eager);
|
||||
@@ -14,7 +14,6 @@ import { ContextKeyExpr } from '../../../../platform/contextkey/common/contextke
|
||||
|
||||
import { ICodeEditorService } from '../../../../editor/browser/services/codeEditorService.js';
|
||||
import { IRange } from '../../../../editor/common/core/range.js';
|
||||
import { ITextModel } from '../../../../editor/common/model.js';
|
||||
import { VOID_VIEW_ID } from './sidebarPane.js';
|
||||
import { IMetricsService } from '../common/metricsService.js';
|
||||
import { ISidebarStateService } from './sidebarStateService.js';
|
||||
@@ -26,10 +25,37 @@ import { Disposable } from '../../../../base/common/lifecycle.js';
|
||||
import { localize2 } from '../../../../nls.js';
|
||||
import { StagingSelectionItem } from '../common/chatThreadServiceTypes.js';
|
||||
import { IChatThreadService } from './chatThreadService.js';
|
||||
import { getActiveWindow } from '../../../../base/browser/dom.js';
|
||||
|
||||
// ---------- Register commands and keybindings ----------
|
||||
|
||||
|
||||
const findStagingSelectionIndex = (currentSelections: StagingSelectionItem[] | undefined, newSelection: StagingSelectionItem): number | null => {
|
||||
if (!currentSelections) return null
|
||||
|
||||
for (let i = 0; i < currentSelections.length; i += 1) {
|
||||
const s = currentSelections[i]
|
||||
|
||||
if (s.uri.fsPath !== newSelection.uri.fsPath) continue
|
||||
|
||||
if (s.type === 'File' && newSelection.type === 'File') {
|
||||
return i
|
||||
}
|
||||
if (s.type === 'CodeSelection' && newSelection.type === 'CodeSelection') {
|
||||
if (s.uri.fsPath !== newSelection.uri.fsPath) continue
|
||||
// if there's any collision return true
|
||||
const [oldStart, oldEnd] = s.range
|
||||
const [newStart, newEnd] = newSelection.range
|
||||
if (oldStart !== newStart || oldEnd !== newEnd) continue
|
||||
return i
|
||||
}
|
||||
if (s.type === 'Folder' && newSelection.type === 'Folder') {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
export const roundRangeToLines = (range: IRange | null | undefined, options: { emptySelectionBehavior: 'null' | 'line' }) => {
|
||||
if (!range)
|
||||
return null
|
||||
@@ -53,24 +79,17 @@ export const roundRangeToLines = (range: IRange | null | undefined, options: { e
|
||||
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
|
||||
}
|
||||
// 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
|
||||
// }
|
||||
|
||||
|
||||
const findMatchingStagingIndex = (currentSelections: StagingSelectionItem[] | undefined, newSelection: StagingSelectionItem) => {
|
||||
return currentSelections?.findIndex(s =>
|
||||
s.fileURI.fsPath === newSelection.fileURI.fsPath
|
||||
&& s.range?.startLineNumber === newSelection.range?.startLineNumber
|
||||
&& s.range?.endLineNumber === newSelection.range?.endLineNumber
|
||||
)
|
||||
}
|
||||
|
||||
const VOID_OPEN_SIDEBAR_ACTION_ID = 'void.sidebar.open'
|
||||
registerAction2(class extends Action2 {
|
||||
@@ -114,22 +133,18 @@ registerAction2(class extends Action2 {
|
||||
editor?.setSelection({ startLineNumber: selectionRange.startLineNumber, endLineNumber: selectionRange.endLineNumber, startColumn: 1, endColumn: Number.MAX_SAFE_INTEGER })
|
||||
}
|
||||
|
||||
const selectionStr = getContentInRange(model, selectionRange)
|
||||
|
||||
const selection: StagingSelectionItem = !selectionRange || !selectionStr || (selectionRange.startLineNumber > selectionRange.endLineNumber) ? {
|
||||
const newSelection: StagingSelectionItem = !selectionRange || (selectionRange.startLineNumber > selectionRange.endLineNumber) ? {
|
||||
type: 'File',
|
||||
fileURI: model.uri,
|
||||
uri: model.uri,
|
||||
language: model.getLanguageId(),
|
||||
selectionStr: null,
|
||||
range: null,
|
||||
state: { isOpened: false, wasAddedAsCurrentFile: false }
|
||||
state: { wasAddedAsCurrentFile: false }
|
||||
} : {
|
||||
type: 'Selection',
|
||||
fileURI: model.uri,
|
||||
type: 'CodeSelection',
|
||||
uri: model.uri,
|
||||
language: model.getLanguageId(),
|
||||
selectionStr: selectionStr,
|
||||
range: selectionRange,
|
||||
state: { isOpened: true, wasAddedAsCurrentFile: false }
|
||||
range: [selectionRange.startLineNumber, selectionRange.endLineNumber],
|
||||
state: { wasAddedAsCurrentFile: false }
|
||||
}
|
||||
|
||||
// update the staging selections
|
||||
@@ -149,21 +164,18 @@ registerAction2(class extends Action2 {
|
||||
setSelections = (s) => chatThreadService.setCurrentMessageState(focusedMessageIdx, { stagingSelections: s })
|
||||
}
|
||||
|
||||
// close all selections besides the new one
|
||||
selections = selections.map(s => ({ ...s, state: { ...s.state, isOpened: false } }))
|
||||
|
||||
// if matches with existing selection, overwrite (since text may change)
|
||||
const matchingStagingEltIdx = findMatchingStagingIndex(selections, selection)
|
||||
if (matchingStagingEltIdx !== undefined && matchingStagingEltIdx !== -1) {
|
||||
const idx = findStagingSelectionIndex(selections, newSelection)
|
||||
if (idx !== null && idx !== -1) {
|
||||
setSelections([
|
||||
...selections!.slice(0, matchingStagingEltIdx),
|
||||
selection,
|
||||
...selections!.slice(matchingStagingEltIdx + 1, Infinity)
|
||||
...selections!.slice(0, idx),
|
||||
newSelection,
|
||||
...selections!.slice(idx + 1, Infinity)
|
||||
])
|
||||
}
|
||||
// if no match, add it
|
||||
else {
|
||||
setSelections([...(selections ?? []), selection])
|
||||
setSelections([...(selections ?? []), newSelection])
|
||||
}
|
||||
|
||||
}
|
||||
@@ -175,7 +187,7 @@ registerAction2(class extends Action2 {
|
||||
super({
|
||||
id: VOID_CTRL_L_ACTION_ID,
|
||||
f1: true,
|
||||
title: localize2('voidCtrlL', 'Void: Add Select to Chat'),
|
||||
title: localize2('voidCtrlL', 'Void: Add Selection to Chat'),
|
||||
keybinding: {
|
||||
primary: KeyMod.CtrlCmd | KeyCode.KeyL,
|
||||
weight: KeybindingWeight.VoidExtension
|
||||
@@ -200,7 +212,11 @@ registerAction2(class extends Action2 {
|
||||
id: 'void.newChatAction',
|
||||
title: 'New Chat',
|
||||
icon: { id: 'add' },
|
||||
menu: [{ id: MenuId.ViewTitle, group: 'navigation', when: ContextKeyExpr.equals('view', VOID_VIEW_ID), }]
|
||||
menu: [{ id: MenuId.ViewTitle, group: 'navigation', when: ContextKeyExpr.equals('view', VOID_VIEW_ID), }],
|
||||
keybinding: {
|
||||
primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KeyL,
|
||||
weight: KeybindingWeight.VoidExtension,
|
||||
},
|
||||
});
|
||||
}
|
||||
async run(accessor: ServicesAccessor): Promise<void> {
|
||||
@@ -210,9 +226,13 @@ registerAction2(class extends Action2 {
|
||||
metricsService.capture('Chat Navigation', { type: 'New Chat' })
|
||||
|
||||
stateService.setState({ isHistoryOpen: false, currentTab: 'chat' })
|
||||
stateService.fireFocusChat()
|
||||
const chatThreadService = accessor.get(IChatThreadService)
|
||||
chatThreadService.openNewThread()
|
||||
|
||||
// focus
|
||||
stateService.fireFocusChat()
|
||||
const window = getActiveWindow()
|
||||
window.requestAnimationFrame(() => stateService.fireFocusChat())
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -108,7 +108,7 @@ export const VOID_VIEW_ID = VOID_VIEW_CONTAINER_ID
|
||||
const viewContainerRegistry = Registry.as<IViewContainersRegistry>(ViewContainerExtensions.ViewContainersRegistry);
|
||||
const container = viewContainerRegistry.registerViewContainer({
|
||||
id: VOID_VIEW_CONTAINER_ID,
|
||||
title: nls.localize2('voidContainer', 'Void Chat'), // this is used to say "Void" (Ctrl + L)
|
||||
title: nls.localize2('voidContainer', 'Chat'), // this is used to say "Void" (Ctrl + L)
|
||||
ctorDescriptor: new SyncDescriptor(ViewPaneContainer, [VOID_VIEW_CONTAINER_ID, {
|
||||
mergeViewWithContainerWhenSingleView: true,
|
||||
orientation: Orientation.HORIZONTAL,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user