diff --git a/.gitignore b/.gitignore index b73ce578..5daf304c 100644 --- a/.gitignore +++ b/.gitignore @@ -21,3 +21,4 @@ vscode.db product.overrides.json *.snap.actual .vscode-test +.tmp/ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a84e550f..3d73f87f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,17 +2,18 @@ Welcome! 👋 This is the official guide on how to contribute to Void. We want to make it as easy as possible to contribute, so if you have any questions or comments, reach out via email or discord! + +If you'd like to get started developing Void, feel free to follow the steps below on building Void yourself and doing a PR. +Void's code mostly lives in `src/vs/workbench/contrib/void/` and `src/vs/platform/void/`. + There are a few ways to contribute: -- 👨‍💻 Build new features - see [Issues](https://github.com/voideditor/void/issues). +- 👨‍💻 Build new features - see [Roadmap](https://github.com/orgs/voideditor/projects/2/views/3) or [Issues](https://github.com/voideditor/void/issues). - 💡 Make suggestions in our [Discord](https://discord.gg/RSNjgaugJs). -- ⭐️ If you want to build your AI tool into Void, feel free to get in touch! It's very easy to extend Void, and the UX you create will be much more natural than a VSCode Extension. - -Most of Void's code lives in `src/vs/workbench/contrib/void/browser/` and `src/vs/platform/void/`. -## Building the full IDE +## Building Void ### a. Build Prerequisites - Mac @@ -41,21 +42,22 @@ First, run `npm install -g node-gyp`. Then: - Red Hat (Fedora, etc): `sudo dnf install @development-tools gcc gcc-c++ make libsecret-devel krb5-devel libX11-devel libxkbfile-devel`. - Others: see [How to Contribute](https://github.com/microsoft/vscode/wiki/How-to-Contribute). -### Building Void +### d. Building Void -To build Void, open `void/` inside VSCode. Then: +To build Void, open `void/` inside VSCode. Then open your terminal and run: 1. `npm install` to install all dependencies. -2. `npm run watchreact` to build Void's browser dependencies like React. -3. Build. +2. `npm run watchreact` to build Void's browser dependencies like React. (If this doesn't work, try `npm run buildreact`). +3. Build Void. - Press Cmd+Shift+B (Mac). - Press Ctrl+Shift+B (Windows/Linux). - This step can take ~5 min. The build is done when you see two check marks. -4. Run. - - Run `./scripts/code.sh` (Mac/Linux). +4. Run Void. + - Run `./scripts/code.sh` (Mac/Linux). - Run `./scripts/code.bat` (Windows). - - This command should open up the built IDE. You can always press Ctrl+Shift+P and run "Reload Window" inside the new window to see changes without re-building, unless they're React changes. - +6. Nice-to-knows. + - You can always press Ctrl+R (Cmd+R) inside the new window to reload and see your new changes. It's faster than Ctrl+Shift+P and `Reload Window`. + - You might want to add the flags `--user-data-dir ./.tmp/user-data --extensions-dir ./.tmp/extensions` to the above run command, which lets you delete the `.tmp` folder to reset any IDE changes you made when testing. #### Building Void from Terminal @@ -70,20 +72,20 @@ Alternatively, if you want to build Void from the terminal, instead of pressing -### Common Fixes +#### Common Fixes -- Make sure you follow the prerequisite steps. +- Make sure you followed the prerequisite steps. - Make sure you have the same NodeJS version as `.nvmrc`. -- Make sure your `npm run watchreact` is running if you change any React files, or else you'll need to re-build. -- If you get `"TypeError: Failed to fetch dynamically imported module: vscode-file://vscode-app/.../workbench.desktop.main.js", source: file:///.../bootstrap-window.js`, make sure all imports end with `.js`. -- If you have any questions, feel free to [submit an issue](https://github.com/voideditor/void/issues/new). For building questions, you can also refer to VSCode's full [How to Contribute](https://github.com/microsoft/vscode/wiki/How-to-Contribute) page. +- 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. +## Packaging -## Bundling +We don't usually recommend packaging. Instead, you should probably just build. If you're sure you want to package Void into an executable app, make sure you've built first, then run one of the following commands. This will create a folder named `VSCode-darwin-arm64` or similar outside of the void/ repo (see below). Be patient - packaging can take ~25 minutes. -We don't usually recommend bundling. Instead, you should probably just build. If you're sure you want to bundle Void into an executable app, make sure you've built first, then run one of the following commands. This will create a folder named `VSCode-darwin-arm64` (or similar) in the repo's parent's directory. Be patient - compiling can take ~25 minutes. ### Mac - `npm run gulp vscode-darwin-arm64` - most common (Apple Silicon) @@ -99,16 +101,41 @@ We don't usually recommend bundling. Instead, you should probably just build. If - `npm run gulp vscode-linux-ia32` +### Output -# Guidelines +This will generate a folder outside of `void/`: +```bash +workspace/ +├── void/ # Your Void fork +└── VSCode-darwin-arm64/ # Generated output +``` + +### Distributing +Void's maintainers distribute Void on our website and in releases. If you'd like to see the scripts to convert `Mac .app -> .dmg`, `Windows folder -> .exe`, and `Linux folder -> appimage` for distribution, feel free to reach out. + +## Pull Request Guidelines + + +- Please submit a pull request once you've made a change. +- No need to submit an Issue unless you're creating a new feature that might involve multiple PRs. +- Please don't use AI to write your PR 🙂 + + +## Codebase Guide + +We highly recommend reading [this](https://github.com/microsoft/vscode/wiki/Source-Code-Organization) article on VSCode's sourcecode organization. + +We are currently putting together our own articles on VSCode and Void's sourcecode organization. The best way to get this information right now is by attending a weekly meeting. + + + +For some miscellaneous useful links on VSCode we've put together, see [`VOID_USEFUL_LINKS.md`](https://github.com/voideditor/void/blob/main/VOID_USEFUL_LINKS.md). -We're always glad to talk about new ideas, help you get set up, and make sure your changes align with our vision for the project! Feel free to shoot Mat or Andrew a message, or start chatting with us in the `#contributing` channel of our [Discord](https://discord.gg/RSNjgaugJs). -## Submitting a Pull Request -- Please submit a pull request once you've made a change. No need to submit an Issue unless you're creating a new feature. -- Please don't use AI to write your PR 🙂. - -## References - -For some useful links we've compiled on VSCode, see [`VOID_USEFUL_LINKS.md`](https://github.com/voideditor/void/blob/main/VOID_USEFUL_LINKS.md). diff --git a/LICENSE-VS-Code.txt b/LICENSE-VS-Code.txt new file mode 100644 index 00000000..d34de290 --- /dev/null +++ b/LICENSE-VS-Code.txt @@ -0,0 +1,26 @@ +Void is a fork of VS Code, which is licensed under the MIT License (below). +Void's additions and modifications are licensed under the Apache 2.0 License (see LICENSE.txt). + +-------------------- + +MIT License + +Copyright (c) 2015 - present Microsoft Corporation + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/LICENSE.txt b/LICENSE.txt index 0ac28ee2..7637992d 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -1,21 +1,201 @@ -MIT License + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ -Copyright (c) 2015 - present Microsoft Corporation + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: + 1. Definitions. -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2025 Glass Devtools, Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.md b/README.md index c0a6ae93..613b772f 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,40 @@ # Welcome to Void. -Void is the open-source Cursor alternative. This repo contains the full sourcecode for Void. We have a [waitlist](https://voideditor.com/email) for downloading the official release, but you can build and develop Void right now. +
+ Void Welcome +
+ +Void is the open-source Cursor alternative. + +This repo contains the full sourcecode for Void. We are currently in [open beta](https://voideditor.com/) for Discord members (see `#announcements` to download), and we have a waitlist for our official release. If you're new, welcome! + +- 👋 [Discord](https://discord.gg/RSNjgaugJs) + +- 🚙 [Roadmap](https://github.com/orgs/voideditor/projects/2) + +- 📝 [Changelog](https://voideditor.com/changelog) + +- 🔨 [Contribute](https://github.com/voideditor/void/blob/main/CONTRIBUTING.md) -If you're new, welcome! ## Contributing -To build and run Void, follow the steps in [`CONTRIBUTING.md`](https://github.com/voideditor/void/blob/main/CONTRIBUTING.md). + +1. To get started developing Void, see [`CONTRIBUTING.md`](https://github.com/voideditor/void/blob/main/CONTRIBUTING.md). + +2. Feel free to attend a weekly meeting in our Discord channel! + +3. We're open to collaborations of all types - just reach out. + ## Reference -Void is a fork of the of [vscode](https://github.com/microsoft/vscode) repository. For some useful links on VSCode, see [`VOID_USEFUL_LINKS.md`](https://github.com/voideditor/void/blob/main/VOID_USEFUL_LINKS.md). +Void is a fork of the [vscode](https://github.com/microsoft/vscode) repository. For some useful links on VSCode, see [`VOID_USEFUL_LINKS.md`](https://github.com/voideditor/void/blob/main/VOID_USEFUL_LINKS.md). ## Support -Feel free to reach out in our [Discord](https://discord.gg/RSNjgaugJs) or contact us via email. +Feel free to reach out in our Discord or contact us via email: support@voideditor.com. diff --git a/SECURITY.md b/SECURITY.md deleted file mode 100644 index 82db58aa..00000000 --- a/SECURITY.md +++ /dev/null @@ -1,41 +0,0 @@ - - -## Security - -Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/Microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet) and [Xamarin](https://github.com/xamarin). - -If you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](https://aka.ms/security.md/definition), please report it to us as described below. - -## Reporting Security Issues - -**Please do not report security vulnerabilities through public GitHub issues.** - -Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://aka.ms/security.md/msrc/create-report). - -If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://aka.ms/security.md/msrc/pgp). - -You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://www.microsoft.com/msrc). - -Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue: - - * Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.) - * Full paths of source file(s) related to the manifestation of the issue - * The location of the affected source code (tag/branch/commit or direct URL) - * Any special configuration required to reproduce the issue - * Step-by-step instructions to reproduce the issue - * Proof-of-concept or exploit code (if possible) - * Impact of the issue, including how an attacker might exploit the issue - -This information will help us triage your report more quickly. - -If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://aka.ms/security.md/msrc/bounty) page for more details about our active programs. - -## Preferred Languages - -We prefer all communications to be in English. - -## Policy - -Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://aka.ms/security.md/cvd). - - diff --git a/VOID_USEFUL_LINKS.md b/VOID_USEFUL_LINKS.md index b3ba5a30..abf9a6e4 100644 --- a/VOID_USEFUL_LINKS.md +++ b/VOID_USEFUL_LINKS.md @@ -32,6 +32,6 @@ Void is no longer an extension, so these links are no longer required, but they - [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) +- [Activation events](https://code.visualstudio.com/api/references/activation-events) you can define in `package.json` (not the most useful). diff --git a/build/azure-pipelines/darwin/product-build-darwin-universal.yml b/build/azure-pipelines/darwin/product-build-darwin-universal.yml index f8c10a03..549e4107 100644 --- a/build/azure-pipelines/darwin/product-build-darwin-universal.yml +++ b/build/azure-pipelines/darwin/product-build-darwin-universal.yml @@ -1,3 +1,5 @@ +# Void - this looks like the relevant file for us (product-build-darwin.yml is independent and maybe just used for testing) + steps: - task: NodeTool@0 inputs: @@ -59,6 +61,8 @@ steps: - script: node build/azure-pipelines/distro/mixin-quality displayName: Mixin distro quality + + ## Void - IMPORTANT - script: | set -e unzip $(Pipeline.Workspace)/unsigned_vscode_client_darwin_x64_archive/VSCode-darwin-x64.zip -d $(agent.builddirectory)/VSCode-darwin-x64 @@ -66,6 +70,7 @@ steps: DEBUG=* node build/darwin/create-universal-app.js $(agent.builddirectory) displayName: Create Universal App + ## Void - IMPORTANT - script: | set -e security create-keychain -p pwd $(agent.tempdirectory)/buildagent.keychain diff --git a/build/darwin/create-universal-app.js b/build/darwin/create-universal-app.js index a3daf187..e6a355d5 100644 --- a/build/darwin/create-universal-app.js +++ b/build/darwin/create-universal-app.js @@ -4,6 +4,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ Object.defineProperty(exports, "__esModule", { value: true }); +// Void explanation - product-build-darwin-universal.yml runs this (create-universal-app.ts), then sign.ts const path = require("path"); const fs = require("fs"); const minimatch = require("minimatch"); diff --git a/build/darwin/create-universal-app.ts b/build/darwin/create-universal-app.ts index 94b8a23b..e732bd69 100644 --- a/build/darwin/create-universal-app.ts +++ b/build/darwin/create-universal-app.ts @@ -3,6 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +// Void explanation - product-build-darwin-universal.yml runs this (create-universal-app.ts), then sign.ts + import * as path from 'path'; import * as fs from 'fs'; import * as minimatch from 'minimatch'; diff --git a/build/darwin/sign.js b/build/darwin/sign.js index feb5834f..90c2e825 100644 --- a/build/darwin/sign.js +++ b/build/darwin/sign.js @@ -78,24 +78,24 @@ async function main(buildDir) { // universal will get its copy from the x64 build. if (arch !== 'universal') { await (0, cross_spawn_promise_1.spawn)('plutil', [ - '-insert', + '-replace', // Void changed this to replace 'NSAppleEventsUsageDescription', '-string', - 'An application in Visual Studio Code wants to use AppleScript.', + 'An application in Void wants to use AppleScript.', `${infoPlistPath}` ]); await (0, cross_spawn_promise_1.spawn)('plutil', [ '-replace', 'NSMicrophoneUsageDescription', '-string', - 'An application in Visual Studio Code wants to use the Microphone.', + 'An application in Void wants to use the Microphone.', `${infoPlistPath}` ]); await (0, cross_spawn_promise_1.spawn)('plutil', [ '-replace', 'NSCameraUsageDescription', '-string', - 'An application in Visual Studio Code wants to use the Camera.', + 'An application in Void wants to use the Camera.', `${infoPlistPath}` ]); } diff --git a/build/darwin/sign.ts b/build/darwin/sign.ts index 5b3413b7..a41ca30d 100644 --- a/build/darwin/sign.ts +++ b/build/darwin/sign.ts @@ -89,24 +89,24 @@ async function main(buildDir?: string): Promise { // universal will get its copy from the x64 build. if (arch !== 'universal') { await spawn('plutil', [ - '-insert', + '-replace', // Void changed this to replace 'NSAppleEventsUsageDescription', '-string', - 'An application in Visual Studio Code wants to use AppleScript.', + 'An application in Void wants to use AppleScript.', `${infoPlistPath}` ]); await spawn('plutil', [ '-replace', 'NSMicrophoneUsageDescription', '-string', - 'An application in Visual Studio Code wants to use the Microphone.', + 'An application in Void wants to use the Microphone.', `${infoPlistPath}` ]); await spawn('plutil', [ '-replace', 'NSCameraUsageDescription', '-string', - 'An application in Visual Studio Code wants to use the Camera.', + 'An application in Void wants to use the Camera.', `${infoPlistPath}` ]); } diff --git a/build/gulpfile.vscode.js b/build/gulpfile.vscode.js index 9dfb6a38..4d90766e 100644 --- a/build/gulpfile.vscode.js +++ b/build/gulpfile.vscode.js @@ -341,6 +341,8 @@ function packageTask(platform, arch, sourceFolderName, destinationFolderName, op this.emit('data', file); })); + + // 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 })) @@ -425,15 +427,15 @@ function packageTask(platform, arch, sourceFolderName, destinationFolderName, op 'resources/win32/vue.ico', 'resources/win32/xml.ico', 'resources/win32/yaml.ico', - 'resources/win32/code_70x70.png', - 'resources/win32/code_150x150.png' + 'resources/win32/code_70x70.png', // <-- Void icon + 'resources/win32/code_150x150.png' // <-- Void icon ], { base: '.' })); } else if (platform === 'linux') { - all = es.merge(all, gulp.src('resources/linux/code.png', { base: '.' })); + all = es.merge(all, gulp.src('resources/linux/code.png', { base: '.' })); // <-- Void icon } else if (platform === 'darwin') { const shortcut = gulp.src('resources/darwin/bin/code.sh') .pipe(replace('@@APPNAME@@', product.applicationName)) - .pipe(rename('bin/code')); + .pipe(rename('bin/code')); // <-- Void icon all = es.merge(all, shortcut); } diff --git a/build/lib/electron.js b/build/lib/electron.js index 99252e4e..3abbbd7d 100644 --- a/build/lib/electron.js +++ b/build/lib/electron.js @@ -54,7 +54,7 @@ function darwinBundleDocumentType(extensions, icon, nameOrSuffix, utis) { role: 'Editor', ostypes: ['TEXT', 'utxt', 'TUTX', '****'], extensions, - iconFile: 'resources/darwin/' + icon.toLowerCase() + '.icns', + iconFile: 'resources/darwin/' + icon.toLowerCase() + '.icns', // <-- Void icon code.icns utis }; } @@ -179,7 +179,7 @@ exports.config = { darwinForceDarkModeSupport: true, darwinCredits: darwinCreditsTemplate ? Buffer.from(darwinCreditsTemplate({ commit: commit, date: new Date().toISOString() })) : undefined, linuxExecutableName: product.applicationName, - winIcon: 'resources/win32/code.ico', + winIcon: 'resources/win32/code.ico', // <-- Void icon token: process.env['GITHUB_TOKEN'], repo: product.electronRepository || undefined, validateChecksum: true, diff --git a/build/lib/electron.ts b/build/lib/electron.ts index 7a2a2a19..47985a95 100644 --- a/build/lib/electron.ts +++ b/build/lib/electron.ts @@ -68,7 +68,7 @@ function darwinBundleDocumentType(extensions: string[], icon: string, nameOrSuff role: 'Editor', ostypes: ['TEXT', 'utxt', 'TUTX', '****'], extensions, - iconFile: 'resources/darwin/' + icon.toLowerCase() + '.icns', + iconFile: 'resources/darwin/' + icon.toLowerCase() + '.icns', // <-- Void icon code.icns utis }; } @@ -196,7 +196,7 @@ export const config = { darwinForceDarkModeSupport: true, darwinCredits: darwinCreditsTemplate ? Buffer.from(darwinCreditsTemplate({ commit: commit, date: new Date().toISOString() })) : undefined, linuxExecutableName: product.applicationName, - winIcon: 'resources/win32/code.ico', + winIcon: 'resources/win32/code.ico', // <-- Void icon token: process.env['GITHUB_TOKEN'], repo: product.electronRepository || undefined, validateChecksum: true, diff --git a/build/win32/code.iss b/build/win32/code.iss index fca3d1e9..8728ca24 100644 --- a/build/win32/code.iss +++ b/build/win32/code.iss @@ -4,6 +4,7 @@ ? ('; LicenseFile: "' + RepoDir + '\licenses\LICENSE-' + Language + '.rtf"') \ : '; LicenseFile: "' + RepoDir + '\' + RootLicenseFileName + '"' + [Setup] AppId={#AppId} AppName={#NameLong} @@ -20,8 +21,10 @@ Compression=lzma SolidCompression=yes AppMutex={code:GetAppMutex} SetupMutex={#AppMutex}setup -WizardImageFile="{#RepoDir}\resources\win32\inno-big-100.bmp,{#RepoDir}\resources\win32\inno-big-125.bmp,{#RepoDir}\resources\win32\inno-big-150.bmp,{#RepoDir}\resources\win32\inno-big-175.bmp,{#RepoDir}\resources\win32\inno-big-200.bmp,{#RepoDir}\resources\win32\inno-big-225.bmp,{#RepoDir}\resources\win32\inno-big-250.bmp" -WizardSmallImageFile="{#RepoDir}\resources\win32\inno-small-100.bmp,{#RepoDir}\resources\win32\inno-small-125.bmp,{#RepoDir}\resources\win32\inno-small-150.bmp,{#RepoDir}\resources\win32\inno-small-175.bmp,{#RepoDir}\resources\win32\inno-small-200.bmp,{#RepoDir}\resources\win32\inno-small-225.bmp,{#RepoDir}\resources\win32\inno-small-250.bmp" +; this is a Void icon comment. Old: WizardImageFile="{#RepoDir}\resources\win32\inno-big-100.bmp,{#RepoDir}\resources\win32\inno-big-125.bmp,{#RepoDir}\resources\win32\inno-big-150.bmp,{#RepoDir}\resources\win32\inno-big-175.bmp,{#RepoDir}\resources\win32\inno-big-200.bmp,{#RepoDir}\resources\win32\inno-big-225.bmp,{#RepoDir}\resources\win32\inno-big-250.bmp" +; this is a Void icon comment. Old: WizardSmallImageFile="{#RepoDir}\resources\win32\inno-small-100.bmp,{#RepoDir}\resources\win32\inno-small-125.bmp,{#RepoDir}\resources\win32\inno-small-150.bmp,{#RepoDir}\resources\win32\inno-small-175.bmp,{#RepoDir}\resources\win32\inno-small-200.bmp,{#RepoDir}\resources\win32\inno-small-225.bmp,{#RepoDir}\resources\win32\inno-small-250.bmp" +WizardImageFile="{#RepoDir}\resources\win32\inno-void.bmp" +WizardSmallImageFile="{#RepoDir}\resources\win32\inno-void.bmp" SetupIconFile={#RepoDir}\resources\win32\code.ico UninstallDisplayIcon={app}\{#ExeBasename}.exe ChangesEnvironment=true diff --git a/extensions/void/LangaugeServerTest/createJsProgramGraph.ts b/extensions/void/LangaugeServerTest/createJsProgramGraph.ts deleted file mode 100644 index 4fb9bcaf..00000000 --- a/extensions/void/LangaugeServerTest/createJsProgramGraph.ts +++ /dev/null @@ -1,333 +0,0 @@ -import * as vscode from 'vscode'; -import Parser from 'tree-sitter'; -import JavaScript from 'tree-sitter-javascript'; - -interface Definition { - file: string; - node: Parser.SyntaxNode; -} - -interface DefnUse { - parent: Parser.SyntaxNode; - file: string; -} - -interface ImportInfo { - source: string; - imported: string; -} - -class ProjectAnalyzer { - private parser: Parser; - private graph: Map>; - private visited: Set; - private parsedFiles: Map; - private imports: Map>; - private definitions: Map; - private fileStack: Set; - - constructor() { - this.parser = new Parser(); - this.parser.setLanguage(JavaScript); - this.graph = new Map(); - this.visited = new Set(); - this.parsedFiles = new Map(); - this.imports = new Map(); - this.definitions = new Map(); - this.fileStack = new Set(); - } - - async parseFile(filePath: string): Promise { - if (this.parsedFiles.has(filePath)) { - return this.parsedFiles.get(filePath)!; - } - - if (this.fileStack.has(filePath)) { - return null; // Circular import - } - - this.fileStack.add(filePath); - - try { - const uri = vscode.Uri.file(filePath); - const document = await vscode.workspace.openTextDocument(uri); - const code = document.getText(); - const tree = this.parser.parse(code); - - this.parsedFiles.set(filePath, tree); - this.collectImports(filePath, tree); - this.collectDefinitions(filePath, tree); - - return tree; - } catch (error) { - console.error(`Error parsing ${filePath}:`, error); - return null; - } finally { - this.fileStack.delete(filePath); - } - } - - private collectImports(filePath: string, tree: Parser.Tree): void { - const fileImports = new Map(); - - const visit = (node: Parser.SyntaxNode): void => { - if (node.type === 'import_declaration') { - const source = node.childForFieldName('source')?.text.slice(1, -1) ?? ''; - const specifiers = node.childForFieldName('specifiers'); - - specifiers?.children.forEach(spec => { - if (spec.type === 'import_specifier') { - const local = spec.childForFieldName('local')?.text ?? ''; - const imported = spec.childForFieldName('imported')?.text ?? ''; - fileImports.set(local, { source, imported }); - } - }); - } - node.children.forEach(visit); - }; - - visit(tree.rootNode); - this.imports.set(filePath, fileImports); - } - - private collectDefinitions(filePath: string, tree: Parser.Tree): void { - const visit = (node: Parser.SyntaxNode): void => { - if (node.type === 'function_declaration') { - const name = node.childForFieldName('name')?.text ?? ''; - this.definitions.set(name, { file: filePath, node }); - } - else if (node.type === 'variable_declarator') { - const name = node.childForFieldName('name')?.text; - const value = node.childForFieldName('value'); - if (name && (value?.type === 'arrow_function' || value?.type === 'function')) { - this.definitions.set(name, { file: filePath, node: value }); - } - } - node.children.forEach(visit); - }; - - visit(tree.rootNode); - } - - private async getTypeFromPosition(uri: vscode.Uri, position: vscode.Position): Promise { - const hover = await vscode.commands.executeCommand( - 'vscode.executeHoverProvider', - uri, - position - ); - - if (hover?.[0]?.contents.length) { - for (const content of hover[0].contents) { - let hoverText = typeof content === 'string' ? - content : - ('value' in content ? content.value : ''); - - // Remove typescript backticks if present - hoverText = hoverText.replace(/```typescript\s*/, '').replace(/```\s*$/, ''); - console.log('Processing hover text:', hoverText); - - // Extract the type information - look for the type after the colon - const typeMatches = [ - /:\s*([\w<>]+)(?:\[\])?/, // matches "foo: Type" or "foo: Type[]" - /var\s+\w+:\s*([\w<>]+)/, // matches "var foo: Type" - /\(type\)\s+[\w<>]+:\s*([\w<>]+)/, // matches "(type) foo: Type" - /\(method\)\s*([\w<>]+)\./ // matches "(method) Type.method" - ]; - - for (const pattern of typeMatches) { - const match = pattern.exec(hoverText); - if (match) { - let type = match[1]; - // Handle array types - if (hoverText.includes('[]')) { - return 'Array'; - } - // Extract base type from generics - if (type.includes('<')) { - type = type.split('<')[0]; - } - return type; - } - } - } - } - return null; - } - - private async getCallsInDefn(defnNode: Parser.SyntaxNode, currentFile: string): Promise> { - const calls = new Set(); - const fileImports = this.imports.get(currentFile) ?? new Map(); - const uri = vscode.Uri.file(currentFile); - - const visit = async (node: Parser.SyntaxNode): Promise => { - if (node.type === 'call_expression') { - const callee = node.childForFieldName('function'); - if (callee?.type === 'identifier') { - const name = callee.text; - const importInfo = fileImports.get(name); - if (importInfo) { - calls.add(`${importInfo.source}:${importInfo.imported}`); - } else { - calls.add(name); - } - } - else if (callee?.type === 'member_expression') { - const method = callee.childForFieldName('property')?.text; - const object = callee.childForFieldName('object'); - - if (method && object) { - const position = new vscode.Position( - object.startPosition.row, - object.startPosition.column - ); - - const type = await this.getTypeFromPosition(uri, position); - if (type) { - calls.add(`${type}.${method}`); - } else { - calls.add(`method:${method}`); - } - } - } - } - - for (const child of node.children) { - await visit(child); - } - }; - - await visit(defnNode); - return calls; - } - - private gotoDefn(name: string): Definition | null { - if (name.includes(':')) { - const [file, funcName] = name.split(':'); - const def = this.definitions.get(funcName); - return def ?? null; - } - - return this.definitions.get(name) ?? null; - } - - private getUses(defnNode: Parser.SyntaxNode, currentFile: string): DefnUse[] { - const uses: DefnUse[] = []; - - let fnName: string | undefined; - if (defnNode.type === 'function_declaration') { - fnName = defnNode.childForFieldName('name')?.text; - } else if (defnNode.type === 'arrow_function' || defnNode.type === 'function') { - const parent = defnNode.parent; - if (parent?.type === 'variable_declarator') { - fnName = parent.childForFieldName('name')?.text; - } - } - - if (!fnName) return uses; - - for (const [file, tree] of this.parsedFiles) { - const visit = (node: Parser.SyntaxNode): void => { - if (node.type === 'call_expression') { - const callee = node.childForFieldName('function'); - if (callee?.type === 'identifier' && callee.text === fnName) { - let current: Parser.SyntaxNode | null = node; - while (current) { - if (current.type === 'function_declaration' || - current.type === 'arrow_function' || - current.type === 'function') { - uses.push({ parent: current, file }); - break; - } - current = current.parent; - } - } - } - node.children.forEach(visit); - }; - - visit(tree.rootNode); - } - - return uses; - } - - private async visitAllNodesInGraphFromDefinition(defn: Parser.SyntaxNode, currentFile: string): Promise { - let defnName: string | undefined; - if (defn.type === 'function_declaration') { - defnName = defn.childForFieldName('name')?.text; - } else if (defn.type === 'arrow_function' || defn.type === 'function') { - const parent = defn.parent; - if (parent?.type === 'variable_declarator') { - defnName = parent.childForFieldName('name')?.text; - } - } - - if (!defnName) return; - - const fullName = `${currentFile}:${defnName}`; - if (this.visited.has(fullName)) return; - - const calls = await this.getCallsInDefn(defn, currentFile); - this.graph.set(fullName, calls); - this.visited.add(fullName); - - const callDefns = Array.from(calls).map(call => this.gotoDefn(call)); - for (const callDefn of callDefns) { - if (callDefn) { - await this.visitAllNodesInGraphFromDefinition(callDefn.node, callDefn.file); - } - } - - const defnUses = this.getUses(defn, currentFile); - for (const defnUse of defnUses) { - await this.visitAllNodesInGraphFromDefinition(defnUse.parent, defnUse.file); - } - } - - async analyze(entryFile: string): Promise>> { - const tree = await this.parseFile(entryFile); - if (!tree) return new Map(); - - const visit = async (node: Parser.SyntaxNode): Promise => { - if (node.type === 'function_declaration') { - await this.visitAllNodesInGraphFromDefinition(node, entryFile); - } - else if (node.type === 'variable_declarator') { - const value = node.childForFieldName('value'); - if (value?.type === 'arrow_function' || value?.type === 'function') { - await this.visitAllNodesInGraphFromDefinition(value, entryFile); - } - } - for (const child of node.children) { - await visit(child); - } - }; - - await visit(tree.rootNode); - return this.graph; - } -} - -export async function runTreeSitter(filePath?: string): Promise> | null> { - const editor = vscode.window.activeTextEditor; - if (!editor && !filePath) { - vscode.window.showWarningMessage('No active editor found'); - return null; - } - - try { - const targetPath = filePath ?? editor!.document.uri.fsPath; - const analyzer = new ProjectAnalyzer(); - const graph = await analyzer.analyze(targetPath); - - for (const [defn, calls] of graph) { - console.log(`${defn} calls: ${[...calls].join(', ')}`); - } - - return graph; - } catch (error) { - console.error('Error analyzing file:', error); - vscode.window.showErrorMessage('Error analyzing file'); - return null; - } -} \ No newline at end of file diff --git a/extensions/void/LangaugeServerTest/findFunctions.ts b/extensions/void/LangaugeServerTest/findFunctions.ts deleted file mode 100644 index 570b4369..00000000 --- a/extensions/void/LangaugeServerTest/findFunctions.ts +++ /dev/null @@ -1,62 +0,0 @@ -import * as vscode from 'vscode'; - -const legend = new vscode.SemanticTokensLegend([], []); - -export async function findFunctions() { - - const editor = vscode.window.activeTextEditor; - if (!editor) return; - const document = editor.document; - - const tokens = await vscode.commands.executeCommand( - 'vscode.provideDocumentSemanticTokens', - document.uri - ); - - if (!tokens) { - console.error('No tokens found'); - return []; - } - - const allTokens = decodeTokens(tokens, document); - - - return allTokens; -} - -function decodeTokens(tokens: vscode.SemanticTokens, document: vscode.TextDocument) { - const data = tokens.data; - const decodedTokens = []; - let line = 0; - let character = 0; - - for (let i = 0; i < data.length; i += 5) { - const deltaLine = data[i]; - const deltaStartChar = data[i + 1]; - const length = data[i + 2]; - const tokenTypeIdx = data[i + 3]; - const tokenModifierIdx = data[i + 4]; - - line += deltaLine; - character = deltaLine === 0 ? character + deltaStartChar : deltaStartChar; - - const type = legend.tokenTypes[tokenTypeIdx] || `(${tokenTypeIdx})`; - const modifier = legend.tokenModifiers[tokenModifierIdx] || `(${tokenModifierIdx})`; - - const tokenRange = new vscode.Range(line, character, line, character + length); - const tokenText = document.getText(tokenRange); - - decodedTokens.push({ - line, - startCharacter: character, - length, - type, - modifier, - text: tokenText, - }); - - console.log(`Token: '${tokenText}' | Type: ${type} | Modifier: ${modifier} | Line: ${line}, Character: ${character}`); - } - - return decodedTokens; -} diff --git a/package-lock.json b/package-lock.json index 307f13cf..91937c1c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,11 +1,11 @@ { - "name": "code-oss-dev", + "name": "void-dev", "version": "1.94.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "code-oss-dev", + "name": "void-dev", "version": "1.94.0", "hasInstallScript": true, "license": "MIT", @@ -39,6 +39,7 @@ "@xterm/addon-webgl": "^0.19.0-beta.64", "@xterm/headless": "^5.6.0-beta.64", "@xterm/xterm": "^5.6.0-beta.64", + "ajv": "^8.17.1", "diff": "^7.0.0", "groq-sdk": "^0.9.0", "http-proxy-agent": "^7.0.0", @@ -155,6 +156,7 @@ "mocha": "^10.2.0", "mocha-junit-reporter": "^2.2.1", "mocha-multi-reporters": "^1.5.1", + "next": "^15.1.4", "nodemon": "^3.1.9", "npm-run-all": "^4.1.5", "opn": "^6.0.0", @@ -1055,6 +1057,17 @@ "node": ">= 4.0.0" } }, + "node_modules/@emnapi/runtime": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.3.1.tgz", + "integrity": "sha512-kEBmG8KyqtxJZv+ygbEim+KCGtIq1fC22Ms3S4ziXmYKm8uyoLX0MHONVKwp+9opg390VaKRNt4a7A9NwmpNhw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@es-joy/jsdoccomment": { "version": "0.41.0", "resolved": "https://registry.npmjs.org/@es-joy/jsdoccomment/-/jsdoccomment-0.41.0.tgz", @@ -1503,6 +1516,30 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/@eslint/eslintrc/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@eslint/eslintrc/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, "node_modules/@eslint/js": { "version": "8.36.0", "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.36.0.tgz", @@ -1677,6 +1714,386 @@ "deprecated": "Use @eslint/object-schema instead", "dev": true }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.5.tgz", + "integrity": "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.0.4" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.5.tgz", + "integrity": "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.0.4" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.4.tgz", + "integrity": "sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.4.tgz", + "integrity": "sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.5.tgz", + "integrity": "sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.4.tgz", + "integrity": "sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.4.tgz", + "integrity": "sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.4.tgz", + "integrity": "sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.4.tgz", + "integrity": "sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.4.tgz", + "integrity": "sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.5.tgz", + "integrity": "sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.0.5" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.5.tgz", + "integrity": "sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.0.4" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.5.tgz", + "integrity": "sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.0.4" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.5.tgz", + "integrity": "sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.0.4" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.5.tgz", + "integrity": "sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.0.4" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.5.tgz", + "integrity": "sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.0.4" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.33.5.tgz", + "integrity": "sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.2.0" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.5.tgz", + "integrity": "sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.5.tgz", + "integrity": "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -1949,6 +2366,149 @@ "exenv-es6": "^1.1.1" } }, + "node_modules/@next/env": { + "version": "15.1.4", + "resolved": "https://registry.npmjs.org/@next/env/-/env-15.1.4.tgz", + "integrity": "sha512-2fZ5YZjedi5AGaeoaC0B20zGntEHRhi2SdWcu61i48BllODcAmmtj8n7YarSPt4DaTsJaBFdxQAVEVzgmx2Zpw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@next/swc-darwin-arm64": { + "version": "15.1.4", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.1.4.tgz", + "integrity": "sha512-wBEMBs+np+R5ozN1F8Y8d/Dycns2COhRnkxRc+rvnbXke5uZBHkUGFgWxfTXn5rx7OLijuUhyfB+gC/ap58dDw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-darwin-x64": { + "version": "15.1.4", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.1.4.tgz", + "integrity": "sha512-7sgf5rM7Z81V9w48F02Zz6DgEJulavC0jadab4ZsJ+K2sxMNK0/BtF8J8J3CxnsJN3DGcIdC260wEKssKTukUw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-gnu": { + "version": "15.1.4", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.1.4.tgz", + "integrity": "sha512-JaZlIMNaJenfd55kjaLWMfok+vWBlcRxqnRoZrhFQrhM1uAehP3R0+Aoe+bZOogqlZvAz53nY/k3ZyuKDtT2zQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-musl": { + "version": "15.1.4", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.1.4.tgz", + "integrity": "sha512-7EBBjNoyTO2ipMDgCiORpwwOf5tIueFntKjcN3NK+GAQD7OzFJe84p7a2eQUeWdpzZvhVXuAtIen8QcH71ZCOQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-gnu": { + "version": "15.1.4", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.1.4.tgz", + "integrity": "sha512-9TGEgOycqZFuADyFqwmK/9g6S0FYZ3tphR4ebcmCwhL8Y12FW8pIBKJvSwV+UBjMkokstGNH+9F8F031JZKpHw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-musl": { + "version": "15.1.4", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.1.4.tgz", + "integrity": "sha512-0578bLRVDJOh+LdIoKvgNDz77+Bd85c5JrFgnlbI1SM3WmEQvsjxTA8ATu9Z9FCiIS/AliVAW2DV/BDwpXbtiQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-arm64-msvc": { + "version": "15.1.4", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.1.4.tgz", + "integrity": "sha512-JgFCiV4libQavwII+kncMCl30st0JVxpPOtzWcAI2jtum4HjYaclobKhj+JsRu5tFqMtA5CJIa0MvYyuu9xjjQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-x64-msvc": { + "version": "15.1.4", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.1.4.tgz", + "integrity": "sha512-xxsJy9wzq7FR5SqPCUqdgSXiNXrMuidgckBa8nH9HtjjxsilgcN6VgXF6tZ3uEWuVEadotQJI8/9EQ6guTC4Yw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -2686,6 +3246,23 @@ "node": ">=10" } }, + "node_modules/@swc/counter": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", + "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/@swc/helpers": { + "version": "0.5.15", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", + "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.8.0" + } + }, "node_modules/@szmarczak/http-timer": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz", @@ -4480,15 +5057,15 @@ } }, "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "license": "MIT", "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" }, "funding": { "type": "github", @@ -4512,28 +5089,6 @@ } } }, - "node_modules/ajv-formats/node_modules/ajv": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", - "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", - "dev": true, - "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/ajv-formats/node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true - }, "node_modules/ajv-keywords": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", @@ -5479,6 +6034,18 @@ "esbuild": ">=0.18" } }, + "node_modules/busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "dev": true, + "dependencies": { + "streamsearch": "^1.1.0" + }, + "engines": { + "node": ">=10.16.0" + } + }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -5904,6 +6471,13 @@ "integrity": "sha512-GRMWDxpOB6Dgk2E5Uo+3eEBvtOOlimMmpbFiKuLFnQzYDavtLFY3K5ona41jgN/WdRZtG7utuVSVTL4HbZHGkw==", "dev": true }, + "node_modules/client-only": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", + "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", + "dev": true, + "license": "MIT" + }, "node_modules/cliui": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", @@ -6108,6 +6682,21 @@ "node": ">=0.10.0" } }, + "node_modules/color": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", + "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "color-convert": "^2.0.1", + "color-string": "^1.9.0" + }, + "engines": { + "node": ">=12.5.0" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -6126,6 +6715,18 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, + "node_modules/color-string": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", + "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, "node_modules/color-support": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", @@ -7815,6 +8416,23 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/eslint/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, "node_modules/eslint/node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -7827,6 +8445,13 @@ "node": ">=10.13.0" } }, + "node_modules/eslint/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, "node_modules/esniff": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/esniff/-/esniff-2.0.1.tgz", @@ -8202,8 +8827,7 @@ "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" }, "node_modules/fast-fifo": { "version": "1.3.2", @@ -8231,7 +8855,8 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/fast-levenshtein": { "version": "2.0.6", @@ -8242,8 +8867,7 @@ "node_modules/fast-uri": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.3.tgz", - "integrity": "sha512-aLrHthzCjH5He4Z2H9YZ+v6Ujb9ocRuW6ZzkJQOrTxleEijANq4v1TsaPaVG1PZcuurEzrLcWRyYBYXD5cEiaw==", - "dev": true + "integrity": "sha512-aLrHthzCjH5He4Z2H9YZ+v6Ujb9ocRuW6ZzkJQOrTxleEijANq4v1TsaPaVG1PZcuurEzrLcWRyYBYXD5cEiaw==" }, "node_modules/fast-xml-parser": { "version": "4.5.1", @@ -8372,6 +8996,23 @@ "webpack": "^4.0.0 || ^5.0.0" } }, + "node_modules/file-loader/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, "node_modules/file-loader/node_modules/ajv-keywords": { "version": "3.5.2", "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", @@ -8381,6 +9022,13 @@ "ajv": "^6.9.1" } }, + "node_modules/file-loader/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, "node_modules/file-loader/node_modules/schema-utils": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", @@ -10446,6 +11094,23 @@ "node": ">=0.4.0" } }, + "node_modules/gulp-eslint/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, "node_modules/gulp-eslint/node_modules/ansi-regex": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.1.tgz", @@ -10719,6 +11384,13 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/gulp-eslint/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, "node_modules/gulp-eslint/node_modules/levn": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", @@ -13840,10 +14512,10 @@ "dev": true }, "node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" }, "node_modules/json-stable-stringify-without-jsonify": { "version": "1.0.1", @@ -15546,12 +16218,96 @@ "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", "dev": true }, + "node_modules/next": { + "version": "15.1.4", + "resolved": "https://registry.npmjs.org/next/-/next-15.1.4.tgz", + "integrity": "sha512-mTaq9dwaSuwwOrcu3ebjDYObekkxRnXpuVL21zotM8qE2W0HBOdVIdg2Li9QjMEZrj73LN96LcWcz62V19FjAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@next/env": "15.1.4", + "@swc/counter": "0.1.3", + "@swc/helpers": "0.5.15", + "busboy": "1.6.0", + "caniuse-lite": "^1.0.30001579", + "postcss": "8.4.31", + "styled-jsx": "5.1.6" + }, + "bin": { + "next": "dist/bin/next" + }, + "engines": { + "node": "^18.18.0 || ^19.8.0 || >= 20.0.0" + }, + "optionalDependencies": { + "@next/swc-darwin-arm64": "15.1.4", + "@next/swc-darwin-x64": "15.1.4", + "@next/swc-linux-arm64-gnu": "15.1.4", + "@next/swc-linux-arm64-musl": "15.1.4", + "@next/swc-linux-x64-gnu": "15.1.4", + "@next/swc-linux-x64-musl": "15.1.4", + "@next/swc-win32-arm64-msvc": "15.1.4", + "@next/swc-win32-x64-msvc": "15.1.4", + "sharp": "^0.33.5" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.1.0", + "@playwright/test": "^1.41.2", + "babel-plugin-react-compiler": "*", + "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", + "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", + "sass": "^1.3.0" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + }, + "@playwright/test": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + }, + "sass": { + "optional": true + } + } + }, "node_modules/next-tick": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.1.0.tgz", "integrity": "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==", "dev": true }, + "node_modules/next/node_modules/postcss": { + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.6", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, "node_modules/nice-try": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", @@ -18671,7 +19427,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -19157,28 +19912,6 @@ "url": "https://opencollective.com/webpack" } }, - "node_modules/schema-utils/node_modules/ajv": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", - "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", - "dev": true, - "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/schema-utils/node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true - }, "node_modules/scope-tailwind": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/scope-tailwind/-/scope-tailwind-1.0.5.tgz", @@ -19393,6 +20126,47 @@ "node": ">=0.10.0" } }, + "node_modules/sharp": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.5.tgz", + "integrity": "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "color": "^4.2.3", + "detect-libc": "^2.0.3", + "semver": "^7.6.3" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.33.5", + "@img/sharp-darwin-x64": "0.33.5", + "@img/sharp-libvips-darwin-arm64": "1.0.4", + "@img/sharp-libvips-darwin-x64": "1.0.4", + "@img/sharp-libvips-linux-arm": "1.0.5", + "@img/sharp-libvips-linux-arm64": "1.0.4", + "@img/sharp-libvips-linux-s390x": "1.0.4", + "@img/sharp-libvips-linux-x64": "1.0.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.0.4", + "@img/sharp-libvips-linuxmusl-x64": "1.0.4", + "@img/sharp-linux-arm": "0.33.5", + "@img/sharp-linux-arm64": "0.33.5", + "@img/sharp-linux-s390x": "0.33.5", + "@img/sharp-linux-x64": "0.33.5", + "@img/sharp-linuxmusl-arm64": "0.33.5", + "@img/sharp-linuxmusl-x64": "0.33.5", + "@img/sharp-wasm32": "0.33.5", + "@img/sharp-win32-ia32": "0.33.5", + "@img/sharp-win32-x64": "0.33.5" + } + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -19553,6 +20327,25 @@ "simple-concat": "^1.0.0" } }, + "node_modules/simple-swizzle": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", + "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "is-arrayish": "^0.3.1" + } + }, + "node_modules/simple-swizzle/node_modules/is-arrayish": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", + "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==", + "dev": true, + "license": "MIT", + "optional": true + }, "node_modules/simple-update-notifier": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", @@ -20203,6 +20996,15 @@ "node": ">=0.10" } }, + "node_modules/streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", + "dev": true, + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/streamx": { "version": "2.21.1", "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.21.1.tgz", @@ -20441,6 +21243,30 @@ "webpack": "^5.0.0" } }, + "node_modules/styled-jsx": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz", + "integrity": "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "client-only": "0.0.1" + }, + "engines": { + "node": ">= 12.0.0" + }, + "peerDependencies": { + "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "babel-plugin-macros": { + "optional": true + } + } + }, "node_modules/stylehacks": { "version": "6.1.1", "resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-6.1.1.tgz", @@ -20705,6 +21531,23 @@ "node": ">=6.0.0" } }, + "node_modules/table/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, "node_modules/table/node_modules/ansi-regex": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz", @@ -20729,6 +21572,13 @@ "node": ">=4" } }, + "node_modules/table/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, "node_modules/table/node_modules/string-width": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", @@ -22149,6 +22999,7 @@ "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { "punycode": "^2.1.0" } @@ -22721,6 +23572,23 @@ "node": ">= 0.10" } }, + "node_modules/webpack/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, "node_modules/webpack/node_modules/ajv-keywords": { "version": "3.5.2", "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", @@ -22752,6 +23620,13 @@ "node": ">=4.0" } }, + "node_modules/webpack/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, "node_modules/webpack/node_modules/schema-utils": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", diff --git a/package.json b/package.json index 0881e4c5..442ec60f 100644 --- a/package.json +++ b/package.json @@ -1,9 +1,12 @@ { - "name": "code-oss-dev", + "name": "void-dev", + "productName": "Void", "version": "1.94.0", - "distro": "ffcc24343ac46468a625666e5b9e673971dd1a1f", + "distro": "this is a commit number if we want to publish on npm", + "homepage": "https://voideditor.com", "author": { - "name": "Microsoft Corporation" + "name": "Glass Devtools, Inc.", + "email": "andrew@voideditor.com" }, "license": "MIT", "main": "./out/main", @@ -104,6 +107,7 @@ "@xterm/addon-webgl": "^0.19.0-beta.64", "@xterm/headless": "^5.6.0-beta.64", "@xterm/xterm": "^5.6.0-beta.64", + "ajv": "^8.17.1", "diff": "^7.0.0", "groq-sdk": "^0.9.0", "http-proxy-agent": "^7.0.0", @@ -220,6 +224,7 @@ "mocha": "^10.2.0", "mocha-junit-reporter": "^2.2.1", "mocha-multi-reporters": "^1.5.1", + "next": "^15.1.4", "nodemon": "^3.1.9", "npm-run-all": "^4.1.5", "opn": "^6.0.0", diff --git a/product.json b/product.json index a35f197c..a79966a3 100644 --- a/product.json +++ b/product.json @@ -1,84 +1,38 @@ { "nameShort": "Void", "nameLong": "Void", - "applicationName": "code-oss", - "dataFolderName": ".vscode-oss", - "win32MutexName": "vscodeoss", + "applicationName": "void", + "dataFolderName": ".void-editor", + "win32MutexName": "voideditor", "licenseName": "MIT", - "licenseUrl": "https://github.com/microsoft/vscode/blob/main/LICENSE.txt", - "serverLicenseUrl": "https://github.com/microsoft/vscode/blob/main/LICENSE.txt", + "licenseUrl": "https://github.com/voideditor/void/blob/main/LICENSE.txt", + "serverLicenseUrl": "https://github.com/voideditor/void/blob/main/LICENSE.txt", "serverGreeting": [], "serverLicense": [], "serverLicensePrompt": "", - "serverApplicationName": "code-server-oss", - "serverDataFolderName": ".vscode-server-oss", - "tunnelApplicationName": "code-tunnel-oss", - "win32DirName": "Microsoft Code OSS", - "win32NameVersion": "Microsoft Code OSS", - "win32RegValueName": "CodeOSS", - "win32x64AppId": "{{D77B7E06-80BA-4137-BCF4-654B95CCEBC5}", - "win32arm64AppId": "{{D1ACE434-89C5-48D1-88D3-E2991DF85475}", - "win32x64UserAppId": "{{CC6B787D-37A0-49E8-AE24-8559A032BE0C}", - "win32arm64UserAppId": "{{3AEBF0C8-F733-4AD4-BADE-FDB816D53D7B}", - "win32AppUserModelId": "Microsoft.CodeOSS", - "win32ShellNameShort": "C&ode - OSS", - "win32TunnelServiceMutex": "vscodeoss-tunnelservice", - "win32TunnelMutex": "vscodeoss-tunnel", - "darwinBundleIdentifier": "com.visualstudio.code.oss", - "linuxIconName": "code-oss", + "serverApplicationName": "void-server", + "serverDataFolderName": ".void-server", + "tunnelApplicationName": "void-tunnel", + "win32DirName": "Void", + "win32NameVersion": "Void", + "win32RegValueName": "VoidEditor", + "win32x64AppId": "{{9D394D01-1728-45A7-B997-A6C82C5452C3}", + "win32arm64AppId": "{{0668DD58-2BDE-4101-8CDA-40252DF8875D}", + "win32x64UserAppId": "{{8BED5DC1-6C55-46E6-9FE6-18F7E6F7C7F1}", + "win32arm64UserAppId": "{{F6C87466-BC82-4A8F-B0FF-18CA366BA4D8}", + "win32AppUserModelId": "Void.Editor", + "win32ShellNameShort": "V&oid", + "win32TunnelServiceMutex": "void-tunnelservice", + "win32TunnelMutex": "void-tunnel", + "darwinBundleIdentifier": "com.voideditor.code", + "linuxIconName": "void-editor", "licenseFileName": "LICENSE.txt", - "reportIssueUrl": "https://github.com/microsoft/vscode/issues/new", + "reportIssueUrl": "https://github.com/voideditor/void/issues/new", "nodejsRepository": "https://nodejs.org", - "urlProtocol": "code-oss", - "webviewContentExternalBaseUrlTemplate": "https://{{uuid}}.vscode-cdn.net/insider/ef65ac1ba57f57f2a3961bfe94aa20481caca4c6/out/vs/workbench/contrib/webview/browser/pre/", - "builtInExtensions": [ - { - "name": "ms-vscode.js-debug-companion", - "version": "1.1.3", - "sha256": "7380a890787452f14b2db7835dfa94de538caf358ebc263f9d46dd68ac52de93", - "repo": "https://github.com/microsoft/vscode-js-debug-companion", - "metadata": { - "id": "99cb0b7f-7354-4278-b8da-6cc79972169d", - "publisherId": { - "publisherId": "5f5636e7-69ed-4afe-b5d6-8d231fb3d3ee", - "publisherName": "ms-vscode", - "displayName": "Microsoft", - "flags": "verified" - }, - "publisherDisplayName": "Microsoft" - } - }, - { - "name": "ms-vscode.js-debug", - "version": "1.93.0", - "sha256": "9339cb8e6b77f554df54d79e71f533279cb76b0f9b04c207f633bfd507442b6a", - "repo": "https://github.com/microsoft/vscode-js-debug", - "metadata": { - "id": "25629058-ddac-4e17-abba-74678e126c5d", - "publisherId": { - "publisherId": "5f5636e7-69ed-4afe-b5d6-8d231fb3d3ee", - "publisherName": "ms-vscode", - "displayName": "Microsoft", - "flags": "verified" - }, - "publisherDisplayName": "Microsoft" - } - }, - { - "name": "ms-vscode.vscode-js-profile-table", - "version": "1.0.9", - "sha256": "3b62ee4276a2bbea3fe230f94b1d5edd915b05966090ea56f882e1e0ab53e1a6", - "repo": "https://github.com/microsoft/vscode-js-profile-visualizer", - "metadata": { - "id": "7e52b41b-71ad-457b-ab7e-0620f1fc4feb", - "publisherId": { - "publisherId": "5f5636e7-69ed-4afe-b5d6-8d231fb3d3ee", - "publisherName": "ms-vscode", - "displayName": "Microsoft", - "flags": "verified" - }, - "publisherDisplayName": "Microsoft" - } - } - ] + "urlProtocol": "void-editor", + "extensionsGallery": { + "serviceUrl": "https://open-vsx.org/vscode/gallery", + "itemUrl": "https://open-vsx.org/vscode/item" + }, + "builtInExtensions": [] } diff --git a/resources/darwin/code.icns b/resources/darwin/code.icns index a91b7c58..1fa6e9d0 100644 Binary files a/resources/darwin/code.icns and b/resources/darwin/code.icns differ diff --git a/resources/linux/code.png b/resources/linux/code.png index 8d8646f8..97ca4fac 100644 Binary files a/resources/linux/code.png and b/resources/linux/code.png differ diff --git a/resources/server/code-192.png b/resources/server/code-192.png index 8d8646f8..d0380464 100644 Binary files a/resources/server/code-192.png and b/resources/server/code-192.png differ diff --git a/resources/server/code-512.png b/resources/server/code-512.png index 8d8646f8..d0380464 100644 Binary files a/resources/server/code-512.png and b/resources/server/code-512.png differ diff --git a/resources/server/favicon.ico b/resources/server/favicon.ico index f9f0de3e..2659eb19 100644 Binary files a/resources/server/favicon.ico and b/resources/server/favicon.ico differ diff --git a/resources/win32/VisualElementsManifest.xml b/resources/win32/VisualElementsManifest.xml index 40efd0a3..71b18418 100644 --- a/resources/win32/VisualElementsManifest.xml +++ b/resources/win32/VisualElementsManifest.xml @@ -1,9 +1,9 @@ + ForegroundText="light" + ShortDisplayName="Void" /> diff --git a/resources/win32/code.ico b/resources/win32/code.ico index e95d71fc..2659eb19 100644 Binary files a/resources/win32/code.ico and b/resources/win32/code.ico differ diff --git a/resources/win32/code_150x150.png b/resources/win32/code_150x150.png index 55e9f8f3..e790eced 100644 Binary files a/resources/win32/code_150x150.png and b/resources/win32/code_150x150.png differ diff --git a/resources/win32/code_70x70.png b/resources/win32/code_70x70.png index ff1bdc9d..08fc0079 100644 Binary files a/resources/win32/code_70x70.png and b/resources/win32/code_70x70.png differ diff --git a/resources/win32/inno-void.bmp b/resources/win32/inno-void.bmp new file mode 100644 index 00000000..fb3fad34 Binary files /dev/null and b/resources/win32/inno-void.bmp differ diff --git a/resources/win32/logo_cube_noshadow.png b/resources/win32/logo_cube_noshadow.png new file mode 100644 index 00000000..225179f8 Binary files /dev/null and b/resources/win32/logo_cube_noshadow.png differ diff --git a/src/vs/base/browser/ui/inputbox/inputBox.ts b/src/vs/base/browser/ui/inputbox/inputBox.ts index 990ee808..5751fb8d 100644 --- a/src/vs/base/browser/ui/inputbox/inputBox.ts +++ b/src/vs/base/browser/ui/inputbox/inputBox.ts @@ -159,7 +159,7 @@ export class InputBox extends Widget { this.scrollableElement = new ScrollableElement(this.element, { vertical: ScrollbarVisibility.Auto }); if (this.options.flexibleWidth) { - this.input.setAttribute('wrap', 'off'); + this.input.setAttribute('wrap', 'on'); this.mirror.style.whiteSpace = 'pre'; this.mirror.style.wordWrap = 'initial'; } diff --git a/src/vs/code/electron-main/app.ts b/src/vs/code/electron-main/app.ts index 1a027000..f9c50efa 100644 --- a/src/vs/code/electron-main/app.ts +++ b/src/vs/code/electron-main/app.ts @@ -124,7 +124,8 @@ import { ExtensionSignatureVerificationService, IExtensionSignatureVerificationS import { LLMMessageChannel } from '../../platform/void/electron-main/llmMessageChannel.js'; import { IMetricsService } from '../../platform/void/common/metricsService.js'; import { MetricsMainService } from '../../platform/void/electron-main/metricsMainService.js'; - +import { VoidMainUpdateService } from '../../platform/void/electron-main/voidUpdateMainService.js'; +import { IVoidUpdateService } from '../../platform/void/common/voidUpdateService.js'; /** * The main VS Code application. There will only ever be one instance, * even if the user starts many instances (e.g. from the command line). @@ -1107,6 +1108,7 @@ export class CodeApplication extends Disposable { // Void main process services (required for services with a channel for comm between browser and electron-main (node)) services.set(IMetricsService, new SyncDescriptor(MetricsMainService, undefined, false)); + services.set(IVoidUpdateService, new SyncDescriptor(VoidMainUpdateService, undefined, false)); // Default Extensions Profile Init services.set(IExtensionsProfileScannerService, new SyncDescriptor(ExtensionsProfileScannerService, undefined, true)); @@ -1245,6 +1247,10 @@ export class CodeApplication extends Disposable { // Void - use loggerChannel as reference const metricsChannel = ProxyChannel.fromService(accessor.get(IMetricsService), disposables); mainProcessElectronServer.registerChannel('void-channel-metrics', metricsChannel); + + const voidUpdatesChannel = ProxyChannel.fromService(accessor.get(IVoidUpdateService), disposables); + mainProcessElectronServer.registerChannel('void-channel-update', voidUpdatesChannel); + const llmMessageChannel = new LLMMessageChannel(accessor.get(IMetricsService)); mainProcessElectronServer.registerChannel('void-channel-llmMessageService', llmMessageChannel); diff --git a/src/vs/editor/browser/services/inlineDiffService/inlineDiffService.ts b/src/vs/editor/browser/services/inlineDiffService/inlineDiffService.ts index 7c2d1324..ece69c58 100644 --- a/src/vs/editor/browser/services/inlineDiffService/inlineDiffService.ts +++ b/src/vs/editor/browser/services/inlineDiffService/inlineDiffService.ts @@ -6,6 +6,11 @@ import { ICodeEditor, IViewZone } from '../../editorBrowser.js'; import { IRange } from '../../../common/core/range.js'; import { EditorOption } from '../../../common/config/editorOptions.js'; + +// THIS FILE IS OLD + UNUSED!!! + +// SEE inlineDiffsService.ts INSTEAD. + export interface IInlineDiffService { readonly _serviceBrand: undefined; addDiff(editor: ICodeEditor, originalText: string, modifiedRange: IRange): void; diff --git a/src/vs/editor/common/services/editorSimpleWorker.ts b/src/vs/editor/common/services/editorSimpleWorker.ts index ff8bbb85..9bc3a7f1 100644 --- a/src/vs/editor/common/services/editorSimpleWorker.ts +++ b/src/vs/editor/common/services/editorSimpleWorker.ts @@ -223,6 +223,11 @@ export class BaseEditorSimpleWorker implements IDisposable, IWorkerTextModelSync private static readonly _diffLimit = 100000; public async $computeMoreMinimalEdits(modelUrl: string, edits: TextEdit[], pretty: boolean): Promise { + return this.$Void_computeMoreMinimalEdits(modelUrl, edits, pretty) + } + + // Void added this as non async + public $Void_computeMoreMinimalEdits(modelUrl: string, edits: TextEdit[], pretty: boolean): TextEdit[] { const model = this._getModel(modelUrl); if (!model) { return edits; diff --git a/src/vs/platform/extensions/common/extensionValidator.ts b/src/vs/platform/extensions/common/extensionValidator.ts index 4a9d3295..e9736b22 100644 --- a/src/vs/platform/extensions/common/extensionValidator.ts +++ b/src/vs/platform/extensions/common/extensionValidator.ts @@ -403,6 +403,7 @@ function isVersionValid(currentVersion: string, date: ProductDate, requestedVers } if (!isValidVersion(currentVersion, date, desiredVersion)) { + // Void - ignore not compatible notices.push(nls.localize('versionMismatch', "Extension is not compatible with Code {0}. Extension requires: {1}.", currentVersion, requestedVersion)); return false; } diff --git a/src/vs/platform/keybinding/common/keybindingsRegistry.ts b/src/vs/platform/keybinding/common/keybindingsRegistry.ts index eeabcc55..e83230ae 100644 --- a/src/vs/platform/keybinding/common/keybindingsRegistry.ts +++ b/src/vs/platform/keybinding/common/keybindingsRegistry.ts @@ -64,7 +64,8 @@ export const enum KeybindingWeight { EditorContrib = 100, WorkbenchContrib = 200, BuiltinExtension = 300, - ExternalExtension = 400 + ExternalExtension = 400, + VoidExtension = 605, // Void - must trump any external extension } export interface ICommandAndKeybindingRule extends IKeybindingRule { diff --git a/src/vs/platform/telemetry/common/serverTelemetryService.ts b/src/vs/platform/telemetry/common/serverTelemetryService.ts index f6fc225a..8839ecf7 100644 --- a/src/vs/platform/telemetry/common/serverTelemetryService.ts +++ b/src/vs/platform/telemetry/common/serverTelemetryService.ts @@ -31,25 +31,29 @@ export class ServerTelemetryService extends TelemetryService implements IServerT } override publicLog(eventName: string, data?: ITelemetryData) { - if (this._injectedTelemetryLevel < TelemetryLevel.USAGE) { - return; - } - return super.publicLog(eventName, data); + // Void commented this out + // if (this._injectedTelemetryLevel < TelemetryLevel.USAGE) { + // return; + // } + // return super.publicLog(eventName, data); } override publicLog2> = never, T extends IGDPRProperty = never>(eventName: string, data?: StrictPropertyCheck) { - return this.publicLog(eventName, data as ITelemetryData | undefined); + // Void commented this out + // return this.publicLog(eventName, data as ITelemetryData | undefined); } override publicLogError(errorEventName: string, data?: ITelemetryData) { - if (this._injectedTelemetryLevel < TelemetryLevel.ERROR) { - return Promise.resolve(undefined); - } - return super.publicLogError(errorEventName, data); + // Void commented this out + // if (this._injectedTelemetryLevel < TelemetryLevel.ERROR) { + // return Promise.resolve(undefined); + // } + // return super.publicLogError(errorEventName, data); } override publicLogError2> = never, T extends IGDPRProperty = never>(eventName: string, data?: StrictPropertyCheck) { - return this.publicLogError(eventName, data as ITelemetryData | undefined); + // Void commented this out + // return this.publicLogError(eventName, data as ITelemetryData | undefined); } async updateInjectedTelemetryLevel(telemetryLevel: TelemetryLevel): Promise { diff --git a/src/vs/platform/telemetry/common/telemetryService.ts b/src/vs/platform/telemetry/common/telemetryService.ts index 51fbb8fa..eb04c7a1 100644 --- a/src/vs/platform/telemetry/common/telemetryService.ts +++ b/src/vs/platform/telemetry/common/telemetryService.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { DisposableStore } from '../../../base/common/lifecycle.js'; -import { mixin } from '../../../base/common/objects.js'; +// import { mixin } from '../../../base/common/objects.js'; import { isWeb } from '../../../base/common/platform.js'; import { escapeRegExpCharacters } from '../../../base/common/strings.js'; import { localize } from '../../../nls.js'; @@ -15,7 +15,8 @@ import { IProductService } from '../../product/common/productService.js'; import { Registry } from '../../registry/common/platform.js'; import { ClassifiedEvent, IGDPRProperty, OmitMetadata, StrictPropertyCheck } from './gdprTypings.js'; import { ITelemetryData, ITelemetryService, TelemetryConfiguration, TelemetryLevel, TELEMETRY_CRASH_REPORTER_SETTING_ID, TELEMETRY_OLD_SETTING_ID, TELEMETRY_SECTION_ID, TELEMETRY_SETTING_ID, ICommonProperties } from './telemetry.js'; -import { cleanData, getTelemetryLevel, ITelemetryAppender } from './telemetryUtils.js'; +import { getTelemetryLevel, ITelemetryAppender } from './telemetryUtils.js'; +// import { cleanData } from './telemetryUtils.js'; export interface ITelemetryServiceConfig { appenders: ITelemetryAppender[]; @@ -38,7 +39,7 @@ export class TelemetryService implements ITelemetryService { readonly firstSessionDate: string; readonly msftInternal: boolean | undefined; - private _appenders: ITelemetryAppender[]; + // private _appenders: ITelemetryAppender[]; private _commonProperties: ICommonProperties; private _experimentProperties: { [name: string]: string } = {}; private _piiPaths: string[]; @@ -53,7 +54,7 @@ export class TelemetryService implements ITelemetryService { @IConfigurationService private _configurationService: IConfigurationService, @IProductService private _productService: IProductService ) { - this._appenders = config.appenders; + // this._appenders = config.appenders; this._commonProperties = config.commonProperties ?? Object.create(null); this.sessionId = this._commonProperties['sessionID'] as string; @@ -121,44 +122,47 @@ export class TelemetryService implements ITelemetryService { this._disposables.dispose(); } - private _log(eventName: string, eventLevel: TelemetryLevel, data?: ITelemetryData) { - // don't send events when the user is optout - if (this._telemetryLevel < eventLevel) { - return; - } + // Void commented this out + // private _log(eventName: string, eventLevel: TelemetryLevel, data?: ITelemetryData) { + // // don't send events when the user is optout + // if (this._telemetryLevel < eventLevel) { + // return; + // } - // add experiment properties - data = mixin(data, this._experimentProperties); + // // add experiment properties + // data = mixin(data, this._experimentProperties); - // remove all PII from data - data = cleanData(data as Record, this._cleanupPatterns); + // // remove all PII from data + // data = cleanData(data as Record, this._cleanupPatterns); - // add common properties - data = mixin(data, this._commonProperties); + // // add common properties + // data = mixin(data, this._commonProperties); - // Log to the appenders of sufficient level - this._appenders.forEach(a => a.log(eventName, data)); - } + // // Log to the appenders of sufficient level + // this._appenders.forEach(a => a.log(eventName, data)); + // } publicLog(eventName: string, data?: ITelemetryData) { - this._log(eventName, TelemetryLevel.USAGE, data); + // this._log(eventName, TelemetryLevel.USAGE, data); } publicLog2> = never, T extends IGDPRProperty = never>(eventName: string, data?: StrictPropertyCheck) { - this.publicLog(eventName, data as ITelemetryData); + // this.publicLog(eventName, data as ITelemetryData); } publicLogError(errorEventName: string, data?: ITelemetryData) { - if (!this._sendErrorTelemetry) { - return; - } + // Void commented this out + // if (!this._sendErrorTelemetry) { + // return; + // } - // Send error event and anonymize paths - this._log(errorEventName, TelemetryLevel.ERROR, data); + // // Send error event and anonymize paths + // this._log(errorEventName, TelemetryLevel.ERROR, data); } publicLogError2> = never, T extends IGDPRProperty = never>(eventName: string, data?: StrictPropertyCheck) { - this.publicLogError(eventName, data as ITelemetryData); + // Void commented this out + // this.publicLogError(eventName, data as ITelemetryData); } } diff --git a/src/vs/platform/update/electron-main/abstractUpdateService.ts b/src/vs/platform/update/electron-main/abstractUpdateService.ts index 48638aa1..92707e34 100644 --- a/src/vs/platform/update/electron-main/abstractUpdateService.ts +++ b/src/vs/platform/update/electron-main/abstractUpdateService.ts @@ -3,11 +3,12 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -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'; import { IEnvironmentMainService } from '../../environment/electron-main/environmentMainService.js'; +// import { IEnvironmentMainService } from '../../environment/electron-main/environmentMainService.js'; import { ILifecycleMainService, LifecycleMainPhase } from '../../lifecycle/electron-main/lifecycleMainService.js'; import { ILogService } from '../../log/common/log.js'; import { IProductService } from '../../product/common/productService.js'; @@ -15,7 +16,10 @@ import { IRequestService } from '../../request/common/request.js'; import { AvailableForDownload, DisablementReason, IUpdateService, State, StateType, UpdateType } from '../common/update.js'; export function createUpdateURL(platform: string, quality: string, productService: IProductService): string { - return `${productService.updateUrl}/api/update/${platform}/${quality}/${productService.commit}`; + // 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}`; } export type UpdateNotAvailableClassification = { @@ -57,7 +61,7 @@ export abstract class AbstractUpdateService implements IUpdateService { @IEnvironmentMainService private readonly environmentMainService: IEnvironmentMainService, @IRequestService protected requestService: IRequestService, @ILogService protected logService: ILogService, - @IProductService protected readonly productService: IProductService + @IProductService protected readonly productService: IProductService, ) { lifecycleMainService.when(LifecycleMainPhase.AfterWindowOpen) .finally(() => this.initialize()); @@ -70,75 +74,35 @@ export abstract class AbstractUpdateService implements IUpdateService { */ protected async initialize(): Promise { if (!this.environmentMainService.isBuilt) { + console.log('is NOT built, canceling update service') this.setState(State.Disabled(DisablementReason.NotBuilt)); return; // updates are never enabled when running out of sources } + console.log('is built, continuing with update service') - if (this.environmentMainService.disableUpdates) { - this.setState(State.Disabled(DisablementReason.DisabledByEnvironment)); - this.logService.info('update#ctor - updates are disabled by the environment'); - return; - } - - if (!this.productService.updateUrl || !this.productService.commit) { - this.setState(State.Disabled(DisablementReason.MissingConfiguration)); - this.logService.info('update#ctor - updates are disabled as there is no update URL'); - return; - } - - const updateMode = this.configurationService.getValue<'none' | 'manual' | 'start' | 'default'>('update.mode'); - const quality = this.getProductQuality(updateMode); - - if (!quality) { - this.setState(State.Disabled(DisablementReason.ManuallyDisabled)); - this.logService.info('update#ctor - updates are disabled by user preference'); - return; - } - - this.url = this.buildUpdateFeedUrl(quality); + this.url = this.doBuildUpdateFeedUrl('stable'); if (!this.url) { this.setState(State.Disabled(DisablementReason.InvalidConfiguration)); this.logService.info('update#ctor - updates are disabled as the update URL is badly formed'); return; } - // hidden setting - if (this.configurationService.getValue('_update.prss')) { - const url = new URL(this.url); - url.searchParams.set('prss', 'true'); - this.url = url.toString(); - } + this.setState(State.Disabled(DisablementReason.ManuallyDisabled)); - this.setState(State.Idle(this.getUpdateType())); - if (updateMode === 'manual') { - this.logService.info('update#ctor - manual checks only; automatic updates are disabled by user preference'); - return; - } + // Void - temporarily disabled while we figure out how to do this the right way - if (updateMode === 'start') { - this.logService.info('update#ctor - startup checks only; automatic updates are disabled by user preference'); + // this.setState(State.Idle(this.getUpdateType())); - // Check for updates only once after 30 seconds - setTimeout(() => this.checkForUpdates(false), 30 * 1000); - } else { - // Start checking for updates after 30 seconds - this.scheduleCheckForUpdates(30 * 1000).then(undefined, err => this.logService.error(err)); - } + // start checking for updates after 10 seconds + // this.scheduleCheckForUpdates(10 * 1000).then(undefined, err => this.logService.error(err)); } - private getProductQuality(updateMode: string): string | undefined { - return updateMode === 'none' ? undefined : this.productService.quality; - } - - private scheduleCheckForUpdates(delay = 60 * 60 * 1000): Promise { - return timeout(delay) - .then(() => this.checkForUpdates(false)) - .then(() => { - // Check again after 1 hour - return this.scheduleCheckForUpdates(60 * 60 * 1000); - }); - } + // private async scheduleCheckForUpdates(delay = 60 * 60 * 1000): Promise { + // await timeout(delay); + // await this.checkForUpdates(false); + // return await this.scheduleCheckForUpdates(60 * 60 * 1000); + // } async checkForUpdates(explicit: boolean): Promise { this.logService.trace('update#checkForUpdates, state = ', this.state.type); @@ -160,6 +124,7 @@ export abstract class AbstractUpdateService implements IUpdateService { await this.doDownloadUpdate(this.state); } + // override implemented by windows and linux protected async doDownloadUpdate(state: AvailableForDownload): Promise { // noop } @@ -174,6 +139,7 @@ export abstract class AbstractUpdateService implements IUpdateService { await this.doApplyUpdate(); } + // windows overrides this protected async doApplyUpdate(): Promise { // noop } @@ -236,6 +202,6 @@ export abstract class AbstractUpdateService implements IUpdateService { // noop } - protected abstract buildUpdateFeedUrl(quality: string): string | undefined; + protected abstract doBuildUpdateFeedUrl(quality: string): string | undefined; protected abstract doCheckForUpdates(context: any): void; } diff --git a/src/vs/platform/update/electron-main/updateService.darwin.ts b/src/vs/platform/update/electron-main/updateService.darwin.ts index d3f27d37..5b00195a 100644 --- a/src/vs/platform/update/electron-main/updateService.darwin.ts +++ b/src/vs/platform/update/electron-main/updateService.darwin.ts @@ -34,7 +34,7 @@ export class DarwinUpdateService extends AbstractUpdateService implements IRelau @IEnvironmentMainService environmentMainService: IEnvironmentMainService, @IRequestService requestService: IRequestService, @ILogService logService: ILogService, - @IProductService productService: IProductService + @IProductService productService: IProductService, ) { super(lifecycleMainService, configurationService, environmentMainService, requestService, logService, productService); @@ -73,7 +73,7 @@ export class DarwinUpdateService extends AbstractUpdateService implements IRelau this.setState(State.Idle(UpdateType.Archive, message)); } - protected buildUpdateFeedUrl(quality: string): string | undefined { + protected doBuildUpdateFeedUrl(quality: string): string | undefined { let assetID: string; if (!this.productService.darwinUniversalAssetId) { assetID = process.arch === 'x64' ? 'darwin' : 'darwin-arm64'; diff --git a/src/vs/platform/update/electron-main/updateService.linux.ts b/src/vs/platform/update/electron-main/updateService.linux.ts index 6e076c72..b01840c5 100644 --- a/src/vs/platform/update/electron-main/updateService.linux.ts +++ b/src/vs/platform/update/electron-main/updateService.linux.ts @@ -30,7 +30,7 @@ export class LinuxUpdateService extends AbstractUpdateService { super(lifecycleMainService, configurationService, environmentMainService, requestService, logService, productService); } - protected buildUpdateFeedUrl(quality: string): string { + protected doBuildUpdateFeedUrl(quality: string): string { return createUpdateURL(`linux-${process.arch}`, quality, this.productService); } diff --git a/src/vs/platform/update/electron-main/updateService.win32.ts b/src/vs/platform/update/electron-main/updateService.win32.ts index 61109e54..0e2396a6 100644 --- a/src/vs/platform/update/electron-main/updateService.win32.ts +++ b/src/vs/platform/update/electron-main/updateService.win32.ts @@ -67,7 +67,7 @@ export class Win32UpdateService extends AbstractUpdateService implements IRelaun @ILogService logService: ILogService, @IFileService private readonly fileService: IFileService, @INativeHostMainService private readonly nativeHostMainService: INativeHostMainService, - @IProductService productService: IProductService + @IProductService productService: IProductService, ) { super(lifecycleMainService, configurationService, environmentMainService, requestService, logService, productService); @@ -99,7 +99,7 @@ export class Win32UpdateService extends AbstractUpdateService implements IRelaun await super.initialize(); } - protected buildUpdateFeedUrl(quality: string): string | undefined { + protected doBuildUpdateFeedUrl(quality: string): string | undefined { let platform = `win32-${process.arch}`; if (getUpdateType() === UpdateType.Archive) { diff --git a/src/vs/platform/void/browser/void.contribution.ts b/src/vs/platform/void/browser/void.contribution.ts index 2d261ea3..276d6e72 100644 --- a/src/vs/platform/void/browser/void.contribution.ts +++ b/src/vs/platform/void/browser/void.contribution.ts @@ -1,4 +1,7 @@ - +/*-------------------------------------------------------------------------------------- + * Copyright 2025 Glass Devtools, Inc. All rights reserved. + * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. + *--------------------------------------------------------------------------------------*/ // ---------- common ---------- @@ -13,3 +16,6 @@ import '../common/refreshModelService.js' // metrics import '../common/metricsService.js' + +// updates +import '../common/voidUpdateService.js' diff --git a/src/vs/platform/void/common/llmMessageService.ts b/src/vs/platform/void/common/llmMessageService.ts index 606f4487..caaeb0c8 100644 --- a/src/vs/platform/void/common/llmMessageService.ts +++ b/src/vs/platform/void/common/llmMessageService.ts @@ -1,7 +1,7 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Glass Devtools, Inc. All rights reserved. - * Void Editor additions licensed under the AGPL 3.0 License. - *--------------------------------------------------------------------------------------------*/ +/*-------------------------------------------------------------------------------------- + * Copyright 2025 Glass Devtools, Inc. All rights reserved. + * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. + *--------------------------------------------------------------------------------------*/ import { EventLLMMessageOnTextParams, EventLLMMessageOnErrorParams, EventLLMMessageOnFinalMessageParams, ServiceSendLLMMessageParams, MainLLMMessageParams, MainLLMMessageAbortParams, ServiceModelListParams, EventModelListOnSuccessParams, EventModelListOnErrorParams, MainModelListParams, OllamaModelResponse, OpenaiCompatibleModelResponse, } from './llmMessageTypes.js'; import { IChannel } from '../../../base/parts/ipc/common/ipc.js'; @@ -52,6 +52,7 @@ export class LLMMessageService extends Disposable implements ILLMMessageService super() // const service = ProxyChannel.toService(mainProcessService.getChannel('void-channel-sendLLMMessage')); // lets you call it like a service + // see llmMessageChannel.ts this.channel = this.mainProcessService.getChannel('void-channel-llmMessageService') // .listen sets up an IPC channel and takes a few ms, so we set up listeners immediately and add hooks to them instead @@ -64,18 +65,18 @@ export class LLMMessageService extends Disposable implements ILLMMessageService this._onRequestIdDone(e.requestId) })) this._register((this.channel.listen('onError_llm') satisfies Event)(e => { - console.log('Error in LLMMessageService:', JSON.stringify(e)) + console.error('Error in LLMMessageService:', JSON.stringify(e)) this.onErrorHooks_llm[e.requestId]?.(e) this._onRequestIdDone(e.requestId) })) - // ollama + // ollama .list() this._register((this.channel.listen('onSuccess_ollama') satisfies Event>)(e => { this.onSuccess_ollama[e.requestId]?.(e) })) this._register((this.channel.listen('onError_ollama') satisfies Event>)(e => { this.onError_ollama[e.requestId]?.(e) })) - // openaiCompatible + // openaiCompatible .list() this._register((this.channel.listen('onSuccess_openAICompatible') satisfies Event>)(e => { this.onSuccess_openAICompatible[e.requestId]?.(e) })) @@ -87,7 +88,7 @@ export class LLMMessageService extends Disposable implements ILLMMessageService sendLLMMessage(params: ServiceSendLLMMessageParams) { const { onText, onFinalMessage, onError, ...proxyParams } = params; - const { featureName } = proxyParams + const { useProviderFor: featureName } = proxyParams // end early if no provider const modelSelection = this.voidSettingsService.state.modelSelectionOfFeature[featureName] @@ -97,6 +98,10 @@ export class LLMMessageService extends Disposable implements ILLMMessageService } const { providerName, modelName } = modelSelection + const aiInstructions = this.voidSettingsService.state.globalSettings.aiInstructions + if (aiInstructions) + proxyParams.messages.unshift({ role: 'system', content: aiInstructions }) + // add state for request id const requestId_ = generateUuid(); this.onTextHooks_llm[requestId_] = onText diff --git a/src/vs/platform/void/common/llmMessageTypes.ts b/src/vs/platform/void/common/llmMessageTypes.ts index 5bc92e24..f14e82a6 100644 --- a/src/vs/platform/void/common/llmMessageTypes.ts +++ b/src/vs/platform/void/common/llmMessageTypes.ts @@ -1,12 +1,25 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Glass Devtools, Inc. All rights reserved. - * Void Editor additions licensed under the AGPL 3.0 License. - *--------------------------------------------------------------------------------------------*/ +/*-------------------------------------------------------------------------------------- + * Copyright 2025 Glass Devtools, Inc. All rights reserved. + * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. + *--------------------------------------------------------------------------------------*/ import { IRange } from '../../../editor/common/core/range' import { ProviderName, SettingsOfProvider } from './voidSettingsTypes.js' +export const errorDetails = (fullError: Error | null): string | null => { + if (fullError === null) { + return null + } + else if (typeof fullError === 'object') { + return JSON.stringify(fullError, null, 2) + } + else if (typeof fullError === 'string') { + return null + } + return null +} + export type OnText = (p: { newText: string, fullText: string }) => void export type OnFinalMessage = (p: { fullText: string }) => void export type OnError = (p: { message: string, fullError: Error | null }) => void @@ -17,14 +30,14 @@ export type LLMMessage = { content: string; } -export type LLMFeatureSelection = { - featureName: 'Ctrl+K', - range: IRange +export type ServiceSendLLMFeatureParams = { + useProviderFor: 'Ctrl+K'; + range: IRange; } | { - featureName: 'Ctrl+L', + useProviderFor: 'Ctrl+L'; } | { - featureName: 'Autocomplete', - range: IRange + useProviderFor: 'Autocomplete'; + range: IRange; } // params to the true sendLLMMessage function @@ -54,7 +67,7 @@ export type ServiceSendLLMMessageParams = { logging: { loggingName: string, }; -} & LLMFeatureSelection +} & ServiceSendLLMFeatureParams // can't send functions across a proxy, use listeners instead export type BlockedMainLLMMessageParams = 'onText' | 'onFinalMessage' | 'onError' | 'abortRef' diff --git a/src/vs/platform/void/common/metricsService.ts b/src/vs/platform/void/common/metricsService.ts index 039697a0..a3aeb6a8 100644 --- a/src/vs/platform/void/common/metricsService.ts +++ b/src/vs/platform/void/common/metricsService.ts @@ -1,16 +1,21 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Glass Devtools, Inc. All rights reserved. - * Void Editor additions licensed under the AGPL 3.0 License. - *--------------------------------------------------------------------------------------------*/ +/*-------------------------------------------------------------------------------------- + * Copyright 2025 Glass Devtools, Inc. All rights reserved. + * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. + *--------------------------------------------------------------------------------------*/ import { createDecorator } from '../../instantiation/common/instantiation.js'; import { ProxyChannel } from '../../../base/parts/ipc/common/ipc.js'; import { IMainProcessService } from '../../ipc/common/mainProcessService.js'; import { InstantiationType, registerSingleton } from '../../instantiation/common/extensions.js'; +import { Action2, registerAction2 } from '../../actions/common/actions.js'; +import { localize2 } from '../../../nls.js'; +import { ServicesAccessor } from '../../../editor/browser/editorExtensions.js'; +import { INotificationService } from '../../notification/common/notification.js'; export interface IMetricsService { readonly _serviceBrand: undefined; capture(event: string, params: Record): void; + getDebuggingProperties(): Promise; } export const IMetricsService = createDecorator('metricsService'); @@ -25,6 +30,7 @@ export class MetricsService implements IMetricsService { constructor( @IMainProcessService mainProcessService: IMainProcessService // (only usable on client side) ) { + // creates an IPC proxy to use metricsMainService.ts this.metricsService = ProxyChannel.toService(mainProcessService.getChannel('void-channel-metrics')); } @@ -33,7 +39,30 @@ export class MetricsService implements IMetricsService { this.metricsService.capture(...params); } + // anything transmitted over a channel must be async even if it looks like it doesn't have to be + async getDebuggingProperties(): Promise { + return this.metricsService.getDebuggingProperties() + } } registerSingleton(IMetricsService, MetricsService, InstantiationType.Eager); + +// debugging action +registerAction2(class extends Action2 { + constructor() { + super({ + id: 'voidDebugInfo', + f1: true, + title: localize2('voidMetricsDebug', 'Void: Log Debug Info'), + }); + } + async run(accessor: ServicesAccessor): Promise { + const metricsService = accessor.get(IMetricsService) + const notifService = accessor.get(INotificationService) + + const debugProperties = await metricsService.getDebuggingProperties() + console.log('Metrics:', debugProperties) + notifService.info(`Void Debug info:\n${JSON.stringify(debugProperties, null, 2)}`) + } +}) diff --git a/src/vs/platform/void/common/refreshModelService.ts b/src/vs/platform/void/common/refreshModelService.ts index e552e6d0..811db0db 100644 --- a/src/vs/platform/void/common/refreshModelService.ts +++ b/src/vs/platform/void/common/refreshModelService.ts @@ -1,7 +1,7 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Glass Devtools, Inc. All rights reserved. - * Void Editor additions licensed under the AGPL 3.0 License. - *--------------------------------------------------------------------------------------------*/ +/*-------------------------------------------------------------------------------------- + * Copyright 2025 Glass Devtools, Inc. All rights reserved. + * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. + *--------------------------------------------------------------------------------------*/ import { createDecorator } from '../../instantiation/common/instantiation.js'; import { InstantiationType, registerSingleton } from '../../instantiation/common/extensions.js'; @@ -9,36 +9,48 @@ import { IVoidSettingsService } from './voidSettingsService.js'; import { ILLMMessageService } from './llmMessageService.js'; import { Emitter, Event } from '../../../base/common/event.js'; import { Disposable, IDisposable } from '../../../base/common/lifecycle.js'; -import { ProviderName, SettingsOfProvider } from './voidSettingsTypes.js'; +import { RefreshableProviderName, refreshableProviderNames, SettingsOfProvider } from './voidSettingsTypes.js'; import { OllamaModelResponse, OpenaiCompatibleModelResponse } from './llmMessageTypes.js'; -export const refreshableProviderNames = ['ollama', 'openAICompatible'] satisfies ProviderName[] - -export type RefreshableProviderName = typeof refreshableProviderNames[number] -type RefreshableState = { +type RefreshableState = ({ state: 'init', timeoutId: null, } | { state: 'refreshing', - timeoutId: NodeJS.Timeout | null, + timeoutId: NodeJS.Timeout | null, // the timeoutId of the most recent call to refreshModels } | { - state: 'success', + state: 'finished', timeoutId: null, -} +} | { + state: 'error', + timeoutId: null, +}) +/* + +user click -> error -> fire(error) + \> success -> fire(success) + finally: keep polling + +poll -> do not fire + +*/ export type RefreshModelStateOfProvider = Record const refreshBasedOn: { [k in RefreshableProviderName]: (keyof SettingsOfProvider[k])[] } = { - ollama: ['enabled', 'endpoint'], - openAICompatible: ['enabled', 'endpoint', 'apiKey'], + ollama: ['_enabled', 'endpoint'], + // openAICompatible: ['_enabled', 'endpoint', 'apiKey'], } -const REFRESH_INTERVAL = 5000 +const REFRESH_INTERVAL = 5_000 +// const COOLDOWN_TIMEOUT = 300 + +const autoOptions = { enableProviderOnSuccess: true, doNotFire: true } // element-wise equals function eq(a: T[], b: T[]): boolean { @@ -50,7 +62,7 @@ function eq(a: T[], b: T[]): boolean { } export interface IRefreshModelService { readonly _serviceBrand: undefined; - refreshModels: (providerName: RefreshableProviderName) => Promise; + startRefreshingModels: (providerName: RefreshableProviderName, options: { enableProviderOnSuccess: boolean, doNotFire: boolean }) => void; onDidChangeState: Event; state: RefreshModelStateOfProvider; } @@ -64,6 +76,7 @@ export class RefreshModelService extends Disposable implements IRefreshModelServ private readonly _onDidChangeState = new Emitter(); readonly onDidChangeState: Event = this._onDidChangeState.event; // this is primarily for use in react, so react can listen + update on state changes + constructor( @IVoidSettingsService private readonly voidSettingsService: IVoidSettingsService, @ILLMMessageService private readonly llmMessageService: ILLMMessageService, @@ -73,31 +86,41 @@ export class RefreshModelService extends Disposable implements IRefreshModelServ const disposables: Set = new Set() - - const startRefreshing = () => { + const initializeAutoPollingAndOnChange = () => { this._clearAllTimeouts() disposables.forEach(d => d.dispose()) disposables.clear() - if (!voidSettingsService.state.featureFlagSettings.autoRefreshModels) return + if (!voidSettingsService.state.globalSettings.autoRefreshModels) return for (const providerName of refreshableProviderNames) { - const refresh = () => { - // const { enabled } = this.voidSettingsService.state.settingsOfProvider[providerName] - this.refreshModels(providerName, { enableProviderOnSuccess: true }) // enable the provider on success - } - - refresh() + // const { _enabled: enabled } = this.voidSettingsService.state.settingsOfProvider[providerName] + this.startRefreshingModels(providerName, autoOptions) // every time providerName.enabled changes, refresh models too, like a useEffect - let relevantVals = () => refreshBasedOn[providerName].map(settingName => this.voidSettingsService.state.settingsOfProvider[providerName][settingName]) + let relevantVals = () => refreshBasedOn[providerName].map(settingName => voidSettingsService.state.settingsOfProvider[providerName][settingName]) let prevVals = relevantVals() // each iteration of a for loop has its own context and vars, so this is ok disposables.add( - this.voidSettingsService.onDidChangeState(() => { // we might want to debounce this + voidSettingsService.onDidChangeState(() => { // we might want to debounce this const newVals = relevantVals() if (!eq(prevVals, newVals)) { - refresh() + + const prevEnabled = prevVals[0] as boolean + const enabled = newVals[0] as boolean + + // if it was just enabled, or there was a change and it wasn't to the enabled state, refresh + if ((enabled && !prevEnabled) || (!enabled && !prevEnabled)) { + // if user just clicked enable, refresh + this.startRefreshingModels(providerName, autoOptions) + } + else { + // else if user just clicked disable, don't refresh + + // //give cooldown before re-enabling (or at least re-fetching) + // const timeoutId = setTimeout(() => this.refreshModels(providerName, !enabled), COOLDOWN_TIMEOUT) + // this._setTimeoutId(providerName, timeoutId) + } prevVals = newVals } }) @@ -105,11 +128,11 @@ export class RefreshModelService extends Disposable implements IRefreshModelServ } } - // on mount (when get init settings state), and if a relevant feature flag changes (detected natively right now by refreshing if any flag changes), start refreshing models + // on mount (when get init settings state), and if a relevant feature flag changes, start refreshing models voidSettingsService.waitForInitState.then(() => { - startRefreshing() + initializeAutoPollingAndOnChange() this._register( - voidSettingsService.onDidChangeState((type) => { if (type === 'featureFlagSettings') startRefreshing() }) + voidSettingsService.onDidChangeState((type) => { if (typeof type === 'object' && type[1] === 'autoRefreshModels') initializeAutoPollingAndOnChange() }) ) }) @@ -117,41 +140,53 @@ export class RefreshModelService extends Disposable implements IRefreshModelServ state: RefreshModelStateOfProvider = { ollama: { state: 'init', timeoutId: null }, - openAICompatible: { state: 'init', timeoutId: null }, } // start listening for models (and don't stop until success) - async refreshModels(providerName: RefreshableProviderName, options?: { enableProviderOnSuccess?: boolean }) { + startRefreshingModels: IRefreshModelService['startRefreshingModels'] = (providerName, options) => { + this._clearProviderTimeout(providerName) - // start loading models - this._setRefreshState(providerName, 'refreshing') + this._setRefreshState(providerName, 'refreshing', options) - const fn = providerName === 'ollama' ? this.llmMessageService.ollamaList + const autoPoll = () => { + if (this.voidSettingsService.state.globalSettings.autoRefreshModels) { + // resume auto-polling + const timeoutId = setTimeout(() => this.startRefreshingModels(providerName, autoOptions), REFRESH_INTERVAL) + this._setTimeoutId(providerName, timeoutId) + } + } + const listFn = providerName === 'ollama' ? this.llmMessageService.ollamaList : providerName === 'openAICompatible' ? this.llmMessageService.openAICompatibleList : () => { } - fn({ + listFn({ onSuccess: ({ models }) => { - this.voidSettingsService.setDefaultModels(providerName, models.map(model => { - if (providerName === 'ollama') return (model as OllamaModelResponse).name - else if (providerName === 'openAICompatible') return (model as OpenaiCompatibleModelResponse).id - else throw new Error('refreshMode fn: unknown provider', providerName) - })) - if (options?.enableProviderOnSuccess) - this.voidSettingsService.setSettingOfProvider(providerName, 'enabled', true) + // set the models to the detected models + this.voidSettingsService.setAutodetectedModels( + providerName, + models.map(model => { + if (providerName === 'ollama') return (model as OllamaModelResponse).name; + else if (providerName === 'openAICompatible') return (model as OpenaiCompatibleModelResponse).id; + else throw new Error('refreshMode fn: unknown provider', providerName); + }), + { enableProviderOnSuccess: options.enableProviderOnSuccess, hideRefresh: options.doNotFire } + ) - this._setRefreshState(providerName, 'success') + if (options.enableProviderOnSuccess) this.voidSettingsService.setSettingOfProvider(providerName, '_enabled', true) + + this._setRefreshState(providerName, 'finished', options) + autoPoll() }, onError: ({ error }) => { - // poll - console.log('retrying list models:', providerName, error) - const timeoutId = setTimeout(() => this.refreshModels(providerName, options), REFRESH_INTERVAL) - this._setTimeoutId(providerName, timeoutId) + this._setRefreshState(providerName, 'error', options) + autoPoll() } }) + + } _clearAllTimeouts() { @@ -172,7 +207,8 @@ export class RefreshModelService extends Disposable implements IRefreshModelServ this.state[providerName].timeoutId = timeoutId } - private _setRefreshState(providerName: RefreshableProviderName, state: RefreshableState['state']) { + private _setRefreshState(providerName: RefreshableProviderName, state: RefreshableState['state'], options?: { doNotFire: boolean }) { + if (options?.doNotFire) return this.state[providerName].state = state this._onDidChangeState.fire(providerName) } diff --git a/src/vs/platform/void/common/voidSettingsService.ts b/src/vs/platform/void/common/voidSettingsService.ts index 0c0ceb28..ffaa5e72 100644 --- a/src/vs/platform/void/common/voidSettingsService.ts +++ b/src/vs/platform/void/common/voidSettingsService.ts @@ -1,7 +1,7 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Glass Devtools, Inc. All rights reserved. - * Void Editor additions licensed under the AGPL 3.0 License. - *--------------------------------------------------------------------------------------------*/ +/*-------------------------------------------------------------------------------------- + * 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'; @@ -10,10 +10,11 @@ import { IEncryptionService } from '../../encryption/common/encryptionService.js import { registerSingleton, InstantiationType } from '../../instantiation/common/extensions.js'; import { createDecorator } from '../../instantiation/common/instantiation.js'; import { IStorageService, StorageScope, StorageTarget } from '../../storage/common/storage.js'; -import { defaultSettingsOfProvider, FeatureName, ProviderName, ModelSelectionOfFeature, SettingsOfProvider, SettingName, providerNames, ModelSelection, modelSelectionsEqual, featureNames, modelInfoOfDefaultNames, VoidModelInfo, FeatureFlagSettings, FeatureFlagName, defaultFeatureFlagSettings } from './voidSettingsTypes.js'; +import { IMetricsService } from './metricsService.js'; +import { defaultSettingsOfProvider, FeatureName, ProviderName, ModelSelectionOfFeature, SettingsOfProvider, SettingName, providerNames, ModelSelection, modelSelectionsEqual, featureNames, modelInfoOfDefaultNames, VoidModelInfo, GlobalSettings, GlobalSettingName, defaultGlobalSettings } from './voidSettingsTypes.js'; -const STORAGE_KEY = 'void.voidSettingsI' +const STORAGE_KEY = 'void.settingsServiceStorage' type SetSettingOfProviderFn = ( providerName: ProviderName, @@ -27,21 +28,22 @@ type SetModelSelectionOfFeatureFn = ( options?: { doNotApplyEffects?: true } ) => Promise; -type SetFeatureFlagFn = (flagName: FeatureFlagName, newVal: boolean) => void; +type SetGlobalSettingFn = (settingName: T, newVal: GlobalSettings[T]) => void; -export type ModelOption = { text: string, value: ModelSelection } +export type ModelOption = { name: string, selection: ModelSelection } export type VoidSettingsState = { readonly settingsOfProvider: SettingsOfProvider; // optionsOfProvider readonly modelSelectionOfFeature: ModelSelectionOfFeature; // stateOfFeature - readonly featureFlagSettings: FeatureFlagSettings; + readonly globalSettings: GlobalSettings; readonly _modelOptions: ModelOption[] // computed based on the two above items } -type EventProp = Exclude | 'all' +type RealVoidSettings = Exclude +type EventProp = T extends 'globalSettings' ? [T, keyof VoidSettingsState[T]] : T | 'all' export interface IVoidSettingsService { @@ -53,9 +55,9 @@ export interface IVoidSettingsService { setSettingOfProvider: SetSettingOfProviderFn; setModelSelectionOfFeature: SetModelSelectionOfFeatureFn; - setFeatureFlag: SetFeatureFlagFn; + setGlobalSetting: SetGlobalSettingFn; - setDefaultModels(providerName: ProviderName, modelNames: string[]): void; + setAutodetectedModels(providerName: ProviderName, modelNames: string[], logging: object): void; toggleModelHidden(providerName: ProviderName, modelName: string): void; addModel(providerName: ProviderName, modelName: string): void; deleteModel(providerName: ProviderName, modelName: string): boolean; @@ -66,10 +68,10 @@ let _computeModelOptions = (settingsOfProvider: SettingsOfProvider) => { let modelOptions: ModelOption[] = [] for (const providerName of providerNames) { const providerConfig = settingsOfProvider[providerName] - if (!providerConfig.enabled) continue // if disabled, don't display model options + if (!providerConfig._enabled) continue // if disabled, don't display model options for (const { modelName, isHidden } of providerConfig.models) { if (isHidden) continue - modelOptions.push({ text: `${modelName} (${providerName})`, value: { providerName, modelName } }) + modelOptions.push({ name: `${modelName} (${providerName})`, selection: { providerName, modelName } }) } } return modelOptions @@ -80,7 +82,7 @@ const defaultState = () => { const d: VoidSettingsState = { settingsOfProvider: deepClone(defaultSettingsOfProvider), modelSelectionOfFeature: { 'Ctrl+L': null, 'Ctrl+K': null, 'Autocomplete': null }, - featureFlagSettings: deepClone(defaultFeatureFlagSettings), + globalSettings: deepClone(defaultGlobalSettings), _modelOptions: _computeModelOptions(defaultSettingsOfProvider), // computed } return d @@ -100,6 +102,7 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService { constructor( @IStorageService private readonly _storageService: IStorageService, @IEncryptionService private readonly _encryptionService: IEncryptionService, + @IMetricsService private readonly _metricsService: IMetricsService, // could have used this, but it's clearer the way it is (+ slightly different eg StorageTarget.USER) // @ISecretStorageService private readonly _secretStorageService: ISecretStorageService, ) { @@ -148,16 +151,16 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService { } } - const newFeatureFlags = this.state.featureFlagSettings + const newGlobalSettings = this.state.globalSettings // if changed models or enabled a provider, recompute models list - const modelsListChanged = settingName === 'models' || settingName === 'enabled' + const modelsListChanged = settingName === 'models' || settingName === '_enabled' const newModelsList = modelsListChanged ? _computeModelOptions(newSettingsOfProvider) : this.state._modelOptions const newState: VoidSettingsState = { modelSelectionOfFeature: newModelSelectionOfFeature, settingsOfProvider: newSettingsOfProvider, - featureFlagSettings: newFeatureFlags, + globalSettings: newGlobalSettings, _modelOptions: newModelsList, } @@ -169,11 +172,11 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService { for (const featureName of featureNames) { const currentSelection = newModelSelectionOfFeature[featureName] - const selnIdx = currentSelection === null ? -1 : newModelsList.findIndex(m => modelSelectionsEqual(m.value, currentSelection)) + const selnIdx = currentSelection === null ? -1 : newModelsList.findIndex(m => modelSelectionsEqual(m.selection, currentSelection)) if (selnIdx === -1) { if (newModelsList.length !== 0) - this.setModelSelectionOfFeature(featureName, newModelsList[0].value, { doNotApplyEffects: true }) + this.setModelSelectionOfFeature(featureName, newModelsList[0].selection, { doNotApplyEffects: true }) else this.setModelSelectionOfFeature(featureName, null, { doNotApplyEffects: true }) } @@ -185,17 +188,17 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService { } - setFeatureFlag: SetFeatureFlagFn = async (flagName, newVal) => { - const newState = { + setGlobalSetting: SetGlobalSettingFn = async (settingName, newVal) => { + const newState: VoidSettingsState = { ...this.state, - featureFlagSettings: { - ...this.state.featureFlagSettings, - [flagName]: newVal + globalSettings: { + ...this.state.globalSettings, + [settingName]: newVal } } this.state = newState await this._storeState() - this._onDidChangeState.fire('featureFlagSettings') + this._onDidChangeState.fire(['globalSettings', settingName]) } @@ -220,25 +223,45 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService { - setDefaultModels(providerName: ProviderName, newDefaultModelNames: string[]) { + setAutodetectedModels(providerName: ProviderName, newDefaultModelNames: string[], logging: object) { + const { models } = this.state.settingsOfProvider[providerName] - const newDefaultModels = modelInfoOfDefaultNames(newDefaultModelNames) + + const old_names = models.map(m => m.modelName) + + const newDefaultModels = modelInfoOfDefaultNames(newDefaultModelNames, { isAutodetected: true, existingModels: models }) const newModels = [ ...newDefaultModels, ...models.filter(m => !m.isDefault), // keep any non-default models ] + + this.setSettingOfProvider(providerName, 'models', newModels) + + // if the models changed, log it + const new_names = newModels.map(m => m.modelName) + if (!(old_names.length === new_names.length + && old_names.every((_, i) => old_names[i] === new_names[i]) + )) { + this._metricsService.capture('Autodetect Models', { providerName, newModels, ...logging }) + } } toggleModelHidden(providerName: ProviderName, modelName: string) { + + const { models } = this.state.settingsOfProvider[providerName] const modelIdx = models.findIndex(m => m.modelName === modelName) if (modelIdx === -1) return + const newIsHidden = !models[modelIdx].isHidden const newModels: VoidModelInfo[] = [ ...models.slice(0, modelIdx), - { ...models[modelIdx], isHidden: !models[modelIdx].isHidden }, + { ...models[modelIdx], isHidden: newIsHidden }, ...models.slice(modelIdx + 1, Infinity) ] this.setSettingOfProvider(providerName, 'models', newModels) + + this._metricsService.capture('Toggle Model Hidden', { providerName, modelName, newIsHidden }) + } addModel(providerName: ProviderName, modelName: string) { const { models } = this.state.settingsOfProvider[providerName] @@ -249,6 +272,9 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService { { modelName, isDefault: false, isHidden: false } ] this.setSettingOfProvider(providerName, 'models', newModels) + + this._metricsService.capture('Add Model', { providerName, modelName }) + } deleteModel(providerName: ProviderName, modelName: string): boolean { const { models } = this.state.settingsOfProvider[providerName] @@ -259,6 +285,9 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService { ...models.slice(delIdx + 1, Infinity) ] this.setSettingOfProvider(providerName, 'models', newModels) + + this._metricsService.capture('Delete Model', { providerName, modelName }) + return true } diff --git a/src/vs/platform/void/common/voidSettingsTypes.ts b/src/vs/platform/void/common/voidSettingsTypes.ts index 7aa1eb78..2577bfe7 100644 --- a/src/vs/platform/void/common/voidSettingsTypes.ts +++ b/src/vs/platform/void/common/voidSettingsTypes.ts @@ -1,8 +1,8 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Glass Devtools, Inc. All rights reserved. - * Void Editor additions licensed under the AGPL 3.0 License. - *--------------------------------------------------------------------------------------------*/ +/*-------------------------------------------------------------------------------------- + * Copyright 2025 Glass Devtools, Inc. All rights reserved. + * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. + *--------------------------------------------------------------------------------------*/ @@ -11,12 +11,39 @@ export type VoidModelInfo = { modelName: string, isDefault: boolean, // whether or not it's a default for its provider isHidden: boolean, // whether or not the user is hiding it + isAutodetected?: boolean, // whether the model was autodetected by polling } +// creates `modelInfo` from `modelNames` +export const modelInfoOfDefaultNames = (modelNames: string[], options?: { isAutodetected: true, existingModels: VoidModelInfo[] }): VoidModelInfo[] => { + + const { isAutodetected, existingModels } = options ?? {} + + if (!existingModels) { // default settings + + return modelNames.map((modelName, i) => ({ + modelName, + isDefault: true, + isAutodetected: isAutodetected, + isHidden: modelNames.length >= 10 // hide all models if there are a ton of them, and make user enable them individually + })) + + } else { // settings if there are existing models (keep existing `isHidden` property) + + const existingModelsMap: Record = {} + for (const existingModel of existingModels) { + existingModelsMap[existingModel.modelName] = existingModel + } + + return modelNames.map((modelName, i) => ({ + modelName, + isDefault: true, + isAutodetected: isAutodetected, + isHidden: !!existingModelsMap[modelName]?.isHidden, + })) + + } -export const modelInfoOfDefaultNames = (modelNames: string[]): VoidModelInfo[] => { - const isHidden = modelNames.length >= 10 // hide all models if there are a ton of them, and make user enable them individually - return modelNames.map((modelName, i) => ({ modelName, isDefault: true, isHidden })) } // https://docs.anthropic.com/en/docs/about-claude/models @@ -96,7 +123,7 @@ type UnionOfKeys = T extends T ? keyof T : never; -export const customProviderSettings = { +export const defaultProviderSettings = { anthropic: { apiKey: '', }, @@ -110,8 +137,8 @@ export const customProviderSettings = { apiKey: '', }, openAICompatible: { - apiKey: '', endpoint: '', + apiKey: '', }, gemini: { apiKey: '', @@ -121,19 +148,23 @@ export const customProviderSettings = { } } as const +export type ProviderName = keyof typeof defaultProviderSettings +export const providerNames = Object.keys(defaultProviderSettings) as ProviderName[] -export type ProviderName = keyof typeof customProviderSettings -export const providerNames = Object.keys(customProviderSettings) as ProviderName[] +export const localProviderNames = ['ollama'] satisfies ProviderName[] // all local names +export const nonlocalProviderNames = providerNames.filter((name) => !(localProviderNames as string[]).includes(name)) // all non-local names - - -type CustomSettingName = UnionOfKeys +type CustomSettingName = UnionOfKeys type CustomProviderSettings = { - [k in CustomSettingName]: k extends keyof typeof customProviderSettings[providerName] ? string : undefined + [k in CustomSettingName]: k extends keyof typeof defaultProviderSettings[providerName] ? string : undefined +} +export const customSettingNamesOfProvider = (providerName: ProviderName) => { + return Object.keys(defaultProviderSettings[providerName]) as CustomSettingName[] } + type CommonProviderSettings = { - enabled: boolean | undefined, // undefined initially + _enabled: boolean | undefined, // undefined initially, computed when user types in all fields models: VoidModelInfo[], } @@ -150,28 +181,49 @@ export type SettingName = keyof SettingsForProvider -export const customSettingNamesOfProvider = (providerName: ProviderName) => { - return Object.keys(customProviderSettings[providerName]) as CustomSettingName[] + +type DisplayInfoForProviderName = { + title: string, + desc?: string, } +export const displayInfoOfProviderName = (providerName: ProviderName): DisplayInfoForProviderName => { + if (providerName === 'anthropic') { + return { + title: 'Anthropic', + } + } + else if (providerName === 'openAI') { + return { + title: 'OpenAI', + } + } + else if (providerName === 'openRouter') { + return { + title: 'OpenRouter', + } + } + else if (providerName === 'ollama') { + return { + title: 'Ollama', - - -export const titleOfProviderName = (providerName: ProviderName) => { - if (providerName === 'anthropic') - return 'Anthropic' - else if (providerName === 'openAI') - return 'OpenAI' - else if (providerName === 'ollama') - return 'Ollama' - else if (providerName === 'openRouter') - return 'OpenRouter' - else if (providerName === 'openAICompatible') - return 'OpenAI-Compatible' - else if (providerName === 'gemini') - return 'Gemini' - else if (providerName === 'groq') - return 'Groq' + } + } + else if (providerName === 'openAICompatible') { + return { + title: 'OpenAI-Compatible', + } + } + else if (providerName === 'gemini') { + return { + title: 'Gemini', + } + } + else if (providerName === 'groq') { + return { + title: 'Groq', + } + } throw new Error(`descOfProviderName: Unknown provider name: "${providerName}"`) } @@ -179,9 +231,7 @@ export const titleOfProviderName = (providerName: ProviderName) => { type DisplayInfo = { title: string, placeholder: string, - - helpfulUrl?: string, - urlPurpose?: string, + subTextMd?: string, } export const displayInfoOfSettingName = (providerName: ProviderName, settingName: SettingName): DisplayInfo => { if (settingName === 'apiKey') { @@ -195,35 +245,30 @@ export const displayInfoOfSettingName = (providerName: ProviderName, settingName providerName === 'openAICompatible' ? 'sk-key...' : '(never)', - helpfulUrl: providerName === 'anthropic' ? 'https://console.anthropic.com/settings/keys' : - providerName === 'openAI' ? 'https://platform.openai.com/api-keys' : - providerName === 'openRouter' ? 'https://openrouter.ai/settings/keys' : - providerName === 'gemini' ? 'https://aistudio.google.com/apikey' : - providerName === 'groq' ? 'https://console.groq.com/keys' : - providerName === 'openAICompatible' ? undefined : + subTextMd: providerName === 'anthropic' ? 'Get your [API Key here](https://console.anthropic.com/settings/keys).' : + providerName === 'openAI' ? 'Get your [API Key here](https://platform.openai.com/api-keys).' : + providerName === 'openRouter' ? 'Get your [API Key here](https://openrouter.ai/settings/keys).' : + providerName === 'gemini' ? 'Get your [API Key here](https://aistudio.google.com/apikey).' : + providerName === 'groq' ? 'Get your [API Key here](https://console.groq.com/keys).' : + providerName === 'openAICompatible' ? 'Add any OpenAI-Compatible endpoint.' : undefined, - - urlPurpose: 'to get your API key.', } } else if (settingName === 'endpoint') { return { - title: providerName === 'ollama' ? 'Your Ollama endpoint' : + title: providerName === 'ollama' ? 'Endpoint' : providerName === 'openAICompatible' ? 'baseURL' // (do not include /chat/completions) : '(never)', - placeholder: providerName === 'ollama' ? customProviderSettings.ollama.endpoint + placeholder: providerName === 'ollama' ? defaultProviderSettings.ollama.endpoint : providerName === 'openAICompatible' ? 'https://my-website.com/v1' : '(never)', - helpfulUrl: providerName === 'ollama' ? 'https://github.com/ollama/ollama/blob/main/docs/faq.md#how-can-i-expose-ollama-on-my-network' - : providerName === 'openAICompatible' ? undefined - : undefined, - - urlPurpose: 'for more information.', + subTextMd: providerName === 'ollama' ? 'If you would like to change this endpoint, please read more about [Endpoints here](https://github.com/ollama/ollama/blob/main/docs/faq.md#how-can-i-expose-ollama-on-my-network).' : + undefined, } } - else if (settingName === 'enabled') { + else if (settingName === '_enabled') { return { title: '(never)', placeholder: '(never)', @@ -276,46 +321,46 @@ export const voidInitModelOptions = { // used when waiting and for a type reference export const defaultSettingsOfProvider: SettingsOfProvider = { anthropic: { - enabled: undefined, + _enabled: undefined, ...defaultCustomSettings, - ...customProviderSettings.anthropic, + ...defaultProviderSettings.anthropic, ...voidInitModelOptions.anthropic, }, openAI: { - enabled: undefined, + _enabled: undefined, ...defaultCustomSettings, - ...customProviderSettings.openAI, + ...defaultProviderSettings.openAI, ...voidInitModelOptions.openAI, }, gemini: { ...defaultCustomSettings, - ...customProviderSettings.gemini, + ...defaultProviderSettings.gemini, ...voidInitModelOptions.gemini, - enabled: undefined, + _enabled: undefined, }, groq: { ...defaultCustomSettings, - ...customProviderSettings.groq, + ...defaultProviderSettings.groq, ...voidInitModelOptions.groq, - enabled: undefined, + _enabled: undefined, }, ollama: { ...defaultCustomSettings, - ...customProviderSettings.ollama, + ...defaultProviderSettings.ollama, ...voidInitModelOptions.ollama, - enabled: undefined, + _enabled: undefined, }, openRouter: { ...defaultCustomSettings, - ...customProviderSettings.openRouter, + ...defaultProviderSettings.openRouter, ...voidInitModelOptions.openRouter, - enabled: undefined, + _enabled: undefined, }, openAICompatible: { ...defaultCustomSettings, - ...customProviderSettings.openAICompatible, + ...defaultProviderSettings.openAICompatible, ...voidInitModelOptions.openAICompatible, - enabled: undefined, + _enabled: undefined, }, } @@ -341,29 +386,27 @@ export const featureNames = ['Ctrl+L', 'Ctrl+K', 'Autocomplete'] as const +// the models of these can be refreshed (in theory all can, but not all should) +export const refreshableProviderNames = localProviderNames +export type RefreshableProviderName = typeof refreshableProviderNames[number] -export type FeatureFlagSettings = { - autoRefreshModels: boolean; // automatically scan for local models and enable when found + + + + + +export type GlobalSettings = { + autoRefreshModels: boolean; + aiInstructions: string; } -export const defaultFeatureFlagSettings: FeatureFlagSettings = { +export const defaultGlobalSettings: GlobalSettings = { autoRefreshModels: true, + aiInstructions: '', } -export type FeatureFlagName = keyof FeatureFlagSettings -export const featureFlagNames = Object.keys(defaultFeatureFlagSettings) as FeatureFlagName[] - -type FeatureFlagDisplayInfo = { - description: string, -} -export const displayInfoOfFeatureFlag = (featureFlag: FeatureFlagName): FeatureFlagDisplayInfo => { - if (featureFlag === 'autoRefreshModels') { - return { - description: 'Automatically scan for and enable local models.', - } - } - throw new Error(`featureFlagInfo: Unknown feature flag: "${featureFlag}"`) -} +export type GlobalSettingName = keyof GlobalSettings +export const globalSettingNames = Object.keys(defaultGlobalSettings) as GlobalSettingName[] diff --git a/src/vs/platform/void/common/voidUpdateService.ts b/src/vs/platform/void/common/voidUpdateService.ts new file mode 100644 index 00000000..0304073f --- /dev/null +++ b/src/vs/platform/void/common/voidUpdateService.ts @@ -0,0 +1,46 @@ +/*-------------------------------------------------------------------------------------- + * Copyright 2025 Glass Devtools, Inc. All rights reserved. + * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. + *--------------------------------------------------------------------------------------*/ + +import { createDecorator } from '../../instantiation/common/instantiation.js'; +import { ProxyChannel } from '../../../base/parts/ipc/common/ipc.js'; +import { IMainProcessService } from '../../ipc/common/mainProcessService.js'; +import { InstantiationType, registerSingleton } from '../../instantiation/common/extensions.js'; + + + +export interface IVoidUpdateService { + readonly _serviceBrand: undefined; + check: () => Promise<{ hasUpdate: true, message: string } | { hasUpdate: false } | null>; +} + + +export const IVoidUpdateService = createDecorator('VoidUpdateService'); + + +// implemented by calling channel +export class VoidUpdateService implements IVoidUpdateService { + + readonly _serviceBrand: undefined; + private readonly voidUpdateService: IVoidUpdateService; + + constructor( + @IMainProcessService mainProcessService: IMainProcessService, // (only usable on client side) + ) { + // creates an IPC proxy to use metricsMainService.ts + this.voidUpdateService = ProxyChannel.toService(mainProcessService.getChannel('void-channel-update')); + } + + + + // anything transmitted over a channel must be async even if it looks like it doesn't have to be + check: IVoidUpdateService['check'] = async () => { + const res = await this.voidUpdateService.check() + return res + } +} + +registerSingleton(IVoidUpdateService, VoidUpdateService, InstantiationType.Eager); + + diff --git a/src/vs/platform/void/electron-main/llmMessage/anthropic.ts b/src/vs/platform/void/electron-main/llmMessage/anthropic.ts index 19a4ddef..04dcaa21 100644 --- a/src/vs/platform/void/electron-main/llmMessage/anthropic.ts +++ b/src/vs/platform/void/electron-main/llmMessage/anthropic.ts @@ -1,7 +1,7 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Glass Devtools, Inc. All rights reserved. - * Void Editor additions licensed under the AGPL 3.0 License. - *--------------------------------------------------------------------------------------------*/ +/*-------------------------------------------------------------------------------------- + * Copyright 2025 Glass Devtools, Inc. All rights reserved. + * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. + *--------------------------------------------------------------------------------------*/ import Anthropic from '@anthropic-ai/sdk'; import { _InternalSendLLMMessageFnType } from '../../common/llmMessageTypes.js'; diff --git a/src/vs/platform/void/electron-main/llmMessage/gemini.ts b/src/vs/platform/void/electron-main/llmMessage/gemini.ts index 59e0c1c3..557d28c7 100644 --- a/src/vs/platform/void/electron-main/llmMessage/gemini.ts +++ b/src/vs/platform/void/electron-main/llmMessage/gemini.ts @@ -1,7 +1,7 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Glass Devtools, Inc. All rights reserved. - * Void Editor additions licensed under the AGPL 3.0 License. - *--------------------------------------------------------------------------------------------*/ +/*-------------------------------------------------------------------------------------- + * Copyright 2025 Glass Devtools, Inc. All rights reserved. + * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. + *--------------------------------------------------------------------------------------*/ import { Content, GoogleGenerativeAI, GoogleGenerativeAIFetchError } from '@google/generative-ai'; import { _InternalSendLLMMessageFnType } from '../../common/llmMessageTypes.js'; diff --git a/src/vs/platform/void/electron-main/llmMessage/greptile.ts b/src/vs/platform/void/electron-main/llmMessage/greptile.ts index 21ac3f71..f61f87af 100644 --- a/src/vs/platform/void/electron-main/llmMessage/greptile.ts +++ b/src/vs/platform/void/electron-main/llmMessage/greptile.ts @@ -1,7 +1,7 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Glass Devtools, Inc. All rights reserved. - * Void Editor additions licensed under the AGPL 3.0 License. - *--------------------------------------------------------------------------------------------*/ +/*-------------------------------------------------------------------------------------- + * Copyright 2025 Glass Devtools, Inc. All rights reserved. + * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. + *--------------------------------------------------------------------------------------*/ // // Greptile // // https://docs.greptile.com/api-reference/query diff --git a/src/vs/platform/void/electron-main/llmMessage/groq.ts b/src/vs/platform/void/electron-main/llmMessage/groq.ts index 1b5918a7..1050e25c 100644 --- a/src/vs/platform/void/electron-main/llmMessage/groq.ts +++ b/src/vs/platform/void/electron-main/llmMessage/groq.ts @@ -1,7 +1,7 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Glass Devtools, Inc. All rights reserved. - * Void Editor additions licensed under the AGPL 3.0 License. - *--------------------------------------------------------------------------------------------*/ +/*-------------------------------------------------------------------------------------- + * Copyright 2025 Glass Devtools, Inc. All rights reserved. + * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. + *--------------------------------------------------------------------------------------*/ import Groq from 'groq-sdk'; import { _InternalSendLLMMessageFnType } from '../../common/llmMessageTypes.js'; diff --git a/src/vs/platform/void/electron-main/llmMessage/ollama.ts b/src/vs/platform/void/electron-main/llmMessage/ollama.ts index d9184157..95792700 100644 --- a/src/vs/platform/void/electron-main/llmMessage/ollama.ts +++ b/src/vs/platform/void/electron-main/llmMessage/ollama.ts @@ -1,10 +1,11 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Glass Devtools, Inc. All rights reserved. - * Void Editor additions licensed under the AGPL 3.0 License. - *--------------------------------------------------------------------------------------------*/ +/*-------------------------------------------------------------------------------------- + * Copyright 2025 Glass Devtools, Inc. All rights reserved. + * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. + *--------------------------------------------------------------------------------------*/ import { Ollama } from 'ollama'; import { _InternalModelListFnType, _InternalSendLLMMessageFnType, OllamaModelResponse } from '../../common/llmMessageTypes.js'; +import { defaultProviderSettings } from '../../common/voidSettingsTypes.js'; export const ollamaList: _InternalModelListFnType = async ({ onSuccess: onSuccess_, onError: onError_, settingsOfProvider }) => { @@ -18,6 +19,9 @@ export const ollamaList: _InternalModelListFnType = async ( try { const thisConfig = settingsOfProvider.ollama + // if endpoint is empty, normally ollama will send to 11434, but we want it to fail - the user should type it in + if (!thisConfig.endpoint) throw new Error(`Ollama Endpoint was empty (please enter ${defaultProviderSettings.ollama.endpoint} in Void if you want the default url).`) + const ollama = new Ollama({ host: thisConfig.endpoint }) ollama.list() .then((response) => { @@ -38,6 +42,8 @@ export const ollamaList: _InternalModelListFnType = async ( export const sendOllamaMsg: _InternalSendLLMMessageFnType = ({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter }) => { const thisConfig = settingsOfProvider.ollama + // if endpoint is empty, normally ollama will send to 11434, but we want it to fail - the user should type it in + if (!thisConfig.endpoint) throw new Error(`Ollama Endpoint was empty (please enter ${defaultProviderSettings.ollama.endpoint} if you want the default).`) let fullText = '' diff --git a/src/vs/platform/void/electron-main/llmMessage/openai.ts b/src/vs/platform/void/electron-main/llmMessage/openai.ts index fa6ac5f3..391764cb 100644 --- a/src/vs/platform/void/electron-main/llmMessage/openai.ts +++ b/src/vs/platform/void/electron-main/llmMessage/openai.ts @@ -1,7 +1,7 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Glass Devtools, Inc. All rights reserved. - * Void Editor additions licensed under the AGPL 3.0 License. - *--------------------------------------------------------------------------------------------*/ +/*-------------------------------------------------------------------------------------- + * Copyright 2025 Glass Devtools, Inc. All rights reserved. + * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. + *--------------------------------------------------------------------------------------*/ import OpenAI from 'openai'; import { _InternalModelListFnType, _InternalSendLLMMessageFnType } from '../../common/llmMessageTypes.js'; diff --git a/src/vs/platform/void/electron-main/llmMessage/sendLLMMessage.ts b/src/vs/platform/void/electron-main/llmMessage/sendLLMMessage.ts index 9e106f97..5d1866db 100644 --- a/src/vs/platform/void/electron-main/llmMessage/sendLLMMessage.ts +++ b/src/vs/platform/void/electron-main/llmMessage/sendLLMMessage.ts @@ -1,7 +1,7 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Glass Devtools, Inc. All rights reserved. - * Void Editor additions licensed under the AGPL 3.0 License. - *--------------------------------------------------------------------------------------------*/ +/*-------------------------------------------------------------------------------------- + * Copyright 2025 Glass Devtools, Inc. All rights reserved. + * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. + *--------------------------------------------------------------------------------------*/ import { LLMMMessageParams, OnText, OnFinalMessage, OnError } from '../../common/llmMessageTypes.js'; import { IMetricsService } from '../../common/metricsService.js'; @@ -34,9 +34,9 @@ export const sendLLMMessage = ({ const captureChatEvent = (eventId: string, extras?: object) => { metricsService.capture(eventId, { providerName, + modelName, numMessages: messages?.length, messagesShape: messages?.map(msg => ({ role: msg.role, length: msg.content.length })), - version: '2024-11-14', ...extras, }) } @@ -61,7 +61,6 @@ export const sendLLMMessage = ({ const onError: OnError = ({ message: error, fullError }) => { if (_didAbort) return - console.log("ERROR!!!!!", error) console.error('sendLLMMessage onError:', error) captureChatEvent(`${loggingName} - Error`, { error }) onError_({ message: error, fullError }) diff --git a/src/vs/platform/void/electron-main/llmMessageChannel.ts b/src/vs/platform/void/electron-main/llmMessageChannel.ts index bf08934e..2430fce2 100644 --- a/src/vs/platform/void/electron-main/llmMessageChannel.ts +++ b/src/vs/platform/void/electron-main/llmMessageChannel.ts @@ -1,7 +1,7 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Glass Devtools, Inc. All rights reserved. - * Void Editor additions licensed under the AGPL 3.0 License. - *--------------------------------------------------------------------------------------------*/ +/*-------------------------------------------------------------------------------------- + * Copyright 2025 Glass Devtools, Inc. All rights reserved. + * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. + *--------------------------------------------------------------------------------------*/ // registered in app.ts // code convention is to make a service responsible for this stuff, and not a channel, but having fewer files is simpler... diff --git a/src/vs/platform/void/electron-main/metricsMainService.ts b/src/vs/platform/void/electron-main/metricsMainService.ts index aaaf8119..fdfb1d16 100644 --- a/src/vs/platform/void/electron-main/metricsMainService.ts +++ b/src/vs/platform/void/electron-main/metricsMainService.ts @@ -1,46 +1,100 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Glass Devtools, Inc. All rights reserved. - * Void Editor additions licensed under the AGPL 3.0 License. - *--------------------------------------------------------------------------------------------*/ +/*-------------------------------------------------------------------------------------- + * Copyright 2025 Glass Devtools, Inc. All rights reserved. + * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. + *--------------------------------------------------------------------------------------*/ import { Disposable } from '../../../base/common/lifecycle.js'; -import { ITelemetryService } from '../../telemetry/common/telemetry.js'; +import { isLinux, isMacintosh, isWindows } from '../../../base/common/platform.js'; +import { generateUuid } from '../../../base/common/uuid.js'; +import { IEnvironmentMainService } from '../../environment/electron-main/environmentMainService.js'; + +import { IProductService } from '../../product/common/productService.js'; +import { IStorageMainService } from '../../storage/electron-main/storageMainService.js'; import { IMetricsService } from '../common/metricsService.js'; import { PostHog } from 'posthog-node' -// posthog-js (old): -// posthog.init('phc_UanIdujHiLp55BkUTjB1AuBXcasVkdqRwgnwRlWESH2', { api_host: 'https://us.i.posthog.com', }) +const os = isWindows ? 'windows' : isMacintosh ? 'mac' : isLinux ? 'linux' : null -// const buildEnv = 'development'; -// const buildNumber = '1.0.0'; -// const isMac = process.platform === 'darwin'; +const VOID_MACHINE_STORAGE_KEY = 'void.machineId' export class MetricsMainService extends Disposable implements IMetricsService { _serviceBrand: undefined; - readonly _distinctId: string - readonly client: PostHog + private readonly client: PostHog + + private readonly _initProperties: object + + + // TODO we should eventually identify people based on email + private get machineId() { + const currVal = this._storageService.applicationStorage.get(VOID_MACHINE_STORAGE_KEY) + if (currVal !== undefined) return currVal + const newVal = generateUuid() + this._storageService.applicationStorage.set(VOID_MACHINE_STORAGE_KEY, newVal) + return newVal + } + constructor( - @ITelemetryService private readonly _telemetryService: ITelemetryService + @IProductService private readonly _productService: IProductService, + @IStorageMainService private readonly _storageService: IStorageMainService, + @IEnvironmentMainService private readonly _envMainService: IEnvironmentMainService, ) { super() - this.client = new PostHog('phc_UanIdujHiLp55BkUTjB1AuBXcasVkdqRwgnwRlWESH2', { host: 'https://us.i.posthog.com', }) + this.client = new PostHog('phc_UanIdujHiLp55BkUTjB1AuBXcasVkdqRwgnwRlWESH2', { + host: 'https://us.i.posthog.com', + }) - const { devDeviceId, firstSessionDate, machineId } = this._telemetryService - this._distinctId = devDeviceId - this.client.identify({ distinctId: devDeviceId, properties: { firstSessionDate, machineId } }) + // we'd like to use devDeviceId on telemetryService, but that gets sanitized by the time it gets here as 'someValue.devDeviceId' - console.log('Void posthog metrics info:', JSON.stringify({ devDeviceId, firstSessionDate, machineId })) + const { commit, version, quality } = this._productService + + const isDevMode = !this._envMainService.isBuilt // found in abstractUpdateService.ts + + + // custom properties we identify + this._initProperties = { + commit, + version, + os, + quality, + distinctId: this.machineId, + isDevMode, + ...this._getOSInfo(), + } + + const identifyMessage = { + distinctId: this.machineId, + properties: this._initProperties, + } + this.client.identify(identifyMessage) + + console.log('Void posthog metrics info:', JSON.stringify(identifyMessage, null, 2)) + + } + + _getOSInfo() { + try { + const { platform, arch } = process // see platform.ts + return { platform, arch } + } + catch (e) { + return { osInfo: { platform: '??', arch: '??' } } + } } capture: IMetricsService['capture'] = (event, params) => { - const capture = { distinctId: this._distinctId, event, properties: params } as const + const capture = { distinctId: this.machineId, event, properties: params } as const // console.log('full capture:', capture) this.client.capture(capture) } + + + async getDebuggingProperties() { + return this._initProperties + } } diff --git a/src/vs/platform/void/electron-main/voidUpdateMainService.ts b/src/vs/platform/void/electron-main/voidUpdateMainService.ts new file mode 100644 index 00000000..029db5f4 --- /dev/null +++ b/src/vs/platform/void/electron-main/voidUpdateMainService.ts @@ -0,0 +1,50 @@ +/*-------------------------------------------------------------------------------------- + * Copyright 2025 Glass Devtools, Inc. All rights reserved. + * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. + *--------------------------------------------------------------------------------------*/ + +import { Disposable } from '../../../base/common/lifecycle.js'; +import { IEnvironmentMainService } from '../../environment/electron-main/environmentMainService.js'; + +import { IProductService } from '../../product/common/productService.js'; + +import { IVoidUpdateService } from '../common/voidUpdateService.js'; + + + +export class VoidMainUpdateService extends Disposable implements IVoidUpdateService { + _serviceBrand: undefined; + + constructor( + @IProductService private readonly _productService: IProductService, + @IEnvironmentMainService private readonly _envMainService: IEnvironmentMainService, + ) { + super() + } + + async check() { + const isDevMode = !this._envMainService.isBuilt // found in abstractUpdateService.ts + + if (isDevMode) { + return { hasUpdate: false } as const + } + + try { + const res = await fetch(`https://updates.voideditor.dev/api/v0/${this._productService.commit}`) + const resJSON = await res.json() + + if (!resJSON) return null + + const { hasUpdate, downloadMessage } = resJSON ?? {} + if (hasUpdate === undefined) + return null + + const after = (downloadMessage || '') + '' + return { hasUpdate: !!hasUpdate, message: after } + } + catch (e) { + return null + } + } +} + diff --git a/src/vs/workbench/browser/layout.ts b/src/vs/workbench/browser/layout.ts index e39d7ce6..770269f8 100644 --- a/src/vs/workbench/browser/layout.ts +++ b/src/vs/workbench/browser/layout.ts @@ -2534,7 +2534,7 @@ const LayoutStateKeys = { // Part Sizing GRID_SIZE: new InitializationStateKey('grid.size', StorageScope.PROFILE, StorageTarget.MACHINE, { width: 800, height: 600 }), SIDEBAR_SIZE: new InitializationStateKey('sideBar.size', StorageScope.PROFILE, StorageTarget.MACHINE, 200), - AUXILIARYBAR_SIZE: new InitializationStateKey('auxiliaryBar.size', StorageScope.PROFILE, StorageTarget.MACHINE, 200), + AUXILIARYBAR_SIZE: new InitializationStateKey('auxiliaryBar.size', StorageScope.PROFILE, StorageTarget.MACHINE, 800), // Void changed this from 200 to 800 PANEL_SIZE: new InitializationStateKey('panel.size', StorageScope.PROFILE, StorageTarget.MACHINE, 300), PANEL_LAST_NON_MAXIMIZED_HEIGHT: new RuntimeStateKey('panel.lastNonMaximizedHeight', StorageScope.PROFILE, StorageTarget.MACHINE, 300), diff --git a/src/vs/workbench/browser/media/code-icon.svg b/src/vs/workbench/browser/media/code-icon.svg deleted file mode 100644 index cc61f81e..00000000 --- a/src/vs/workbench/browser/media/code-icon.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/vs/workbench/browser/media/void-icon-sm.png b/src/vs/workbench/browser/media/void-icon-sm.png new file mode 100644 index 00000000..225179f8 Binary files /dev/null and b/src/vs/workbench/browser/media/void-icon-sm.png differ diff --git a/src/vs/workbench/browser/parts/auxiliarybar/auxiliaryBarPart.ts b/src/vs/workbench/browser/parts/auxiliarybar/auxiliaryBarPart.ts index f3718162..f7225d40 100644 --- a/src/vs/workbench/browser/parts/auxiliarybar/auxiliaryBarPart.ts +++ b/src/vs/workbench/browser/parts/auxiliarybar/auxiliaryBarPart.ts @@ -45,7 +45,7 @@ export class AuxiliaryBarPart extends AbstractPaneCompositePart { static readonly viewContainersWorkspaceStateKey = 'workbench.auxiliarybar.viewContainersWorkspaceState'; // Use the side bar dimensions - override readonly minimumWidth: number = 170; + override readonly minimumWidth: number = 230; // Void changed this (was 170) override readonly maximumWidth: number = Number.POSITIVE_INFINITY; override readonly minimumHeight: number = 0; override readonly maximumHeight: number = Number.POSITIVE_INFINITY; diff --git a/src/vs/workbench/browser/parts/banner/media/bannerpart.css b/src/vs/workbench/browser/parts/banner/media/bannerpart.css index a0de81f2..90e26053 100644 --- a/src/vs/workbench/browser/parts/banner/media/bannerpart.css +++ b/src/vs/workbench/browser/parts/banner/media/bannerpart.css @@ -30,7 +30,7 @@ background-repeat: no-repeat; background-position: center center; background-size: 16px; - background-image: url('../../../../browser/media/code-icon.svg'); + background-image: url('../../../../browser/media/void-icon-sm.png'); width: 16px; padding: 0; margin: 0 6px 0 10px; diff --git a/src/vs/workbench/browser/parts/editor/editorGroupWatermark.ts b/src/vs/workbench/browser/parts/editor/editorGroupWatermark.ts index 88c9cf34..787632ae 100644 --- a/src/vs/workbench/browser/parts/editor/editorGroupWatermark.ts +++ b/src/vs/workbench/browser/parts/editor/editorGroupWatermark.ts @@ -18,13 +18,13 @@ import { isRecentFolder, IWorkspacesService } from '../../../../platform/workspa import { ICommandService } from '../../../../platform/commands/common/commands.js'; import { OpenFileFolderAction, OpenFolderAction } from '../../actions/workspaceActions.js'; import { isMacintosh, isNative, OS } from '../../../../base/common/platform.js'; -import { VOID_CTRL_L_ACTION_ID } from '../../../contrib/void/browser/sidebarActions.js'; -import { VOID_CTRL_K_ACTION_ID } from '../../../contrib/void/browser/quickEditActions.js'; import { defaultKeybindingLabelStyles } from '../../../../platform/theme/browser/defaultStyles.js'; import { IWindowOpenable } from '../../../../platform/window/common/window.js'; import { ILabelService, Verbosity } from '../../../../platform/label/common/label.js'; import { splitRecentLabel } from '../../../../base/common/labels.js'; import { IHostService } from '../../../services/host/browser/host.js'; +import { VOID_OPEN_SETTINGS_ACTION_ID } from '../../../contrib/void/browser/voidSettingsPane.js'; +import { VOID_CTRL_K_ACTION_ID, VOID_CTRL_L_ACTION_ID } from '../../../contrib/void/browser/actionIDs.js'; // import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; registerColor('editorWatermark.foreground', { dark: transparent(editorForeground, 0.6), light: transparent(editorForeground, 0.68), hcDark: editorForeground, hcLight: editorForeground }, localize('editorLineHighlight', 'Foreground color for the labels in the editor watermark.')); @@ -104,7 +104,7 @@ export class EditorGroupWatermark extends Disposable { const isDark = theme === ColorScheme.DARK || theme === ColorScheme.HIGH_CONTRAST_DARK elements.icon.style.maxWidth = '220px' elements.icon.style.opacity = '50%' - elements.icon.style.filter = isDark ? 'brightness(.5)' : 'invert(1)' + elements.icon.style.filter = isDark ? '' : 'invert(1)' //brightness(.5) } updateTheme() this._register( @@ -167,14 +167,17 @@ export class EditorGroupWatermark extends Disposable { // .filter(entry => !!this.keybindingService.lookupKeybinding(entry.id)); this.clear(); - const box = append(this.shortcuts, $('.watermark-box')); - const boxBelow = append(this.shortcuts, $('')) + const voidIconBox = append(this.shortcuts, $('.watermark-box')); + const recentsBox = append(this.shortcuts, $('div')); + recentsBox.style.display = 'flex' + recentsBox.style.flex = 'row' + recentsBox.style.justifyContent = 'center' const update = async () => { - clearNode(box); - clearNode(boxBelow); + clearNode(voidIconBox); + clearNode(recentsBox); this.currentDisposables.forEach(label => label.dispose()); this.currentDisposables.clear(); @@ -183,10 +186,15 @@ export class EditorGroupWatermark extends Disposable { // Void - if the workbench is empty, show open if (this.contextService.getWorkbenchState() === WorkbenchState.EMPTY) { - // Open Folder - const button = h('button') - button.root.textContent = 'Open Folder' - button.root.onclick = () => { + // Open a folder + const openFolderButton = h('button') + openFolderButton.root.classList.add('void-watermark-button') + openFolderButton.root.style.display = 'block' + openFolderButton.root.style.marginLeft = 'auto' + openFolderButton.root.style.marginRight = 'auto' + openFolderButton.root.style.marginBottom = '16px' + openFolderButton.root.textContent = 'Open a folder' + openFolderButton.root.onclick = () => { this.commandService.executeCommand(isMacintosh && isNative ? OpenFileFolderAction.ID : OpenFolderAction.ID) // if (this.contextKeyService.contextMatchesRules(ContextKeyExpr.and(WorkbenchStateContext.isEqualTo('workspace')))) { // this.commandService.executeCommand(OpenFolderViaWorkspaceAction.ID); @@ -194,69 +202,72 @@ export class EditorGroupWatermark extends Disposable { // this.commandService.executeCommand(isMacintosh ? 'workbench.action.files.openFileFolder' : 'workbench.action.files.openFolder'); // } } - box.appendChild(button.root); + voidIconBox.appendChild(openFolderButton.root); + // Recents const recentlyOpened = await this.workspacesService.getRecentlyOpened() .catch(() => ({ files: [], workspaces: [] })).then(w => w.workspaces); + if (recentlyOpened.length !== 0) { - box.append( - ...recentlyOpened.map(w => { + voidIconBox.append( + ...recentlyOpened.map((w, i) => { - let fullPath: string; - let windowOpenable: IWindowOpenable; - if (isRecentFolder(w)) { - windowOpenable = { folderUri: w.folderUri }; - fullPath = w.label || this.labelService.getWorkspaceLabel(w.folderUri, { verbose: Verbosity.LONG }); - } - else { - return null - // fullPath = w.label || this.labelService.getWorkspaceLabel(w.workspace, { verbose: Verbosity.LONG }); - // windowOpenable = { workspaceUri: w.workspace.configPath }; - } + let fullPath: string; + let windowOpenable: IWindowOpenable; + if (isRecentFolder(w)) { + windowOpenable = { folderUri: w.folderUri }; + fullPath = w.label || this.labelService.getWorkspaceLabel(w.folderUri, { verbose: Verbosity.LONG }); + } + else { + return null + // fullPath = w.label || this.labelService.getWorkspaceLabel(w.workspace, { verbose: Verbosity.LONG }); + // windowOpenable = { workspaceUri: w.workspace.configPath }; + } + const { name, parentPath } = splitRecentLabel(fullPath); - const { name, parentPath } = splitRecentLabel(fullPath); + const linkSpan = $('span'); + linkSpan.classList.add('void-link') + linkSpan.style.display = 'flex' + linkSpan.style.gap = '4px' + linkSpan.style.padding = '8px' - const li = $('li'); - const link = $('button.button-link'); - - link.innerText = name; - link.title = fullPath; - link.setAttribute('aria-label', localize('welcomePage.openFolderWithPath', "Open folder {0} with path {1}", name, parentPath)); - link.addEventListener('click', e => { - this.hostService.openWindow([windowOpenable], { - forceNewWindow: e.ctrlKey || e.metaKey, - remoteAuthority: w.remoteAuthority || null // local window if remoteAuthority is not set or can not be deducted from the openable + linkSpan.addEventListener('click', e => { + this.hostService.openWindow([windowOpenable], { + forceNewWindow: e.ctrlKey || e.metaKey, + remoteAuthority: w.remoteAuthority || null // local window if remoteAuthority is not set or can not be deducted from the openable + }); + e.preventDefault(); + e.stopPropagation(); }); - e.preventDefault(); - e.stopPropagation(); - }); - li.appendChild(link); - const span = $('span'); - span.classList.add('path'); - span.classList.add('detail'); - span.innerText = parentPath; - span.title = fullPath; - li.appendChild(span); + const nameSpan = $('span'); + nameSpan.innerText = name; + nameSpan.title = fullPath; + linkSpan.appendChild(nameSpan); + const dirSpan = $('span'); + dirSpan.style.paddingLeft = '4px'; + dirSpan.innerText = parentPath; + dirSpan.title = fullPath; - return li - }).filter(v => !!v) - ) - + linkSpan.appendChild(dirSpan); + return linkSpan + }).filter(v => !!v) + ) + } } else { // show them Void keybindings const keys = this.keybindingService.lookupKeybinding(VOID_CTRL_L_ACTION_ID); - const dl = append(box, $('dl')); + const dl = append(voidIconBox, $('dl')); const dt = append(dl, $('dt')); dt.textContent = 'Chat' const dd = append(dl, $('dd')); @@ -267,7 +278,7 @@ export class EditorGroupWatermark extends Disposable { const keys2 = this.keybindingService.lookupKeybinding(VOID_CTRL_K_ACTION_ID); - const dl2 = append(box, $('dl')); + const dl2 = append(voidIconBox, $('dl')); const dt2 = append(dl2, $('dt')); dt2.textContent = 'Quick Edit' const dd2 = append(dl2, $('dd')); @@ -277,18 +288,21 @@ export class EditorGroupWatermark extends Disposable { this.currentDisposables.add(label2); const keys3 = this.keybindingService.lookupKeybinding('workbench.action.openGlobalKeybindings'); - const button3 = append(boxBelow, $('button')); - button3.textContent = 'Change Keybindings' + const button3 = append(recentsBox, $('button')); + button3.textContent = 'Void Settings' + button3.style.display = 'block' + button3.style.marginLeft = 'auto' + button3.style.marginRight = 'auto' + button3.classList.add('void-settings-watermark-button') + const label3 = new KeybindingLabel(button3, OS, { renderUnboundKeybindings: true, ...defaultKeybindingLabelStyles }); if (keys3) label3.set(keys3); button3.onclick = () => { - this.commandService.executeCommand('workbench.action.openGlobalKeybindings') + this.commandService.executeCommand(VOID_OPEN_SETTINGS_ACTION_ID) } this.currentDisposables.add(label3); - - } }; diff --git a/src/vs/workbench/browser/parts/titlebar/media/titlebarpart.css b/src/vs/workbench/browser/parts/titlebar/media/titlebarpart.css index ccc39382..221634bb 100644 --- a/src/vs/workbench/browser/parts/titlebar/media/titlebarpart.css +++ b/src/vs/workbench/browser/parts/titlebar/media/titlebarpart.css @@ -253,7 +253,7 @@ } .monaco-workbench .part.titlebar > .titlebar-container > .titlebar-left > .window-appicon:not(.codicon) { - background-image: url('../../../media/code-icon.svg'); + background-image: url('../../../media/void-icon-sm.png'); background-repeat: no-repeat; background-position: center center; background-size: 16px; @@ -275,7 +275,7 @@ height: 8px; z-index: 1; /* on top of home indicator */ - background-image: url('../../../media/code-icon.svg'); + background-image: url('../../../media/void-icon-sm.png'); background-repeat: no-repeat; background-position: center center; background-size: 8px; diff --git a/src/vs/workbench/contrib/update/browser/media/releasenoteseditor.css b/src/vs/workbench/contrib/update/browser/media/releasenoteseditor.css index 4210055b..b79e3905 100644 --- a/src/vs/workbench/contrib/update/browser/media/releasenoteseditor.css +++ b/src/vs/workbench/contrib/update/browser/media/releasenoteseditor.css @@ -5,5 +5,5 @@ .file-icons-enabled .show-file-icons .webview-vs_code_release_notes-name-file-icon.file-icon::before { content: ' '; - background-image: url('../../../../browser/media/code-icon.svg'); + background-image: url('../../../../browser/media/void-icon-sm.png'); } diff --git a/src/vs/workbench/contrib/void/browser/actionIDs.ts b/src/vs/workbench/contrib/void/browser/actionIDs.ts new file mode 100644 index 00000000..b237ecf8 --- /dev/null +++ b/src/vs/workbench/contrib/void/browser/actionIDs.ts @@ -0,0 +1,6 @@ +// Normally you'd want to put these exports in the files that register them, but if you do that you'll get an import order error if you import them in certain cases. +// (importing them runs the whole file to get the ID, causing an import error). I guess it's best practice to separate out IDs, pretty annoying... + +export const VOID_CTRL_L_ACTION_ID = 'void.ctrlLAction' + +export const VOID_CTRL_K_ACTION_ID = 'void.ctrlKAction' diff --git a/src/vs/workbench/contrib/void/browser/autocompleteService.ts b/src/vs/workbench/contrib/void/browser/autocompleteService.ts index 649b310c..9237c7ca 100644 --- a/src/vs/workbench/contrib/void/browser/autocompleteService.ts +++ b/src/vs/workbench/contrib/void/browser/autocompleteService.ts @@ -1,7 +1,7 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Glass Devtools, Inc. All rights reserved. - * Void Editor additions licensed under the AGPL 3.0 License. - *--------------------------------------------------------------------------------------------*/ +/*-------------------------------------------------------------------------------------- + * Copyright 2025 Glass Devtools, Inc. All rights reserved. + * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. + *--------------------------------------------------------------------------------------*/ import { Disposable } from '../../../../base/common/lifecycle.js'; import { ILanguageFeaturesService } from '../../../../editor/common/services/languageFeatures.js'; @@ -17,6 +17,7 @@ import { IEditorService } from '../../../services/editor/common/editorService.js import { isCodeEditor } from '../../../../editor/browser/editorBrowser.js'; import { EditorResourceAccessor } from '../../../common/editor.js'; import { IModelService } from '../../../../editor/common/services/model.js'; +import { extractCodeFromRegular } from './helpers/extractCodeFromResult.js'; // The extension this was called from is here - https://github.com/voideditor/void/blob/autocomplete/extensions/void/src/extension/extension.ts @@ -165,20 +166,6 @@ const postprocessResult = (result: string) => { } -const extractCodeFromResult = (result: string) => { - // Match either: - // 1. ```language\n``` - // 2. `````` - const match = result.match(/```(?:\w+\n)?([\s\S]*?)```|```([\s\S]*?)```/); - - if (!match) { - return result; - } - - // Return whichever group matched (non-empty) - return match[1] ?? match[2] ?? result; -} - // trims the end of the prefix to improve cache hit rate const removeLeftTabsAndTrimEnd = (s: string): string => { @@ -665,7 +652,8 @@ export class AutocompleteService extends Disposable implements IAutocompleteServ // newAutocompletion.abortRef = { current: () => { } } newAutocompletion.status = 'finished' // newAutocompletion.promise = undefined - newAutocompletion.insertText = postprocessResult(extractCodeFromResult(fullText)) + const [text, _] = extractCodeFromRegular({ text: fullText, recentlyAddedTextLen: 0 }) + newAutocompletion.insertText = postprocessResult(text) resolve(newAutocompletion.insertText) @@ -675,7 +663,7 @@ export class AutocompleteService extends Disposable implements IAutocompleteServ newAutocompletion.status = 'error' reject(message) }, - featureName: 'Autocomplete', + useProviderFor: 'Autocomplete', range: { startLineNumber: position.lineNumber, startColumn: position.column, endLineNumber: position.lineNumber, endColumn: position.column }, }) newAutocompletion.requestId = requestId @@ -768,3 +756,5 @@ export class AutocompleteService extends Disposable implements IAutocompleteServ registerSingleton(IAutocompleteService, AutocompleteService, InstantiationType.Eager); + + diff --git a/src/vs/workbench/contrib/void/browser/chatThreadService.ts b/src/vs/workbench/contrib/void/browser/chatThreadService.ts new file mode 100644 index 00000000..7fede377 --- /dev/null +++ b/src/vs/workbench/contrib/void/browser/chatThreadService.ts @@ -0,0 +1,302 @@ +/*-------------------------------------------------------------------------------------- + * Copyright 2025 Glass Devtools, Inc. All rights reserved. + * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. + *--------------------------------------------------------------------------------------*/ + +import { Disposable } from '../../../../base/common/lifecycle.js'; +import { registerSingleton, InstantiationType } from '../../../../platform/instantiation/common/extensions.js'; +import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; +import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; + +import { URI } from '../../../../base/common/uri.js'; +import { Emitter, Event } from '../../../../base/common/event.js'; +import { IRange } from '../../../../editor/common/core/range.js'; +import { ILLMMessageService } from '../../../../platform/void/common/llmMessageService.js'; +import { IModelService } from '../../../../editor/common/services/model.js'; +import { VSReadFile } from './helpers/readFile.js'; +import { chat_prompt, chat_systemMessage } from './prompt/prompts.js'; + +export type CodeSelection = { + fileURI: URI; + selectionStr: string | null; + content: string; // TODO remove this (replace `selectionStr` with `content`) + range: IRange; +} + +// if selectionStr is null, it means to use the entire file at send time +export type CodeStagingSelection = { + type: 'Selection', + fileURI: URI, + selectionStr: string, + range: IRange +} | { + type: 'File', + fileURI: URI, + selectionStr: null, + range: null +} + + +// WARNING: changing this format is a big deal!!!!!! need to migrate old format to new format on users' computers so people don't get errors. +export type ChatMessage = + | { + role: 'user'; + content: string | null; // content sent to the llm - allowed to be '', will be replaced with (empty) + displayContent: string | null; // content displayed to user - allowed to be '', will be ignored + selections: CodeSelection[] | null; // the user's selection + } + | { + role: 'assistant'; + content: string | null; // content received from LLM - allowed to be '', will be replaced with (empty) + displayContent: string | null; // content displayed to user (this is the same as content for now) - allowed to be '', will be ignored + } + | { + role: 'system'; + content: string; + displayContent?: undefined; + } + +// a 'thread' means a chat message history +export type ChatThreads = { + [id: string]: { + id: string; // store the id here too + createdAt: string; // ISO string + lastModified: string; // ISO string + messages: ChatMessage[]; + }; +} + +export type ThreadsState = { + allThreads: ChatThreads; + currentThreadId: string; // intended for internal use only + currentStagingSelections: CodeStagingSelection[] | null; +} + +export type ThreadStreamState = { + [threadId: string]: undefined | { + error?: { message: string, fullError: Error | null }; + messageSoFar?: string; + streamingToken?: string; + } +} + + +const newThreadObject = () => { + const now = new Date().toISOString() + return { + id: new Date().getTime().toString(), + createdAt: now, + lastModified: now, + messages: [], + } satisfies ChatThreads[string] +} + +const THREAD_STORAGE_KEY = 'void.chatThreadStorage' + +export interface IChatThreadService { + readonly _serviceBrand: undefined; + + readonly state: ThreadsState; + readonly streamState: ThreadStreamState; + + onDidChangeCurrentThread: Event; + onDidChangeStreamState: Event<{ threadId: string }> + + getCurrentThread(): ChatThreads[string]; + openNewThread(): void; + switchToThread(threadId: string): void; + + setStaging(stagingSelection: CodeStagingSelection[] | null): void; + + addUserMessageAndStreamResponse(userMessage: string): Promise; + cancelStreaming(threadId: string): void; + dismissStreamError(threadId: string): void; + +} + +export const IChatThreadService = createDecorator('voidChatThreadService'); +class ChatThreadService extends Disposable implements IChatThreadService { + _serviceBrand: undefined; + + // this fires when the current thread changes at all (a switch of currentThread, or a message added to it, etc) + private readonly _onDidChangeCurrentThread = new Emitter(); + readonly onDidChangeCurrentThread: Event = this._onDidChangeCurrentThread.event; + + readonly streamState: ThreadStreamState = {} + private readonly _onDidChangeStreamState = new Emitter<{ threadId: string }>(); + readonly onDidChangeStreamState: Event<{ threadId: string }> = this._onDidChangeStreamState.event; + + state: ThreadsState // allThreads is persisted, currentThread is not + + constructor( + @IStorageService private readonly _storageService: IStorageService, + @IModelService private readonly _modelService: IModelService, + @ILLMMessageService private readonly _llmMessageService: ILLMMessageService, + ) { + super() + + this.state = { + allThreads: this._readAllThreads(), + currentThreadId: null as unknown as string, // gets set in startNewThread() + currentStagingSelections: null, + } + + // always be in a thread + this.openNewThread() + } + + + private _readAllThreads(): ChatThreads { + // PUT ANY VERSION CHANGE FORMAT CONVERSION CODE HERE + const threads = this._storageService.get(THREAD_STORAGE_KEY, StorageScope.APPLICATION) + return threads ? JSON.parse(threads) : {} + } + + private _storeAllThreads(threads: ChatThreads) { + this._storageService.store(THREAD_STORAGE_KEY, JSON.stringify(threads), StorageScope.APPLICATION, StorageTarget.USER) + } + + // this should be the only place this.state = ... appears besides constructor + private _setState(state: Partial, affectsCurrent: boolean) { + this.state = { + ...this.state, + ...state + } + if (affectsCurrent) + this._onDidChangeCurrentThread.fire() + } + + private _setStreamState(threadId: string, state: Partial>) { + this.streamState[threadId] = { + ...this.streamState[threadId], + ...state + } + this._onDidChangeStreamState.fire({ threadId }) + } + + + // ---------- streaming ---------- + + finishStreaming = (threadId: string, content: string, error?: { message: string, fullError: Error | null }) => { + // add assistant's message to chat history, and clear selection + const assistantHistoryElt: ChatMessage = { role: 'assistant', content, displayContent: content || null } + this._addMessageToThread(threadId, assistantHistoryElt) + this._setStreamState(threadId, { messageSoFar: undefined, streamingToken: undefined, error }) + } + + async addUserMessageAndStreamResponse(userMessage: string) { + const threadId = this.getCurrentThread().id + + const currSelns = this.state.currentStagingSelections ?? [] + const selections = !currSelns ? null : await Promise.all( + currSelns.map(async (sel) => ({ ...sel, content: await VSReadFile(this._modelService, sel.fileURI) })) + ).then( + (files) => files.filter(file => file.content !== null) as CodeSelection[] + ) + + // add user's message to chat history + const instructions = userMessage + const userHistoryElt: ChatMessage = { role: 'user', content: chat_prompt(instructions, selections), displayContent: instructions, selections: selections } + this._addMessageToThread(threadId, userHistoryElt) + + + this._setStreamState(threadId, { error: undefined }) + + const llmCancelToken = this._llmMessageService.sendLLMMessage({ + logging: { loggingName: 'Chat' }, + messages: [ + { role: 'system', content: chat_systemMessage }, + ...this.getCurrentThread().messages.map(m => ({ role: m.role, content: m.content || '(null)' })), + ], + onText: ({ newText, fullText }) => { + this._setStreamState(threadId, { messageSoFar: fullText }) + }, + onFinalMessage: ({ fullText: content }) => { + this.finishStreaming(threadId, content) + }, + onError: (error) => { + this.finishStreaming(threadId, this.streamState[threadId]?.messageSoFar ?? '', error) + }, + useProviderFor: 'Ctrl+L', + + }) + if (llmCancelToken === null) return + this._setStreamState(threadId, { streamingToken: llmCancelToken }) + + } + + cancelStreaming(threadId: string) { + const llmCancelToken = this.streamState[threadId]?.streamingToken + if (llmCancelToken !== undefined) this._llmMessageService.abort(llmCancelToken) + this.finishStreaming(threadId, this.streamState[threadId]?.messageSoFar ?? '') + } + + dismissStreamError(threadId: string): void { + this._setStreamState(threadId, { error: undefined }) + } + + + + // ---------- the rest ---------- + + getCurrentThread(): ChatThreads[string] { + const state = this.state + return state.allThreads[state.currentThreadId]; + } + + switchToThread(threadId: string) { + // console.log('threadId', threadId) + // console.log('messages', this.state.allThreads[threadId].messages) + this._setState({ currentThreadId: threadId }, true) + } + + + openNewThread() { + // if a thread with 0 messages already exists, switch to it + const { allThreads: currentThreads } = this.state + for (const threadId in currentThreads) { + if (currentThreads[threadId].messages.length === 0) { + this.switchToThread(threadId) + return + } + } + // otherwise, start a new thread + const newThread = newThreadObject() + + // update state + const newThreads: ChatThreads = { + ...currentThreads, + [newThread.id]: newThread + } + this._storeAllThreads(newThreads) + this._setState({ allThreads: newThreads, currentThreadId: newThread.id }, true) + } + + + _addMessageToThread(threadId: string, message: ChatMessage) { + const { allThreads } = this.state + + const oldThread = allThreads[threadId] + + // update state and store it + const newThreads = { + ...allThreads, + [oldThread.id]: { + ...oldThread, + lastModified: new Date().toISOString(), + messages: [...oldThread.messages, message], + } + } + this._storeAllThreads(newThreads) + this._setState({ allThreads: newThreads }, true) // the current thread just changed (it had a message added to it) + } + + + setStaging(stagingSelection: CodeStagingSelection[] | null): void { + this._setState({ currentStagingSelections: stagingSelection }, true) // this is a hack for now + } + +} + +registerSingleton(IChatThreadService, ChatThreadService, InstantiationType.Eager); + diff --git a/src/vs/workbench/contrib/void/browser/helperServices/consistentItemService.ts b/src/vs/workbench/contrib/void/browser/helperServices/consistentItemService.ts new file mode 100644 index 00000000..6de0b53b --- /dev/null +++ b/src/vs/workbench/contrib/void/browser/helperServices/consistentItemService.ts @@ -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 { Disposable } from '../../../../../base/common/lifecycle.js'; +import { URI } from '../../../../../base/common/uri.js'; +import { generateUuid } from '../../../../../base/common/uuid.js'; +import { ICodeEditor } from '../../../../../editor/browser/editorBrowser.js'; +import { ICodeEditorService } from '../../../../../editor/browser/services/codeEditorService.js'; +import { InstantiationType, registerSingleton } from '../../../../../platform/instantiation/common/extensions.js'; +import { createDecorator } from '../../../../../platform/instantiation/common/instantiation.js'; + + +// lets you add a "consistent" item to a Model (aka URI), instead of just to a single editor + +type AddItemInputs = { uri: URI; fn: (editor: ICodeEditor) => (() => void); } + +export interface IConsistentItemService { + readonly _serviceBrand: undefined; + getEditorsOnURI(uri: URI): ICodeEditor[]; + addConsistentItemToURI(inputs: AddItemInputs): string; + removeConsistentItemFromURI(consistentItemId: string): void; +} + +export const IConsistentItemService = createDecorator('ConsistentItemService'); + +export class ConsistentItemService extends Disposable { + + readonly _serviceBrand: undefined + + // the items that are attached to each URI, completely independent from current state of editors + private readonly consistentItemIdsOfURI: Record | undefined> = {} + private readonly infoOfConsistentItemId: Record = {} + + + // current state of items on each editor, and the fns to call to remove them + private readonly itemIdsOfEditorId: Record | undefined> = {} + private readonly consistentItemIdOfItemId: Record = {} + private readonly disposeFnOfItemId: Record void> = {} + + + constructor( + @ICodeEditorService private readonly _editorService: ICodeEditorService, + ) { + super() + + + const removeItemsFromEditor = (editor: ICodeEditor) => { + const editorId = editor.getId() + for (const itemId of this.itemIdsOfEditorId[editorId] ?? []) + this._removeItemFromEditor(editor, itemId) + } + + // put items on the editor, based on the consistent items for that URI + const putItemsOnEditor = (editor: ICodeEditor, uri: URI | null) => { + if (!uri) return + for (const consistentItemId of this.consistentItemIdsOfURI[uri.fsPath] ?? []) + this._putItemOnEditor(editor, consistentItemId) + } + + + // when editor switches tabs (models) + const addTabSwitchListeners = (editor: ICodeEditor) => { + this._register( + editor.onDidChangeModel(e => { + removeItemsFromEditor(editor) + putItemsOnEditor(editor, e.newModelUrl) + }) + ) + } + + // when editor is disposed + const addDisposeListener = (editor: ICodeEditor) => { + this._register(editor.onDidDispose(() => { + // anything on the editor has been disposed already + for (const itemId of this.itemIdsOfEditorId[editor.getId()] ?? []) + delete this.disposeFnOfItemId[itemId] + })) + } + + const initializeEditor = (editor: ICodeEditor) => { + // if (editor.getModel()?.uri.scheme !== 'file') return // THIS BREAKS THINGS + addTabSwitchListeners(editor) + addDisposeListener(editor) + putItemsOnEditor(editor, editor.getModel()?.uri ?? null) + } + + // initialize current editors + any new editors + for (let editor of this._editorService.listCodeEditors()) initializeEditor(editor) + this._register(this._editorService.onCodeEditorAdd(editor => { initializeEditor(editor) })) + + // when an editor is deleted, remove its items + this._register(this._editorService.onCodeEditorRemove(editor => { + removeItemsFromEditor(editor) + })) + + } + + + + _putItemOnEditor(editor: ICodeEditor, consistentItemId: string) { + const { fn } = this.infoOfConsistentItemId[consistentItemId] + + // add item + const dispose = fn(editor) + + const itemId = generateUuid() + const editorId = editor.getId() + + if (!(editorId in this.itemIdsOfEditorId)) + this.itemIdsOfEditorId[editorId] = new Set() + this.itemIdsOfEditorId[editorId]!.add(itemId) + + + this.consistentItemIdOfItemId[itemId] = consistentItemId + + this.disposeFnOfItemId[itemId] = () => { + // console.log('calling remove for', itemId) + dispose?.() + } + + } + + + _removeItemFromEditor(editor: ICodeEditor, itemId: string) { + + const editorId = editor.getId() + this.itemIdsOfEditorId[editorId]?.delete(itemId) + if (this.itemIdsOfEditorId[editorId]?.size === 0) + delete this.itemIdsOfEditorId[editorId] + + this.disposeFnOfItemId[itemId]?.() + delete this.disposeFnOfItemId[itemId] + + delete this.consistentItemIdOfItemId[itemId] + } + + getEditorsOnURI(uri: URI) { + const editors = this._editorService.listCodeEditors().filter(editor => editor.getModel()?.uri.fsPath === uri.fsPath) + return editors + } + + consistentItemIdPool = 0 + addConsistentItemToURI({ uri, fn }: AddItemInputs) { + const consistentItemId = (this.consistentItemIdPool++) + '' + + if (!(uri.fsPath in this.consistentItemIdsOfURI)) + this.consistentItemIdsOfURI[uri.fsPath] = new Set() + this.consistentItemIdsOfURI[uri.fsPath]!.add(consistentItemId) + + this.infoOfConsistentItemId[consistentItemId] = { fn, uri } + + const editors = this.getEditorsOnURI(uri) + for (const editor of editors) + this._putItemOnEditor(editor, consistentItemId) + + return consistentItemId + } + + + removeConsistentItemFromURI(consistentItemId: string) { + if (!(consistentItemId in this.infoOfConsistentItemId)) + return + + const { uri } = this.infoOfConsistentItemId[consistentItemId] + const editors = this.getEditorsOnURI(uri) + + for (const editor of editors) { + for (const itemId of this.itemIdsOfEditorId[editor.getId()] ?? []) { + if (this.consistentItemIdOfItemId[itemId] === consistentItemId) + this._removeItemFromEditor(editor, itemId) + } + } + + // clear + this.consistentItemIdsOfURI[uri.fsPath]?.delete(consistentItemId) + if (this.consistentItemIdsOfURI[uri.fsPath]?.size === 0) + delete this.consistentItemIdsOfURI[uri.fsPath] + + delete this.infoOfConsistentItemId[consistentItemId] + + } + +} + +registerSingleton(IConsistentItemService, ConsistentItemService, InstantiationType.Eager); + + + + + + + + + + + + + + + + + +// mostly generated by o1 (almost the same as above, but just for 1 editor) +export interface IConsistentEditorItemService { + readonly _serviceBrand: undefined; + addToEditor(editor: ICodeEditor, fn: () => () => void): string; + removeFromEditor(itemId: string): void; +} +export const IConsistentEditorItemService = createDecorator('ConsistentEditorItemService'); + + +export class ConsistentEditorItemService extends Disposable { + readonly _serviceBrand: undefined; + + /** + * For each editorId, we track the set of itemIds that have been "added" to that editor. + * This does *not* necessarily mean they're currently mounted (the user may have switched models). + */ + private readonly itemIdsByEditorId: Record> = {}; + + /** + * For each itemId, we store relevant info (the fn to call on the editor, the editorId, the uri, and the current dispose function). + */ + private readonly itemInfoById: Record< + string, + { + editorId: string; + uriFsPath: string; + fn: (editor: ICodeEditor) => () => void; + disposeFn?: () => void; + } + > = {}; + + constructor( + @ICodeEditorService private readonly _editorService: ICodeEditorService, + ) { + super(); + + // + // Wire up listeners to watch for new editors, removed editors, etc. + // + + // Initialize any already-existing editors + for (const editor of this._editorService.listCodeEditors()) { + this._initializeEditor(editor); + } + + // When an editor is added, track it + this._register( + this._editorService.onCodeEditorAdd((editor) => { + this._initializeEditor(editor); + }) + ); + + // When an editor is removed, remove all items associated with that editor + this._register( + this._editorService.onCodeEditorRemove((editor) => { + this._removeAllItemsFromEditor(editor); + }) + ); + } + + /** + * Sets up listeners on the provided editor so that: + * - If the editor changes models, we remove items and re-mount only if the new model matches. + * - If the editor is disposed, we do the needed cleanup. + */ + private _initializeEditor(editor: ICodeEditor) { + const editorId = editor.getId(); + + // + // Listen for model changes + // + this._register( + editor.onDidChangeModel((e) => { + this._removeAllItemsFromEditor(editor); + if (!e.newModelUrl) { + return; + } + // Re-mount any items that belong to this editor and match the new URI + const itemsForEditor = this.itemIdsByEditorId[editorId]; + if (itemsForEditor) { + for (const itemId of itemsForEditor) { + const itemInfo = this.itemInfoById[itemId]; + if (itemInfo && itemInfo.uriFsPath === e.newModelUrl.fsPath) { + this._mountItemOnEditor(editor, itemId); + } + } + } + }) + ); + + // + // When the editor is disposed, remove all items from it + // + this._register( + editor.onDidDispose(() => { + this._removeAllItemsFromEditor(editor); + }) + ); + + // + // If the editor already has a model (e.g. on initial load), try mounting items + // + const uri = editor.getModel()?.uri; + if (!uri) { + return; + } + + const itemsForEditor = this.itemIdsByEditorId[editorId]; + if (itemsForEditor) { + for (const itemId of itemsForEditor) { + const itemInfo = this.itemInfoById[itemId]; + if (itemInfo && itemInfo.uriFsPath === uri.fsPath) { + this._mountItemOnEditor(editor, itemId); + } + } + } + } + + /** + * Actually calls the item-creation function `fn(editor)` and saves the resulting disposeFn + * so we can later clean it up. + */ + private _mountItemOnEditor(editor: ICodeEditor, itemId: string) { + const info = this.itemInfoById[itemId]; + if (!info) { + return; + } + const { fn } = info; + const disposeFn = fn(editor); + info.disposeFn = disposeFn; + } + + /** + * Removes a single item from an editor (calling its `disposeFn` if present). + */ + private _removeItemFromEditor(editor: ICodeEditor, itemId: string) { + const info = this.itemInfoById[itemId]; + if (info?.disposeFn) { + info.disposeFn(); + info.disposeFn = undefined; + } + } + + /** + * Removes *all* items from the given editor. Typically called when the editor changes model or is disposed. + */ + private _removeAllItemsFromEditor(editor: ICodeEditor) { + const editorId = editor.getId(); + const itemsForEditor = this.itemIdsByEditorId[editorId]; + if (!itemsForEditor) { + return; + } + + for (const itemId of itemsForEditor) { + this._removeItemFromEditor(editor, itemId); + } + } + + /** + * Public API: Adds an item to an *individual* editor (determined by editor ID), + * but only when that editor is showing the same model (uri.fsPath). + */ + addToEditor(editor: ICodeEditor, fn: () => () => void): string { + const uri = editor.getModel()?.uri + if (!uri) { + throw new Error('No URI on the provided editor or in AddItemInputs.'); + } + + const editorId = editor.getId(); + + // Create an ID for this item + const itemId = generateUuid(); + + // Record the info + this.itemInfoById[itemId] = { + editorId, + uriFsPath: uri.fsPath, + fn, + }; + + // Add to the editor's known items + if (!this.itemIdsByEditorId[editorId]) { + this.itemIdsByEditorId[editorId] = new Set(); + } + this.itemIdsByEditorId[editorId].add(itemId); + + // If the editor's current URI matches, mount it now + if (editor.getModel()?.uri.fsPath === uri.fsPath) { + this._mountItemOnEditor(editor, itemId); + } + + return itemId; + } + + /** + * Public API: Removes an item from the *specific* editor. We look up which editor + * had this item and remove it from that editor. + */ + removeFromEditor(itemId: string): void { + const info = this.itemInfoById[itemId]; + if (!info) { + // Nothing to remove + return; + } + + const { editorId } = info; + + // Find the editor in question + const editor = this._editorService.listCodeEditors().find( + (ed) => ed.getId() === editorId + ); + if (editor) { + // Dispose on that editor + this._removeItemFromEditor(editor, itemId); + } + + // Clean up references + this.itemIdsByEditorId[editorId]?.delete(itemId); + delete this.itemInfoById[itemId]; + } +} + +registerSingleton(IConsistentEditorItemService, ConsistentEditorItemService, InstantiationType.Eager); + + diff --git a/src/vs/workbench/contrib/void/browser/helpers/detectLanguage.ts b/src/vs/workbench/contrib/void/browser/helpers/detectLanguage.ts new file mode 100644 index 00000000..b4b9d513 --- /dev/null +++ b/src/vs/workbench/contrib/void/browser/helpers/detectLanguage.ts @@ -0,0 +1,170 @@ + +// eg "bash" -> "shell" +export const nameToVscodeLanguage: { [key: string]: string } = { + // Web Technologies + 'html': 'html', + 'css': 'css', + 'scss': 'scss', + 'sass': 'scss', + 'less': 'less', + 'javascript': 'typescript', + 'js': 'typescript', // use more general renderer + 'jsx': 'typescript', + 'typescript': 'typescript', + 'ts': 'typescript', + 'tsx': 'typescript', + 'json': 'json', + 'jsonc': 'json', + + // Programming Languages + 'python': 'python', + 'py': 'python', + 'java': 'java', + 'cpp': 'cpp', + 'c++': 'cpp', + 'c': 'c', + 'csharp': 'csharp', + 'cs': 'csharp', + 'c#': 'csharp', + 'go': 'go', + 'golang': 'go', + 'rust': 'rust', + 'rs': 'rust', + 'ruby': 'ruby', + 'rb': 'ruby', + 'php': 'php', + 'shell': 'shell', + 'bash': 'shell', + 'sh': 'shell', + 'zsh': 'shell', + + // Markup and Config + 'markdown': 'markdown', + 'md': 'markdown', + 'xml': 'xml', + 'svg': 'xml', + 'yaml': 'yaml', + 'yml': 'yaml', + 'ini': 'ini', + 'toml': 'ini', + + // Database and Query Languages + 'sql': 'sql', + 'mysql': 'sql', + 'postgresql': 'sql', + 'graphql': 'graphql', + 'gql': 'graphql', + + // Others + 'dockerfile': 'dockerfile', + 'docker': 'dockerfile', + 'makefile': 'makefile', + 'plaintext': 'plaintext', + 'text': 'plaintext' +}; + + + +// eg ".ts" -> "typescript" +const fileExtensionToVscodeLanguage: { [key: string]: string } = { + // Web + 'html': 'html', + 'htm': 'html', + 'css': 'css', + 'scss': 'scss', + 'less': 'less', + 'js': 'javascript', + 'jsx': 'javascript', + 'ts': 'typescript', + 'tsx': 'typescript', + 'json': 'json', + 'jsonc': 'json', + + // Programming Languages + 'py': 'python', + 'java': 'java', + 'cpp': 'cpp', + 'cc': 'cpp', + 'c': 'c', + 'h': 'cpp', + 'hpp': 'cpp', + 'cs': 'csharp', + 'go': 'go', + 'rs': 'rust', + 'rb': 'ruby', + 'php': 'php', + 'sh': 'shell', + 'bash': 'shell', + 'zsh': 'shell', + + // Markup/Config + 'md': 'markdown', + 'markdown': 'markdown', + 'xml': 'xml', + 'svg': 'xml', + 'yaml': 'yaml', + 'yml': 'yaml', + 'ini': 'ini', + 'toml': 'ini', + + // Other + 'sql': 'sql', + 'graphql': 'graphql', + 'gql': 'graphql', + 'dockerfile': 'dockerfile', + 'docker': 'dockerfile', + 'mk': 'makefile', + + // Config Files and Dot Files + 'npmrc': 'ini', + 'env': 'ini', + 'gitignore': 'ignore', + 'dockerignore': 'ignore', + 'eslintrc': 'json', + 'babelrc': 'json', + 'prettierrc': 'json', + 'stylelintrc': 'json', + 'editorconfig': 'ini', + 'htaccess': 'apacheconf', + 'conf': 'ini', + 'config': 'ini', + + // Package Files + 'package': 'json', + 'package-lock': 'json', + 'gemfile': 'ruby', + 'podfile': 'ruby', + 'rakefile': 'ruby', + + // Build Systems + 'cmake': 'cmake', + 'makefile': 'makefile', + 'gradle': 'groovy', + + // Shell Scripts + 'bashrc': 'shell', + 'zshrc': 'shell', + 'fish': 'shell', + + // Version Control + 'gitconfig': 'ini', + 'hgrc': 'ini', + 'svnconfig': 'ini', + + // Web Server + 'nginx': 'nginx', + + // Misc Config + 'properties': 'properties', + 'cfg': 'ini', + 'reg': 'ini' +}; + + +export function filenameToVscodeLanguage(filename: string): string | undefined { + + const ext = filename.toLowerCase().split('.').pop(); + if (!ext) return undefined; + + return fileExtensionToVscodeLanguage[ext]; +} diff --git a/src/vs/workbench/contrib/void/browser/helpers/extractCodeFromResult.ts b/src/vs/workbench/contrib/void/browser/helpers/extractCodeFromResult.ts new file mode 100644 index 00000000..d7e109ae --- /dev/null +++ b/src/vs/workbench/contrib/void/browser/helpers/extractCodeFromResult.ts @@ -0,0 +1,169 @@ +/*-------------------------------------------------------------------------------------- + * Copyright 2025 Glass Devtools, Inc. All rights reserved. + * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. + *--------------------------------------------------------------------------------------*/ + +class SurroundingsRemover { + readonly originalS: string + i: number + j: number + + // string is s[i...j] + + constructor(s: string) { + this.originalS = s + this.i = 0 + this.j = s.length - 1 + } + value() { + return this.originalS.substring(this.i, this.j + 1) + } + + // returns whether it removed the whole prefix + removePrefix = (prefix: string): boolean => { + let offset = 0 + // console.log('prefix', prefix, Math.min(this.j, prefix.length - 1)) + while (this.i <= this.j && offset <= prefix.length - 1) { + if (this.originalS.charAt(this.i) !== prefix.charAt(offset)) + break + offset += 1 + this.i += 1 + } + return offset === prefix.length + } + + // // removes suffix from right to left + removeSuffix = (suffix: string): boolean => { + // e.g. suffix =
, the string is 
hi

= 1; len -= 1) { + if (s.endsWith(suffix.substring(0, len))) { // the end of the string equals a prefix + this.j -= len + return len === suffix.length + } + } + return false + } + // removeSuffix = (suffix: string): boolean => { + // let offset = 0 + + // while (this.j >= Math.max(this.i, 0)) { + // if (this.originalS.charAt(this.j) !== suffix.charAt(suffix.length - 1 - offset)) + // break + // offset += 1 + // this.j -= 1 + // } + // return offset === suffix.length + // } + + removeFromStartUntil = (until: string, alsoRemoveUntilStr: boolean) => { + const index = this.originalS.indexOf(until, this.i) + + if (index === -1) { + this.i = this.j + 1 + return false + } + // console.log('index', index, until.length) + + if (alsoRemoveUntilStr) + this.i = index + until.length + else + this.i = index + + return true + } + + + removeCodeBlock = () => { + const pm = this + const foundCodeBlock = pm.removePrefix('```') + if (!foundCodeBlock) return false + + pm.removeFromStartUntil('\n', true) // language + + const foundCodeBlockEnd = pm.removeSuffix('```') + if (!foundCodeBlockEnd) return false + + pm.removeSuffix('\n') + return true + } + + + actualRecentlyAdded = (recentlyAddedTextLen: number) => { + // aaaaaatextaaaaaa{recentlyAdded} + // i ^ j + // | + // recentyAddedIdx + const recentlyAddedIdx = this.j - recentlyAddedTextLen + 1 + return this.originalS.substring(Math.max(this.i, recentlyAddedIdx), this.j + 1) + } + + +} + + + +export const extractCodeFromRegular = ({ text, recentlyAddedTextLen }: { text: string, recentlyAddedTextLen: number }): [string, string] => { + // Match either: + // 1. ```language\n``` + // 2. `````` + + const pm = new SurroundingsRemover(text) + + pm.removeCodeBlock() + + const s = pm.value() + const actual = pm.actualRecentlyAdded(recentlyAddedTextLen) + + return [s, actual] +} + + + + + +// Ollama has its own FIM, we should not use this if we use that +export const extractCodeFromFIM = ({ text, recentlyAddedTextLen, midTag, }: { text: string, recentlyAddedTextLen: number, midTag: string }): [string, string] => { + + /* ------------- summary of the regex ------------- + [optional ` | `` | ```] + (match optional_language_name) + [optional strings here] + [required tag] + (match the stuff between mid tags) + [optional tag] + [optional ` | `` | ```] + */ + + const pm = new SurroundingsRemover(text) + + pm.removeCodeBlock() + + const foundMid = pm.removePrefix(`<${midTag}>`) + + if (foundMid) { + pm.removeSuffix(``) + } + const s = pm.value() + const actual = pm.actualRecentlyAdded(recentlyAddedTextLen) + + return [s, actual] + + + // // const regex = /[\s\S]*?(?:`{1,3}\s*([a-zA-Z_]+[\w]*)?[\s\S]*?)?([\s\S]*?)(?:<\/MID>|`{1,3}|$)/; + // const regex = new RegExp( + // `[\\s\\S]*?(?:\`{1,3}\\s*([a-zA-Z_]+[\\w]*)?[\\s\\S]*?)?<${midTag}>([\\s\\S]*?)(?:|\`{1,3}|$)`, + // '' + // ); + // const match = text.match(regex); + // if (match) { + // const [_, languageName, codeBetweenMidTags] = match; + // return [languageName, codeBetweenMidTags] as const + + // } else { + // return [undefined, extractCodeFromRegular(text)] as const + // } + +} + diff --git a/src/vs/workbench/contrib/void/browser/helpers/findDiffs.ts b/src/vs/workbench/contrib/void/browser/helpers/findDiffs.ts index 8586a3ca..c9235c14 100644 --- a/src/vs/workbench/contrib/void/browser/helpers/findDiffs.ts +++ b/src/vs/workbench/contrib/void/browser/helpers/findDiffs.ts @@ -1,7 +1,7 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Glass Devtools, Inc. All rights reserved. - * Void Editor additions licensed under the AGPL 3.0 License. - *--------------------------------------------------------------------------------------------*/ +/*-------------------------------------------------------------------------------------- + * Copyright 2025 Glass Devtools, Inc. All rights reserved. + * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. + *--------------------------------------------------------------------------------------*/ import { diffLines } from '../react/out/diff/index.js' diff --git a/src/vs/workbench/contrib/void/browser/helpers/getCmdKey.ts b/src/vs/workbench/contrib/void/browser/helpers/getCmdKey.ts deleted file mode 100644 index fe520e42..00000000 --- a/src/vs/workbench/contrib/void/browser/helpers/getCmdKey.ts +++ /dev/null @@ -1,20 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Glass Devtools, Inc. All rights reserved. - * Void Editor additions licensed under the AGPL 3.0 License. - *--------------------------------------------------------------------------------------------*/ - -import { isMacintosh } from '../../../../../base/common/platform.js'; - -// import { OperatingSystem, OS } from '../../../../base/common/platform.js'; -// OS === OperatingSystem.Macintosh -export function getCmdKey(): string { - if (isMacintosh) { - return '⌘'; - } else { - return 'Ctrl'; - } -} - - - - diff --git a/src/vs/workbench/contrib/void/browser/helpers/reactServicesHelper.ts b/src/vs/workbench/contrib/void/browser/helpers/reactServicesHelper.ts deleted file mode 100644 index 3c043344..00000000 --- a/src/vs/workbench/contrib/void/browser/helpers/reactServicesHelper.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { ServicesAccessor } from '../../../../../editor/browser/editorExtensions.js'; -import { IModelService } from '../../../../../editor/common/services/model.js'; -import { IClipboardService } from '../../../../../platform/clipboard/common/clipboardService.js'; -import { IContextViewService, IContextMenuService } from '../../../../../platform/contextview/browser/contextView.js'; -import { IFileService } from '../../../../../platform/files/common/files.js'; -import { IHoverService } from '../../../../../platform/hover/browser/hover.js'; -import { IThemeService } from '../../../../../platform/theme/common/themeService.js'; -import { ILLMMessageService } from '../../../../../platform/void/common/llmMessageService.js'; -import { IRefreshModelService } from '../../../../../platform/void/common/refreshModelService.js'; -import { IVoidSettingsService } from '../../../../../platform/void/common/voidSettingsService.js'; -import { IInlineDiffsService } from '../inlineDiffsService.js'; -import { ISidebarStateService } from '../sidebarStateService.js'; -import { IThreadHistoryService } from '../threadHistoryService.js'; - -export type ReactServicesType = { - sidebarStateService: ISidebarStateService; - settingsStateService: IVoidSettingsService; - threadsStateService: IThreadHistoryService; - fileService: IFileService; - modelService: IModelService; - inlineDiffService: IInlineDiffsService; - llmMessageService: ILLMMessageService; - clipboardService: IClipboardService; - refreshModelService: IRefreshModelService; - - themeService: IThemeService, - hoverService: IHoverService, - - contextViewService: IContextViewService; - contextMenuService: IContextMenuService; -} - - -export const getReactServices = (accessor: ServicesAccessor): ReactServicesType => { - return { - settingsStateService: accessor.get(IVoidSettingsService), - sidebarStateService: accessor.get(ISidebarStateService), - threadsStateService: accessor.get(IThreadHistoryService), - fileService: accessor.get(IFileService), - modelService: accessor.get(IModelService), - inlineDiffService: accessor.get(IInlineDiffsService), - llmMessageService: accessor.get(ILLMMessageService), - clipboardService: accessor.get(IClipboardService), - themeService: accessor.get(IThemeService), - hoverService: accessor.get(IHoverService), - refreshModelService: accessor.get(IRefreshModelService), - contextViewService: accessor.get(IContextViewService), - contextMenuService: accessor.get(IContextMenuService), - } -} - diff --git a/src/vs/workbench/contrib/void/browser/helpers/readFile.ts b/src/vs/workbench/contrib/void/browser/helpers/readFile.ts new file mode 100644 index 00000000..60e5dc5c --- /dev/null +++ b/src/vs/workbench/contrib/void/browser/helpers/readFile.ts @@ -0,0 +1,10 @@ +import { URI } from '../../../../../base/common/uri' +import { EndOfLinePreference } from '../../../../../editor/common/model' +import { IModelService } from '../../../../../editor/common/services/model.js' + +// read files from VSCode +export const VSReadFile = async (modelService: IModelService, uri: URI): Promise => { + const model = modelService.getModel(uri) + if (!model) return null + return model.getValue(EndOfLinePreference.LF) +} diff --git a/src/vs/workbench/contrib/void/browser/inlineDiffsService.ts b/src/vs/workbench/contrib/void/browser/inlineDiffsService.ts index 5d6c0c49..76bcbd8c 100644 --- a/src/vs/workbench/contrib/void/browser/inlineDiffsService.ts +++ b/src/vs/workbench/contrib/void/browser/inlineDiffsService.ts @@ -1,118 +1,224 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Glass Devtools, Inc. All rights reserved. - * Void Editor additions licensed under the AGPL 3.0 License. - *--------------------------------------------------------------------------------------------*/ +/*-------------------------------------------------------------------------------------- + * Copyright 2025 Glass Devtools, Inc. All rights reserved. + * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. + *--------------------------------------------------------------------------------------*/ import { Disposable } from '../../../../base/common/lifecycle.js'; import { registerSingleton, InstantiationType } from '../../../../platform/instantiation/common/extensions.js'; -import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; -import { ICodeEditor, IOverlayWidget, IViewZone } from '../../../../editor/browser/editorBrowser.js'; +import { createDecorator, IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; +import { ICodeEditor, IOverlayWidget, IViewZone, OverlayWidgetPositionPreference } from '../../../../editor/browser/editorBrowser.js'; // import { IUndoRedoService } from '../../../../platform/undoRedo/common/undoRedo.js'; import { ICodeEditorService } from '../../../../editor/browser/services/codeEditorService.js'; // import { throttle } from '../../../../base/common/decorators.js'; -import { writeFileWithDiffInstructions } from './prompt/systemPrompts.js'; import { ComputedDiff, findDiffs } from './helpers/findDiffs.js'; -import { EndOfLinePreference, ITextModel } from '../../../../editor/common/model.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'; import { Color, RGBA } from '../../../../base/common/color.js'; import { IModelService } from '../../../../editor/common/services/model.js'; import { IUndoRedoElement, IUndoRedoService, UndoRedoElementType } from '../../../../platform/undoRedo/common/undoRedo.js'; -import { LineSource, renderLines, RenderOptions } from '../../../../editor/browser/widget/diffEditor/components/diffEditorViewZones/renderLines.js'; -import { LineTokens } from '../../../../editor/common/tokens/lineTokens.js'; -import { ILanguageService } from '../../../../editor/common/languages/language.js'; +import { RenderOptions } from '../../../../editor/browser/widget/diffEditor/components/diffEditorViewZones/renderLines.js'; // import { IModelService } from '../../../../editor/common/services/model.js'; import * as dom from '../../../../base/browser/dom.js'; import { Widget } from '../../../../base/browser/ui/widget.js'; import { URI } from '../../../../base/common/uri.js'; -import { LLMFeatureSelection, ServiceSendLLMMessageParams } from '../../../../platform/void/common/llmMessageTypes.js'; +import { IConsistentEditorItemService, IConsistentItemService } from './helperServices/consistentItemService.js'; +import { ctrlKStream_prefixAndSuffix, ctrlKStream_prompt, ctrlKStream_systemMessage, ctrlLStream_prompt, ctrlLStream_systemMessage, defaultFimTags } from './prompt/prompts.js'; import { ILLMMessageService } from '../../../../platform/void/common/llmMessageService.js'; +import { mountCtrlK } from '../browser/react/out/quick-edit-tsx/index.js' +import { QuickEditPropsType } from './quickEditActions.js'; +import { errorDetails, LLMMessage } from '../../../../platform/void/common/llmMessageTypes.js'; +import { IModelContentChangedEvent } from '../../../../editor/common/textModelEvents.js'; +import { extractCodeFromFIM, extractCodeFromRegular } from './helpers/extractCodeFromResult.js'; +import { IMetricsService } from '../../../../platform/void/common/metricsService.js'; +import { filenameToVscodeLanguage } from './helpers/detectLanguage.js'; +import { INotificationService, Severity } from '../../../../platform/notification/common/notification.js'; +import { isMacintosh } from '../../../../base/common/platform.js'; +import { EditorOption } from '../../../../editor/common/config/editorOptions.js'; +import { Emitter } from '../../../../base/common/event.js'; +import { VOID_OPEN_SETTINGS_ACTION_ID } from './voidSettingsPane.js'; +import { ICommandService } from '../../../../platform/commands/common/commands.js'; -// gets converted to --vscode-void-greenBG, see void.css +const configOfBG = (color: Color) => { + return { dark: color, light: color, hcDark: color, hcLight: color, } +} +// gets converted to --vscode-void-greenBG, see void.css, asCssVariable const greenBG = new Color(new RGBA(155, 185, 85, .3)); // default is RGBA(155, 185, 85, .2) -registerColor('void.greenBG', { - dark: greenBG, - light: greenBG, hcDark: null, hcLight: null -}, '', true); +registerColor('void.greenBG', configOfBG(greenBG), '', true); const redBG = new Color(new RGBA(255, 0, 0, .3)); // default is RGBA(255, 0, 0, .2) -registerColor('void.redBG', { - dark: redBG, - light: redBG, hcDark: null, hcLight: null -}, '', true); +registerColor('void.redBG', configOfBG(redBG), '', true); const sweepBG = new Color(new RGBA(100, 100, 100, .2)); -registerColor('void.sweepBG', { - dark: sweepBG, - light: sweepBG, hcDark: null, hcLight: null -}, '', true); +registerColor('void.sweepBG', configOfBG(sweepBG), '', true); + +const highlightBG = new Color(new RGBA(100, 100, 100, .1)); +registerColor('void.highlightBG', configOfBG(highlightBG), '', true); const sweepIdxBG = new Color(new RGBA(100, 100, 100, .5)); -registerColor('void.sweepIdxBG', { - dark: sweepIdxBG, - light: sweepIdxBG, hcDark: null, hcLight: null -}, '', true); +registerColor('void.sweepIdxBG', configOfBG(sweepIdxBG), '', true); +const getLeadingWhitespacePx = (editor: ICodeEditor, startLine: number): number => { + + const model = editor.getModel(); + if (!model) { + return 0; + } + + // Get the line content, defaulting to empty string if line doesn't exist + const lineContent = model.getLineContent(startLine) || ''; + + // Find the first non-whitespace character + const firstNonWhitespaceIndex = lineContent.search(/\S/); + + // Extract leading whitespace, handling case where line is all whitespace + const leadingWhitespace = firstNonWhitespaceIndex === -1 + ? lineContent + : lineContent.slice(0, firstNonWhitespaceIndex); + + // Get font information from editor render options + const { tabSize: numSpacesInTab } = model.getFormattingOptions(); + 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; + } + } + + return paddingLeft; +}; + +// similar to ServiceLLM +export type StartApplyingOpts = { + featureName: 'Ctrl+K'; + diffareaid: number; // id of the CtrlK area (contains text selection) + userMessage: string; // user message +} | { + featureName: 'Ctrl+L'; + userMessage: string; +} | { + featureName: 'Autocomplete'; + range: IRange; + userMessage: string; +} + +export type AddCtrlKOpts = { + startLine: number, + endLine: number, + editor: ICodeEditor, +} + +// // 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 DiffArea = { + +type CommonZoneProps = { diffareaid: number; - originalCode: string; startLine: number; endLine: number; _URI: URI; // typically we get the URI from model + + _removeStylesFns: Set; // these don't remove diffs or this diffArea, only their styles +} + +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 + +} & CommonZoneProps + + +type DiffZone = { + type: 'DiffZone', + originalCode: string; _diffOfId: Record; // diffid -> diff in this DiffArea -} & ({ - _sweepState: { + _streamState: { isStreaming: true; + streamRequestIdRef: { current: string | null }; line: number; } | { isStreaming: false; - line: null; + streamRequestIdRef?: undefined; + line?: undefined; }; -}) + editorId?: undefined; + linkedStreamingDiffZone?: undefined; +} & CommonZoneProps + + + +// called DiffArea for historical purposes, we can rename to something like TextRegion if we want +type DiffArea = CtrlKZone | DiffZone const diffAreaSnapshotKeys = [ + 'type', 'diffareaid', 'originalCode', 'startLine', 'endLine', + 'editorId', + ] as const satisfies (keyof DiffArea)[] -type DiffAreaSnapshot = Pick +type DiffAreaSnapshot = Pick type HistorySnapshot = { snapshottedDiffAreaOfId: Record; entireFileCode: string; -} & - ({ - type: 'Ctrl+K'; - ctrlKText: string; - } | { - type: 'Ctrl+L'; - }) +} export interface IInlineDiffsService { readonly _serviceBrand: undefined; - startStreaming(params: LLMFeatureSelection, str: string): void; + startApplying(opts: StartApplyingOpts): number | undefined; + interruptStreaming(diffareaid: number): void; + addCtrlKZone(opts: AddCtrlKOpts): number | undefined; + removeCtrlKZone(opts: { diffareaid: number }): void; + // testDiffs(): void; } export const IInlineDiffsService = createDecorator('inlineDiffAreasService'); @@ -120,25 +226,18 @@ export const IInlineDiffsService = createDecorator('inlineD class InlineDiffsService extends Disposable implements IInlineDiffsService { _serviceBrand: undefined; - // state of each document - removeStylesFnsOfUri: Record> = {} // functions that remove the styles of this uri + // URI <--> model diffAreasOfURI: Record> = {} diffAreaOfId: Record = {}; diffOfId: Record = {}; // redundant with diffArea._diffs - _diffareaidPool = 0 // each diffarea has an id - _diffidPool = 0 // each diff has an id - /* - Picture of all the data structures: - () -modelid-> {originalFileStr, Set(diffareaid), state} - ^ | - \________________ diffareaid -> diffarea -> diff[] - ^ | - \____ diff - */ + // only applies to diffZones + // streamingDiffZones: Set = new Set() + private readonly _onDidChangeStreaming = new Emitter<{ uri: URI; diffareaid: number }>(); + private readonly _onDidAddOrDeleteDiffZones = new Emitter<{ uri: URI }>(); constructor( @@ -146,8 +245,13 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService { @ICodeEditorService private readonly _editorService: ICodeEditorService, @IModelService private readonly _modelService: IModelService, @IUndoRedoService private readonly _undoRedoService: IUndoRedoService, // undoRedo service is the history of pressing ctrl+z - @ILanguageService private readonly _langService: ILanguageService, @ILLMMessageService private readonly _llmMessageService: ILLMMessageService, + @IConsistentItemService private readonly _consistentItemService: IConsistentItemService, + @IInstantiationService private readonly _instantiationService: IInstantiationService, + @IConsistentEditorItemService private readonly _consistentEditorItemService: IConsistentEditorItemService, + @IMetricsService private readonly _metricsService: IMetricsService, + @INotificationService private readonly _notificationService: INotificationService, + @ICommandService private readonly _commandService: ICommandService, ) { super(); @@ -156,165 +260,411 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService { if (!(model.uri.fsPath in this.diffAreasOfURI)) { this.diffAreasOfURI[model.uri.fsPath] = new Set(); } - if (!(model.uri.fsPath in this.removeStylesFnsOfUri)) { - this.removeStylesFnsOfUri[model.uri.fsPath] = new Set(); - } // when the user types, realign diff areas and re-render them this._register( model.onDidChangeContent(e => { // it's as if we just called _write, now all we need to do is realign and refresh - if (this._weAreWriting) return + if (this.weAreWriting) return const uri = model.uri - // realign - for (const change of e.changes) { this._realignAllDiffAreasLines(uri, change.text, change.range) } - // refresh - this._refreshDiffsInURI(uri) + this._onUserChangeContent(uri, e) }) ) - } - // initialize all existing models - for (let model of this._modelService.getModels()) { initializeModel(model) } - // initialize whenever a new model mounts - this._register(this._modelService.onModelAdded(model => initializeModel(model))); + // when a stream starts or ends + let removeAcceptRejectAllUI: (() => void) | null = null + const onChangeUriState = () => { + const uri = model.uri + const diffZones = [...this.diffAreasOfURI[uri.fsPath].values()] + .map(diffareaid => this.diffAreaOfId[diffareaid]) + .filter(diffArea => !!diffArea && diffArea.type === 'DiffZone') + const isStreaming = diffZones.find(diffZone => !!diffZone._streamState.isStreaming) + if (diffZones.length !== 0 && !isStreaming && !removeAcceptRejectAllUI) { + removeAcceptRejectAllUI = this._addAcceptRejectUI(uri) ?? null + } else { + removeAcceptRejectAllUI?.() + removeAcceptRejectAllUI = null + } + } + this._register(this._onDidAddOrDeleteDiffZones.event(({ uri: uri_ }) => { if (uri_.fsPath === model.uri.fsPath) onChangeUriState() })) + this._register(this._onDidChangeStreaming.event(({ uri: uri_ }) => { if (uri_.fsPath === model.uri.fsPath) onChangeUriState() })) + } + // initialize all existing models + initialize when a new model mounts + for (let model of this._modelService.getModels()) { initializeModel(model) } + this._register(this._modelService.onModelAdded(model => initializeModel(model))); // this function adds listeners to refresh styles when editor changes tab let initializeEditor = (editor: ICodeEditor) => { const uri = editor.getModel()?.uri ?? null - if (uri) this._refreshDiffsInURI(uri) - - // called when the user switches tabs (typically there's only 1 editor on the screen, make sure you understand this) - this._register(editor.onDidChangeModel((e) => { - if (e.oldModelUrl) this._refreshDiffsInURI(e.oldModelUrl) - if (e.newModelUrl) this._refreshDiffsInURI(e.newModelUrl) - })) + if (uri) this._refreshStylesAndDiffsInURI(uri) } - // add listeners for all existing editors + // add listeners for all existing editors + listen for editor being added for (let editor of this._editorService.listCodeEditors()) { initializeEditor(editor) } - // add listeners when an editor is created - this._register(this._editorService.onCodeEditorAdd(editor => { console.log('ADD EDITOR'); initializeEditor(editor) })) - this._register(this._editorService.onCodeEditorRemove(editor => { console.log('REMOVE EDITOR'); initializeEditor(editor) })) + this._register(this._editorService.onCodeEditorAdd(editor => { initializeEditor(editor) })) } + private _onUserChangeContent(uri: URI, e: IModelContentChangedEvent) { + for (const change of e.changes) { + this._realignAllDiffAreasLines(uri, change.text, change.range) + } + this._refreshStylesAndDiffsInURI(uri) + } + + private _onInternalChangeContent(uri: URI, { shouldRealign }: { shouldRealign: false | { newText: string, oldRange: IRange } }) { + if (shouldRealign) { + const { newText, oldRange } = shouldRealign + // console.log('realiging', newText, oldRange) + this._realignAllDiffAreasLines(uri, newText, oldRange) + } + this._refreshStylesAndDiffsInURI(uri) + + } - - - - - - - - private _addSweepStylesToURI = (uri: URI, sweepLine: number, endLine: number) => { - - const decorationIds: (string | null)[] = [] - - const model = this._getModel(uri) + // highlight the region + private _addLineDecoration = (model: ITextModel | null, startLine: number, endLine: number, className: string, options?: Partial) => { if (model === null) return + const id = model.changeDecorations(accessor => accessor.addDecoration( + { startLineNumber: startLine, startColumn: 1, endLineNumber: endLine, endColumn: Number.MAX_SAFE_INTEGER }, + { + className: className, + description: className, + isWholeLine: true, + ...options + })) + const disposeHighlight = () => { + if (id && !model.isDisposed()) model.changeDecorations(accessor => accessor.removeDecoration(id)) + } + return disposeHighlight + } - // sweepLine ... sweepLine - decorationIds.push( - model.changeDecorations(accessor => accessor.addDecoration( - { startLineNumber: sweepLine, startColumn: 1, endLineNumber: sweepLine, endColumn: Number.MAX_SAFE_INTEGER }, - { - className: 'void-sweepIdxBG', - description: 'void-sweepIdxBG', - isWholeLine: true - })) - ) - // sweepLine+1 ... endLine - decorationIds.push( - model.changeDecorations(accessor => accessor.addDecoration( - { startLineNumber: sweepLine + 1, startColumn: 1, endLineNumber: endLine, endColumn: Number.MAX_SAFE_INTEGER }, - { - className: 'void-sweepBG', - description: 'void-sweepBG', - isWholeLine: true - })) - ) - const disposeSweepStyles = () => { - for (const id of decorationIds) { - if (id) model.changeDecorations(accessor => accessor.removeDecoration(id)) + private _addDiffAreaStylesToURI = (uri: URI) => { + const model = this._getModel(uri) + + for (const diffareaid of this.diffAreasOfURI[uri.fsPath]) { + const diffArea = this.diffAreaOfId[diffareaid] + + if (diffArea.type === 'DiffZone') { + // add sweep styles to the diffZone + if (diffArea._streamState.isStreaming) { + // sweepLine ... sweepLine + const fn1 = this._addLineDecoration(model, diffArea._streamState.line, diffArea._streamState.line, 'void-sweepIdxBG') + // sweepLine+1 ... endLine + const fn2 = diffArea._streamState.line + 1 <= diffArea.endLine ? + this._addLineDecoration(model, diffArea._streamState.line + 1, diffArea.endLine, 'void-sweepBG') + : null + diffArea._removeStylesFns.add(() => { fn1?.(); fn2?.(); }) + + } + } + + else if (diffArea.type === 'CtrlKZone' && diffArea._linkedStreamingDiffZone === null) { + // highlight zone's text + const fn = this._addLineDecoration(model, diffArea.startLine, diffArea.endLine, 'void-highlightBG') + diffArea._removeStylesFns.add(() => fn?.()); } } - return disposeSweepStyles + } + + + private _computeDiffsAndAddStylesToURI = (uri: URI) => { + const fullFileText = this._readURI(uri) ?? '' + + for (const diffareaid of this.diffAreasOfURI[uri.fsPath]) { + const diffArea = this.diffAreaOfId[diffareaid] + if (diffArea.type !== 'DiffZone') continue + + const newDiffAreaCode = fullFileText.split('\n').slice((diffArea.startLine - 1), (diffArea.endLine - 1) + 1).join('\n') + const computedDiffs = findDiffs(diffArea.originalCode, newDiffAreaCode) + for (let computedDiff of computedDiffs) { + if (computedDiff.type === 'deletion') { + computedDiff.startLine += diffArea.startLine - 1 + } + if (computedDiff.type === 'edit' || computedDiff.type === 'insertion') { + computedDiff.startLine += diffArea.startLine - 1 + computedDiff.endLine += diffArea.startLine - 1 + } + this._addDiff(computedDiff, diffArea) + } + + } + } + + private _addAcceptRejectUI(uri: URI) { + + // find all diffzones that aren't streaming + const diffZones: DiffZone[] = [] + for (let diffareaid of this.diffAreasOfURI[uri.fsPath]) { + const diffArea = this.diffAreaOfId[diffareaid] + if (diffArea.type !== 'DiffZone') continue + if (diffArea._streamState.isStreaming) continue + diffZones.push(diffArea) + } + if (diffZones.length === 0) return + + const consistentItemId = this._consistentItemService.addConsistentItemToURI({ + uri, + fn: (editor) => { + const buttonsWidget = new AcceptAllRejectAllWidget({ + editor, + onAcceptAll: () => { + this.removeDiffAreas({ uri, behavior: 'accept', removeCtrlKs: false }) + this._metricsService.capture('Accept All', {}) + }, + onRejectAll: () => { + this.removeDiffAreas({ uri, behavior: 'reject', removeCtrlKs: false }) + this._metricsService.capture('Reject All', {}) + }, + }) + return () => { buttonsWidget.dispose() } + } + }) + + + return () => { this._consistentItemService.removeConsistentItemFromURI(consistentItemId) } + } + + + mostRecentTextOfCtrlKZoneId: Record = {} + private _addCtrlKZoneInput = (ctrlKZone: CtrlKZone) => { + + const { editorId } = ctrlKZone + const editor = this._editorService.listCodeEditors().find(e => e.getId() === editorId) + if (!editor) { return null } + + let zoneId: string | null = null + let viewZone_: IViewZone | null = null + const textAreaRef: { current: HTMLTextAreaElement | null } = { current: null } + + + const paddingLeft = getLeadingWhitespacePx(editor, ctrlKZone.startLine) + + const itemId = this._consistentEditorItemService.addToEditor(editor, () => { + const domNode = document.createElement('div'); + domNode.style.zIndex = '1' + domNode.style.height = 'auto' + domNode.style.paddingLeft = `${paddingLeft}px` + const viewZone: IViewZone = { + afterLineNumber: ctrlKZone.startLine - 1, + domNode: domNode, + // heightInPx: 80, + suppressMouseDown: false, + showInHiddenAreas: true, + }; + viewZone_ = viewZone + + // mount zone + editor.changeViewZones(accessor => { + zoneId = accessor.addZone(viewZone) + }) + + // mount react + this._instantiationService.invokeFunction(accessor => { + mountCtrlK(domNode, accessor, { + + diffareaid: ctrlKZone.diffareaid, + initStreamingDiffZoneId: ctrlKZone._linkedStreamingDiffZone, + + textAreaRef: (r) => { + textAreaRef.current = r + if (!textAreaRef.current) return + + if (!(ctrlKZone.diffareaid in this.mostRecentTextOfCtrlKZoneId)) { // detect first mount this way (a hack) + this.mostRecentTextOfCtrlKZoneId[ctrlKZone.diffareaid] = undefined + setTimeout(() => textAreaRef.current?.focus(), 100) + } + }, + onChangeHeight(height) { + if (height === 0) return // the viewZone sets this height to the container if it's out of view, ignore it + viewZone.heightInPx = height + // re-render with this new height + editor.changeViewZones(accessor => { + if (zoneId) accessor.layoutZone(zoneId) + }) + }, + onChangeText: (text) => { + this.mostRecentTextOfCtrlKZoneId[ctrlKZone.diffareaid] = text; + }, + initText: this.mostRecentTextOfCtrlKZoneId[ctrlKZone.diffareaid] ?? null, + } satisfies QuickEditPropsType) + + }) + + return () => editor.changeViewZones(accessor => { + if (zoneId) + accessor.removeZone(zoneId) + }) + }) + + return { + textAreaRef, + refresh: () => editor.changeViewZones(accessor => { + if (zoneId && viewZone_) { + viewZone_.afterLineNumber = ctrlKZone.startLine - 1 + accessor.layoutZone(zoneId) + } + }), + dispose: () => { + this._consistentEditorItemService.removeFromEditor(itemId) + }, + } satisfies CtrlKZone['_mountInfo'] } + private _refreshCtrlKInputs = async (uri: URI) => { + for (const diffareaid of this.diffAreasOfURI[uri.fsPath]) { + const diffArea = this.diffAreaOfId[diffareaid] + if (diffArea.type !== 'CtrlKZone') continue + if (!diffArea._mountInfo) { + diffArea._mountInfo = this._addCtrlKZoneInput(diffArea) + // console.log('MOUNTED', diffArea.diffareaid) + } + else { + diffArea._mountInfo.refresh() + } + } + } - private _addDiffStylesToEditor = (editor: ICodeEditor, diff: Diff) => { + + private _addDiffStylesToURI = (uri: URI, diff: Diff) => { const { type, diffid } = diff const disposeInThisEditorFns: (() => void)[] = [] - // green decoration and minimap decoration - editor.changeDecorations(accessor => { - if (type === 'deletion') return; + const model = this._modelService.getModel(uri) - const greenRange = { startLineNumber: diff.startLine, startColumn: 1, endLineNumber: diff.endLine, endColumn: Number.MAX_SAFE_INTEGER, } // 1-indexed - const decorationId = accessor.addDecoration(greenRange, { - className: 'void-greenBG', // .monaco-editor .line-insert - description: 'Void added this code', - isWholeLine: true, - minimap: { - color: { id: 'minimapGutter.addedBackground' }, - position: 2 - }, - overviewRuler: { - color: { id: 'editorOverviewRuler.addedForeground' }, - position: 7 - } + // green decoration and minimap decoration + if (type !== 'deletion') { + const fn = this._addLineDecoration(model, diff.startLine, diff.endLine, 'void-greenBG', { + minimap: { color: { id: 'minimapGutter.addedBackground' }, position: 2 }, + overviewRuler: { color: { id: 'editorOverviewRuler.addedForeground' }, position: 7 } }) - disposeInThisEditorFns.push(() => { editor.changeDecorations(accessor => { if (decorationId) accessor.removeDecoration(decorationId) }) }) - }) + disposeInThisEditorFns.push(() => { fn?.() }) + } + // red in a view zone - editor.changeViewZones(accessor => { - if (type === 'insertion') return; + if (type !== 'insertion') { + const consistentZoneId = this._consistentItemService.addConsistentItemToURI({ + uri, + fn: (editor) => { - const domNode = document.createElement('div'); - domNode.className = 'void-redBG' + const domNode = document.createElement('div'); + domNode.className = 'void-redBG' - const renderOptions = RenderOptions.fromEditor(editor); - // applyFontInfo(domNode, renderOptions.fontInfo) + const renderOptions = RenderOptions.fromEditor(editor) - // Compute view-lines based on redText - const redText = diff.originalCode - const lines = redText.split('\n'); - const lineTokens = lines.map(line => LineTokens.createFromTextAndMetadata([{ text: line, metadata: 0 }], this._langService.languageIdCodec)); - const source = new LineSource(lineTokens, lines.map(() => null), false, false) - const result = renderLines(source, renderOptions, [], domNode); + const processedText = diff.originalCode.replace(/\t/g, ' '.repeat(renderOptions.tabSize)); - const viewZone: IViewZone = { - // afterLineNumber: computedDiff.startLine - 1, - afterLineNumber: type === 'edit' ? diff.endLine : diff.startLine - 1, - heightInLines: result.heightInLines, - minWidthInPx: result.minWidthInPx, - domNode: domNode, - marginDomNode: document.createElement('div'), // displayed to left - suppressMouseDown: true, - }; + const lines = processedText.split('\n'); - const zoneId = accessor.addZone(viewZone) - disposeInThisEditorFns.push(() => { editor.changeViewZones(accessor => { if (zoneId) accessor.removeZone(zoneId) }) }) + const linesContainer = document.createElement('div'); + linesContainer.style.fontFamily = renderOptions.fontInfo.fontFamily + linesContainer.style.fontSize = `${renderOptions.fontInfo.fontSize}px` + linesContainer.style.lineHeight = `${renderOptions.fontInfo.lineHeight}px` + // linesContainer.style.tabSize = `${tabWidth}px` // \t + linesContainer.style.whiteSpace = 'pre' + linesContainer.style.position = 'relative' + linesContainer.style.width = '100%' - }); + lines.forEach(line => { + // div for current line + const lineDiv = document.createElement('div'); + lineDiv.className = 'view-line'; + lineDiv.style.whiteSpace = 'pre' + lineDiv.style.position = 'relative' + lineDiv.style.height = `${renderOptions.fontInfo.lineHeight}px` - // Accept | Reject widget - const buttonsWidget = new AcceptRejectWidget({ - editor, - onAccept: () => { this.acceptDiff({ diffid }) }, - onReject: () => { this.rejectDiff({ diffid }) }, - diffid: diffid.toString(), - startLine: diff.startLine, - }) - disposeInThisEditorFns.push(() => { buttonsWidget.dispose() }) + // span (this is just how vscode does it) + const span = document.createElement('span'); + span.textContent = line || '\u00a0'; + span.style.whiteSpace = 'pre' + span.style.display = 'inline-block' + + lineDiv.appendChild(span); + linesContainer.appendChild(lineDiv); + }); + + domNode.appendChild(linesContainer); + + // Calculate height based on number of lines and line height + const heightInLines = lines.length; + const minWidthInPx = Math.max(...lines.map(line => + Math.ceil(renderOptions.fontInfo.typicalFullwidthCharacterWidth * line.length) + )); + + const viewZone: IViewZone = { + afterLineNumber: diff.startLine - 1, + heightInLines, + minWidthInPx, + domNode, + marginDomNode: document.createElement('div'), + suppressMouseDown: false, + showInHiddenAreas: false, + }; + + let zoneId: string | null = null + editor.changeViewZones(accessor => { zoneId = accessor.addZone(viewZone) }) + return () => editor.changeViewZones(accessor => { if (zoneId) accessor.removeZone(zoneId) }) + }, + }) + + disposeInThisEditorFns.push(() => { this._consistentItemService.removeConsistentItemFromURI(consistentZoneId) }) + + } + + + + const diffZone = this.diffAreaOfId[diff.diffareaid] + if (diffZone.type === 'DiffZone' && !diffZone._streamState.isStreaming) { + // Accept | Reject widget + const consistentWidgetId = this._consistentItemService.addConsistentItemToURI({ + uri, + fn: (editor) => { + let startLine: number + let offsetLines: number + if (diff.type === 'insertion' || diff.type === 'edit') { + startLine = diff.startLine // green start + offsetLines = 0 + } + else if (diff.type === 'deletion') { + // if diff.startLine is out of bounds + if (diff.startLine === 1) { + const numRedLines = diff.originalEndLine - diff.originalStartLine + 1 + startLine = diff.startLine + offsetLines = -numRedLines + } + else { + startLine = diff.startLine - 1 + offsetLines = 1 + } + } + else { throw 1 } + + const buttonsWidget = new AcceptRejectWidget({ + editor, + onAccept: () => { + this.acceptDiff({ diffid }) + this._metricsService.capture('Accept Diff', {}) + }, + onReject: () => { + this.rejectDiff({ diffid }) + this._metricsService.capture('Reject Diff', {}) + }, + diffid: diffid.toString(), + startLine, + offsetLines + }) + return () => { buttonsWidget.dispose() } + } + }) + disposeInThisEditorFns.push(() => { this._consistentItemService.removeConsistentItemFromURI(consistentWidgetId) }) + } const disposeInEditor = () => { disposeInThisEditorFns.forEach(f => f()) } return disposeInEditor; @@ -330,24 +680,46 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService { } return model } - private _readURI(uri: URI): string | null { - return this._getModel(uri)?.getValue(EndOfLinePreference.LF) ?? null + private _readURI(uri: URI, range?: IRange): string | null { + if (!range) return this._getModel(uri)?.getValue(EndOfLinePreference.LF) ?? null + else return this._getModel(uri)?.getValueInRange(range, EndOfLinePreference.LF) ?? null } private _getNumLines(uri: URI): number | null { return this._getModel(uri)?.getLineCount() ?? null } + private _getActiveEditorURI(): URI | null { + const editor = this._editorService.getActiveCodeEditor() + if (!editor) return null + const uri = editor.getModel()?.uri + if (!uri) return null + return uri + } - - _weAreWriting = false - private _writeText(uri: URI, text: string, range: IRange) { + weAreWriting = false + private async _writeText(uri: URI, text: string, range: IRange, { shouldRealignDiffAreas }: { shouldRealignDiffAreas: boolean }) { const model = this._getModel(uri) if (!model) return + const uriStr = this._readURI(uri, range) + if (uriStr === null) return - this._weAreWriting = true - model.applyEdits([{ range, text }]) // applies edits without adding them to undo/redo stack - this._weAreWriting = false - this._realignAllDiffAreasLines(uri, text, range) + // heuristic check if don't need to make edits + const dontNeedToWrite = uriStr === text + if (dontNeedToWrite) { + // at the end of a write, we still expect to refresh all styles + // e.g. sometimes we expect to restore all the decorations even if no edits were made when _writeText is used + this._refreshStylesAndDiffsInURI(uri) + return + } + + // minimal edits so not so flashy + // const edits = this.worker.$Void_computeMoreMinimalEdits(uri.toString(), [{ range, text }], false) + this.weAreWriting = true + model.applyEdits([{ range, text }]) + this.weAreWriting = false + + this._onInternalChangeContent(uri, { shouldRealign: shouldRealignDiffAreas && { newText: text, oldRange: range } }) + } @@ -356,11 +728,13 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService { private _addToHistory(uri: URI) { const getCurrentSnapshot = (): HistorySnapshot => { - const diffAreaOfId = this.diffAreaOfId - const snapshottedDiffAreaOfId: Record = {} - for (const diffareaid in diffAreaOfId) { - const diffArea = diffAreaOfId[diffareaid] + + for (const diffareaid in this.diffAreaOfId) { + const diffArea = this.diffAreaOfId[diffareaid] + + if (diffArea._URI.fsPath !== uri.fsPath) continue + snapshottedDiffAreaOfId[diffareaid] = structuredClone( // a structured clone must be on a JSON object Object.fromEntries(diffAreaSnapshotKeys.map(key => [key, diffArea[key]])) ) as DiffAreaSnapshot @@ -368,39 +742,61 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService { return { snapshottedDiffAreaOfId, entireFileCode: this._readURI(uri) ?? '', // the whole file's code - type: 'Ctrl+L', } } const restoreDiffAreas = (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 } = structuredClone(snapshot) // don't want to destroy the snapshot - // delete all current decorations (diffs, sweep styles) so we don't have any unwanted leftover decorations - this._clearAllDiffsAndStyles(uri) - // restore diffAreaOfId and diffAreasOfModelId - this.diffAreaOfId = {} - this.diffAreasOfURI[uri.fsPath].clear() for (const diffareaid in snapshottedDiffAreaOfId) { - this.diffAreaOfId[diffareaid] = { - ...snapshottedDiffAreaOfId[diffareaid], - _diffOfId: {}, - _URI: uri, - _sweepState: { - isStreaming: false, - line: null, - }, + + const snapshottedDiffArea = snapshottedDiffAreaOfId[diffareaid] + + if (snapshottedDiffArea.type === 'DiffZone') { + this.diffAreaOfId[diffareaid] = { + ...snapshottedDiffArea as DiffAreaSnapshot, + 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, + _URI: uri, + _removeStylesFns: new Set(), + _mountInfo: null, + _linkedStreamingDiffZone: null, // when restoring, we will never be streaming + } } this.diffAreasOfURI[uri.fsPath].add(diffareaid) } + this._onDidAddOrDeleteDiffZones.fire({ uri }) // restore file content const numLines = this._getNumLines(uri) if (numLines === null) return - this._writeText(uri, entireModelCode, { startColumn: 1, startLineNumber: 1, endLineNumber: numLines, endColumn: Number.MAX_SAFE_INTEGER }) - // restore all the decorations - this._refreshDiffsInURI(uri) + + this._writeText(uri, entireModelCode, + { startColumn: 1, startLineNumber: 1, endLineNumber: numLines, endColumn: Number.MAX_SAFE_INTEGER }, + { shouldRealignDiffAreas: false } + ) } const beforeSnapshot: HistorySnapshot = getCurrentSnapshot() @@ -424,37 +820,95 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService { // delete diffOfId and diffArea._diffOfId private _deleteDiff(diff: Diff) { const diffArea = this.diffAreaOfId[diff.diffareaid] + if (diffArea.type !== 'DiffZone') return delete diffArea._diffOfId[diff.diffid] delete this.diffOfId[diff.diffid] } - private _deleteDiffs(diffArea: DiffArea) { - for (const diffid in diffArea._diffOfId) { - const diff = diffArea._diffOfId[diffid] + private _deleteDiffs(diffZone: DiffZone) { + for (const diffid in diffZone._diffOfId) { + const diff = diffZone._diffOfId[diffid] this._deleteDiff(diff) } } - private _clearAllDiffsAndStyles(uri: URI) { - for (let diffareaid of this.diffAreasOfURI[uri.fsPath]) { - const diffArea = this.diffAreaOfId[diffareaid] + private _clearAllDiffAreaEffects(diffArea: DiffArea) { + // clear diffZone effects (diffs) + if (diffArea.type === 'DiffZone') this._deleteDiffs(diffArea) - } - for (const removeStyleFn of this.removeStylesFnsOfUri[uri.fsPath]) { - removeStyleFn() - } - this.removeStylesFnsOfUri[uri.fsPath].clear() + + diffArea._removeStylesFns.forEach(removeStyles => removeStyles()) + diffArea._removeStylesFns.clear() } + // clears all Diffs (and their styles) and all styles of DiffAreas, etc + private _clearAllEffects(uri: URI) { + for (let diffareaid of this.diffAreasOfURI[uri.fsPath]) { + const diffArea = this.diffAreaOfId[diffareaid] + this._clearAllDiffAreaEffects(diffArea) + } + } + // delete all diffs, update diffAreaOfId, update diffAreasOfModelId - private _deleteDiffArea(diffArea: DiffArea) { - this._deleteDiffs(diffArea) - delete this.diffAreaOfId[diffArea.diffareaid] - this.diffAreasOfURI[diffArea._URI.fsPath].delete(diffArea.diffareaid.toString()) + private _deleteDiffZone(diffZone: DiffZone) { + this._clearAllDiffAreaEffects(diffZone) + delete this.diffAreaOfId[diffZone.diffareaid] + this.diffAreasOfURI[diffZone._URI.fsPath].delete(diffZone.diffareaid.toString()) + this._onDidAddOrDeleteDiffZones.fire({ uri: diffZone._URI }) } + private _deleteCtrlKZone(ctrlKZone: CtrlKZone) { + this._clearAllEffects(ctrlKZone._URI) + ctrlKZone._mountInfo?.dispose() + delete this.diffAreaOfId[ctrlKZone.diffareaid] + this.diffAreasOfURI[ctrlKZone._URI.fsPath].delete(ctrlKZone.diffareaid.toString()) + } + + + private _deleteAllDiffAreas(uri: URI) { + const diffAreas = this.diffAreasOfURI[uri.fsPath] + diffAreas.forEach(diffareaid => { + const diffArea = this.diffAreaOfId[diffareaid] + if (diffArea.type === 'DiffZone') + this._deleteDiffZone(diffArea) + else if (diffArea.type === 'CtrlKZone') + this._deleteCtrlKZone(diffArea) + }) + } + + + + private _diffareaidPool = 0 // each diffarea has an id + private _addDiffArea(diffArea: Omit): T { + const diffareaid = this._diffareaidPool++ + const diffArea2 = { ...diffArea, diffareaid } as T + this.diffAreasOfURI[diffArea2._URI.fsPath].add(diffareaid.toString()) + this.diffAreaOfId[diffareaid] = diffArea2 + return diffArea2 + } + + private _diffidPool = 0 // each diff has an id + private _addDiff(computedDiff: ComputedDiff, diffZone: DiffZone): Diff { + const uri = diffZone._URI + const diffid = this._diffidPool++ + + // create a Diff of it + const newDiff: Diff = { + ...computedDiff, + diffid: diffid, + diffareaid: diffZone.diffareaid, + } + + const fn = this._addDiffStylesToURI(uri, newDiff) + diffZone._removeStylesFns.add(fn) + + this.diffOfId[diffid] = newDiff + diffZone._diffOfId[diffid] = newDiff + + return newDiff + } @@ -462,171 +916,161 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService { // changes the start/line locations of all DiffAreas on the page (adjust their start/end based on the change) based on the change that was recently made private _realignAllDiffAreasLines(uri: URI, text: string, recentChange: { startLineNumber: number; endLineNumber: number }) { + // console.log('recent change', recentChange) + const model = this._getModel(uri) if (!model) return // compute net number of newlines lines that were added/removed const startLine = recentChange.startLineNumber const endLine = recentChange.endLineNumber - const changeRangeHeight = endLine - startLine + 1 const newTextHeight = (text.match(/\n/g) || []).length + 1 // number of newlines is number of \n's + 1, e.g. "ab\ncd" - const deltaNewlines = newTextHeight - changeRangeHeight - // compute overlap with each diffArea and shrink/elongate each diffArea accordingly for (const diffareaid of this.diffAreasOfURI[model.uri.fsPath] || []) { const diffArea = this.diffAreaOfId[diffareaid] - // if the diffArea is above the range, it is not affected + // if the diffArea is entirely above the range, it is not affected if (diffArea.endLine < startLine) { - console.log('A') + // console.log('CHANGE FULLY BELOW DA (doing nothing)') continue } - - // console.log('Changing DiffArea:', diffArea.startLine, diffArea.endLine) - + // if a diffArea is entirely below the range, shift the diffArea up/down by the delta amount of newlines + else if (endLine < diffArea.startLine) { + // console.log('CHANGE FULLY ABOVE DA') + const changedRangeHeight = endLine - startLine + 1 + const deltaNewlines = newTextHeight - changedRangeHeight + diffArea.startLine += deltaNewlines + diffArea.endLine += deltaNewlines + } // if the diffArea fully contains the change, elongate it by the delta amount of newlines - if (startLine >= diffArea.startLine && endLine <= diffArea.endLine) { + else if (startLine >= diffArea.startLine && endLine <= diffArea.endLine) { + // console.log('DA FULLY CONTAINS CHANGE') + const changedRangeHeight = endLine - startLine + 1 + const deltaNewlines = newTextHeight - changedRangeHeight diffArea.endLine += deltaNewlines } // if the change fully contains the diffArea, make the diffArea have the same range as the change else if (diffArea.startLine > startLine && diffArea.endLine < endLine) { - + // console.log('CHANGE FULLY CONTAINS DA') diffArea.startLine = startLine diffArea.endLine = startLine + newTextHeight - console.log('B', diffArea.startLine, diffArea.endLine) } // if the change contains only the diffArea's top - else if (diffArea.startLine > startLine) { - // TODO fill in this case - console.log('C', diffArea.startLine, diffArea.endLine) + else if (startLine < diffArea.startLine && diffArea.startLine <= endLine) { + // console.log('CHANGE CONTAINS TOP OF DA ONLY') + const numOverlappingLines = endLine - diffArea.startLine + 1 + const numRemainingLinesInDA = diffArea.endLine - diffArea.startLine + 1 - numOverlappingLines + const newHeight = (numRemainingLinesInDA - 1) + (newTextHeight - 1) + 1 + diffArea.startLine = startLine + diffArea.endLine = startLine + newHeight } // if the change contains only the diffArea's bottom - else if (diffArea.endLine < endLine) { + else if (startLine <= diffArea.endLine && diffArea.endLine < endLine) { + // console.log('CHANGE CONTAINS BOTTOM OF DA ONLY') const numOverlappingLines = diffArea.endLine - startLine + 1 - diffArea.endLine += newTextHeight - numOverlappingLines // TODO double check this - console.log('D', diffArea.startLine, diffArea.endLine) + diffArea.endLine += newTextHeight - numOverlappingLines } - // if a diffArea is below the last character of the change, shift the diffArea up/down by the delta amount of newlines - else if (diffArea.startLine > endLine) { - diffArea.startLine += deltaNewlines - diffArea.endLine += deltaNewlines - console.log('E', diffArea.startLine, diffArea.endLine) - } - - // console.log('To:', diffArea.startLine, diffArea.endLine) } } - private _refreshDiffsInURI(uri: URI) { - const content = this._readURI(uri) - if (content === null) return + private _refreshStylesAndDiffsInURI(uri: URI) { - // 1. clear Diffs and styles - this._clearAllDiffsAndStyles(uri) + // 1. clear DiffArea styles and Diffs + this._clearAllEffects(uri) - // 2. recompute all diffs on each editor with this URI - const editors = this._editorService.listCodeEditors().filter(editor => editor.getModel()?.uri.fsPath === uri.fsPath) - const fullFileText = this._readURI(uri) ?? '' - - - // go thru all diffareas in this URI, creating diffs and adding styles to it - for (let diffareaid of this.diffAreasOfURI[uri.fsPath]) { - const diffArea = this.diffAreaOfId[diffareaid] - - const newDiffAreaCode = fullFileText.split('\n').slice((diffArea.startLine - 1), (diffArea.endLine - 1) + 1).join('\n') - const computedDiffs = findDiffs(diffArea.originalCode, newDiffAreaCode) - - for (let computedDiff of computedDiffs) { - const diffid = this._diffidPool++ - - // create a Diff of it - const newDiff: Diff = { - ...computedDiff, - diffid: diffid, - diffareaid: diffArea.diffareaid, - } - - for (let editor of editors) { - const fn = this._addDiffStylesToEditor(editor, newDiff) - this.removeStylesFnsOfUri[uri.fsPath].add(() => fn()) - } - - this.diffOfId[diffid] = newDiff - diffArea._diffOfId[diffid] = newDiff - } - - if (diffArea._sweepState.isStreaming) { - const fn = this._addSweepStylesToURI(uri, diffArea._sweepState.line, diffArea.endLine) - this.removeStylesFnsOfUri[uri.fsPath].add(() => fn?.()) - } - } + // 2. style DiffAreas (sweep, etc) + this._addDiffAreaStylesToURI(uri) + // 3. add Diffs + this._computeDiffsAndAddStylesToURI(uri) + // 4. refresh ctrlK zones + this._refreshCtrlKInputs(uri) } + + // @throttle(100) - private _writeDiffAreaLLMText(diffArea: DiffArea, newCodeSoFar: string) { + private _writeStreamedDiffZoneLLMText(diffZone: DiffZone, llmText: string, deltaText: string, latest: { line: number, col: number, addedSplitYet: boolean, originalCodeStartLine: number }) { // ----------- 1. Write the new code to the document ----------- // figure out where to highlight based on where the AI is in the stream right now, use the last diff to figure that out - const uri = diffArea._URI - const computedDiffs = findDiffs(diffArea.originalCode, newCodeSoFar) + const uri = diffZone._URI + const computedDiffs = findDiffs(diffZone.originalCode, llmText) - // if not streaming, just write the new code - if (!diffArea._sweepState.isStreaming) { - this._writeText(uri, newCodeSoFar, - { startLineNumber: diffArea.startLine, startColumn: 1, endLineNumber: diffArea.endLine, endColumn: Number.MAX_SAFE_INTEGER, } // 1-indexed - ) + // should always be in streaming state here + if (!diffZone._streamState.isStreaming) { + console.error('DiffZone was not in streaming state on _writeDiffZoneLLMText') + return } + // if streaming, use diffs to figure out where to write new code - else { - // these are two different coordinate systems - new and old line number - let newFileEndLine: number // get new[0...newStoppingPoint] with line=newStoppingPoint highlighted - let oldFileStartLine: number // get original[oldStartingPoint...] + // these are two different coordinate systems - new and old line number + let newCodeEndLine: number // get file[diffArea.startLine...newFileEndLine] with line=newFileEndLine highlighted + let originalCodeStartLine: number // get original[oldStartingPoint...] (line in the original code, so starts at 1) - const lastDiff = computedDiffs.pop() - - if (!lastDiff) { - // if the writing is identical so far, display no changes - newFileEndLine = 1 - oldFileStartLine = 1 - } - else { - if (lastDiff.type === 'insertion') { - newFileEndLine = lastDiff.endLine - oldFileStartLine = lastDiff.originalStartLine - } - else if (lastDiff.type === 'deletion') { - newFileEndLine = lastDiff.startLine - oldFileStartLine = lastDiff.originalStartLine - } - else if (lastDiff.type === 'edit') { - newFileEndLine = lastDiff.endLine - oldFileStartLine = lastDiff.originalStartLine - } - else { - throw new Error(`Void: diff.type not recognized on: ${lastDiff}`) - } - } - - diffArea._sweepState.line = newFileEndLine - - // lines are 1-indexed - const newFileTop = newCodeSoFar.split('\n').slice(0, (newFileEndLine - 1)).join('\n') - const oldFileBottom = diffArea.originalCode.split('\n').slice((oldFileStartLine - 1), Infinity).join('\n') - - const newCode = `${newFileTop}\n${oldFileBottom}` - - this._writeText(uri, newCode, - { startLineNumber: diffArea.startLine, startColumn: 1, endLineNumber: diffArea.endLine, endColumn: Number.MAX_SAFE_INTEGER, } // 1-indexed - ) + const lastDiff = computedDiffs.pop() + if (!lastDiff) { + // console.log('!lastDiff') + // if the writing is identical so far, display no changes + originalCodeStartLine = 1 + newCodeEndLine = 1 } + else { + originalCodeStartLine = lastDiff.originalStartLine + if (lastDiff.type === 'insertion' || lastDiff.type === 'edit') + newCodeEndLine = lastDiff.endLine + else if (lastDiff.type === 'deletion') + newCodeEndLine = lastDiff.startLine + else + throw new Error(`Void: diff.type not recognized on: ${lastDiff}`) + } + + + + // at the start, add a newline between the stream and originalCode to make reasoning easier + if (!latest.addedSplitYet) { + this._writeText(uri, '\n', + { startLineNumber: latest.line, startColumn: latest.col, endLineNumber: latest.line, endColumn: latest.col, }, + { shouldRealignDiffAreas: true } + ) + latest.addedSplitYet = true + } + + // insert deltaText at latest line and col + this._writeText(uri, deltaText, + { startLineNumber: latest.line, startColumn: latest.col, endLineNumber: latest.line, endColumn: latest.col }, + { shouldRealignDiffAreas: true } + ) + latest.line += deltaText.split('\n').length - 1 + const lastNewlineIdx = deltaText.lastIndexOf('\n') + latest.col = lastNewlineIdx === -1 ? latest.col + deltaText.length : deltaText.length - lastNewlineIdx + + // delete or insert to get original up to speed + if (latest.originalCodeStartLine < originalCodeStartLine) { + // moved up, delete + const numLinesDeleted = originalCodeStartLine - latest.originalCodeStartLine + this._writeText(uri, '', + { startLineNumber: latest.line, startColumn: latest.col, endLineNumber: latest.line + numLinesDeleted, endColumn: Number.MAX_SAFE_INTEGER, }, + { shouldRealignDiffAreas: true } + ) + } + else if (latest.originalCodeStartLine > originalCodeStartLine) { + this._writeText(uri, '\n' + diffZone.originalCode.split('\n').slice((originalCodeStartLine - 1), (latest.originalCodeStartLine - 1) - 1 + 1).join('\n'), + { startLineNumber: latest.line, startColumn: latest.col, endLineNumber: latest.line, endColumn: latest.col }, + { shouldRealignDiffAreas: true } + ) + } + latest.originalCodeStartLine = originalCodeStartLine + + // add diffZone.startLine to convert to right coordinate system (line in file, not in diffarea) + diffZone._streamState.line = (diffZone.startLine - 1) + newCodeEndLine return computedDiffs @@ -634,145 +1078,412 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService { + // // if streaming, use diffs to figure out where to write new code + // // these are two different coordinate systems - new and old line number + // let newFileEndLine: number // get new[0...newStoppingPoint] with line=newStoppingPoint highlighted + // let originalCodeStartLine: number // get original[oldStartingPoint...] - private async _initializeStream(opts: LLMFeatureSelection, diffRepr: string, uri: URI,) { + // const lastDiff = computedDiffs.pop() - // diff area begin and end line - const numLines = this._getNumLines(uri) - if (numLines === null) return + // if (!lastDiff) { + // // if the writing is identical so far, display no changes + // newFileEndLine = diffZone.startLine + // originalCodeStartLine = 1 + // } + // else { + // if (lastDiff.type === 'insertion') { + // newFileEndLine = lastDiff.endLine + // originalCodeStartLine = lastDiff.originalStartLine + // } + // else if (lastDiff.type === 'deletion') { + // newFileEndLine = lastDiff.startLine + // originalCodeStartLine = lastDiff.originalStartLine + // } + // else if (lastDiff.type === 'edit') { + // newFileEndLine = lastDiff.endLine + // originalCodeStartLine = lastDiff.originalStartLine + // } + // else { + // throw new Error(`Void: diff.type not recognized on: ${lastDiff}`) + // } + // } - const beginLine = 1 - const endLine = numLines + // diffZone._streamState.line = newFileEndLine - // check if there's overlap with any other diffAreas and return early if there is - for (const diffareaid of this.diffAreasOfURI[uri.fsPath]) { - const da2 = this.diffAreaOfId[diffareaid] - if (!da2) continue - const noOverlap = da2.startLine > endLine || da2.endLine < beginLine - if (!noOverlap) { - // TODO add a message here that says this to the user too - console.error('Not diffing because found overlap:', this.diffAreasOfURI[uri.fsPath], beginLine, endLine) - return - } - } + // // lines are 1-indexed + // const newFileTop = llmText.split('\n').slice(diffZone.startLine, (newFileEndLine - 1)).join('\n') + // const oldFileBottom = diffZone.originalCode.split('\n').slice((originalCodeStartLine - 1), Infinity).join('\n') - const currentFileStr = this._readURI(uri) - if (currentFileStr === null) return - const originalCode = currentFileStr.split('\n').slice((beginLine - 1), (endLine - 1) + 1).join('\n') + // const newCode = `${newFileTop}\n${oldFileBottom}` - // add to history - const { onFinishEdit } = this._addToHistory(uri) - - // create a diffArea for the stream - const diffareaid = this._diffareaidPool++ - - // in ctrl+L the start and end lines are the full document - const diffArea: DiffArea = { - diffareaid: diffareaid, - // originalStartLine: beginLine, - // originalEndLine: endLine, - originalCode: originalCode, - startLine: beginLine, - endLine: endLine, // starts out the same as the current file - _URI: uri, - _sweepState: { - isStreaming: true, - line: 1, - }, - _diffOfId: {}, // added later - } - - console.log('adding uri.fspath', uri.fsPath, diffArea.diffareaid.toString()) - this.diffAreasOfURI[uri.fsPath].add(diffArea.diffareaid.toString()) - this.diffAreaOfId[diffArea.diffareaid] = diffArea - - // actually call the LLM - const promptContent = `\ -ORIGINAL_CODE -\`\`\` -${originalCode} -\`\`\` - -DIFF -\`\`\` -${diffRepr} -\`\`\` - -INSTRUCTIONS -Please finish writing the new file by applying the diff to the original file. Return ONLY the completion of the file, without any explanation. -` + // this._writeText(uri, newCode, + // { startLineNumber: diffZone.startLine, startColumn: 1, endLineNumber: diffZone.endLine, endColumn: Number.MAX_SAFE_INTEGER, }, // 1-indexed + // { shouldRealignDiffAreas: true } + // ) - await new Promise((resolve, reject) => { - - let streamRequestId: string | null = null - - const object: ServiceSendLLMMessageParams = { - logging: { loggingName: 'streamChunk' }, - messages: [ - { role: 'system', content: writeFileWithDiffInstructions, }, - // TODO include more context too - { role: 'user', content: promptContent, } - ], - onText: ({ newText, fullText }) => { - this._writeDiffAreaLLMText(diffArea, fullText) - this._refreshDiffsInURI(uri) - }, - onFinalMessage: ({ fullText }) => { - this._writeText(uri, fullText, - { startLineNumber: diffArea.startLine, startColumn: 1, endLineNumber: diffArea.endLine, endColumn: Number.MAX_SAFE_INTEGER }, // 1-indexed - ) - diffArea._sweepState = { isStreaming: false, line: null } - this._refreshDiffsInURI(uri) - resolve(); - }, - onError: (e: any) => { - console.error('Error rewriting file with diff', e); - // TODO indicate there was an error - if (streamRequestId) - this._llmMessageService.abort(streamRequestId) - - diffArea._sweepState = { isStreaming: false, line: null } - resolve(); - }, - ...opts - } - - streamRequestId = this._llmMessageService.sendLLMMessage(object) - }) - - onFinishEdit() - - } + // return computedDiffs - async startStreaming(opts: LLMFeatureSelection, userMessage: string) { - - const editor = this._editorService.getActiveCodeEditor() - if (!editor) return + // called first, then call startApplying + public addCtrlKZone({ startLine, endLine, editor }: AddCtrlKOpts) { const uri = editor.getModel()?.uri if (!uri) return - // TODO reject all diffs in the diff area + // check if there's overlap with any other ctrlKZone and if so, focus it + const overlappingCtrlKZone = this._findOverlappingDiffArea({ startLine, endLine, uri, filter: (diffArea) => diffArea.type === 'CtrlKZone' }) + if (overlappingCtrlKZone) { + editor.revealLine(overlappingCtrlKZone.startLine) // important + setTimeout(() => (overlappingCtrlKZone as CtrlKZone)._mountInfo?.textAreaRef.current?.focus(), 100) + return + } - // TODO deselect user's cursor + const overlappingDiffZone = this._findOverlappingDiffArea({ startLine, endLine, uri, filter: (diffArea) => diffArea.type === 'DiffZone' }) + if (overlappingDiffZone) + return - this._initializeStream(opts, userMessage, uri) + editor.revealLine(startLine) + editor.setSelection({ startLineNumber: startLine, endLineNumber: startLine, startColumn: 1, endColumn: 1 }) + + const { onFinishEdit } = this._addToHistory(uri) + + const adding: Omit = { + type: 'CtrlKZone', + startLine: startLine, + endLine: endLine, + editorId: editor.getId(), + _URI: uri, + _removeStylesFns: new Set(), + _mountInfo: null, + _linkedStreamingDiffZone: null, + } + const ctrlKZone = this._addDiffArea(adding) + this._refreshStylesAndDiffsInURI(uri) + + onFinishEdit() + return ctrlKZone.diffareaid + } + + // _remove means delete and also add to history + public removeCtrlKZone({ diffareaid }: { diffareaid: number }) { + const ctrlKZone = this.diffAreaOfId[diffareaid] + if (!ctrlKZone) return + if (ctrlKZone.type !== 'CtrlKZone') return + + const uri = ctrlKZone._URI + const { onFinishEdit } = this._addToHistory(uri) + this._deleteCtrlKZone(ctrlKZone) + this._refreshStylesAndDiffsInURI(uri) + onFinishEdit() } - interruptStreaming() { - // TODO add abort + + public startApplying(opts: StartApplyingOpts) { + const addedDiffZone = this._initializeStartApplying(opts) + return addedDiffZone?.diffareaid } + private _findOverlappingDiffArea({ startLine, endLine, uri, filter }: { startLine: number, endLine: number, uri: URI, filter?: (diffArea: DiffArea) => boolean }): DiffArea | null { + // check if there's overlap with any other diffAreas and return early if there is + for (const diffareaid of this.diffAreasOfURI[uri.fsPath]) { + const diffArea = this.diffAreaOfId[diffareaid] + if (!diffArea) continue + if (!filter?.(diffArea)) continue + const noOverlap = diffArea.startLine > endLine || diffArea.endLine < startLine + if (!noOverlap) { + return diffArea + } + } + return null + } + + + private _initializeStartApplying(opts: StartApplyingOpts): DiffZone | undefined { + + const { featureName } = opts + + let startLine: number + let endLine: number + let uri: URI + let userMessage: string + + if (featureName === 'Ctrl+L') { + + const uri_ = this._getActiveEditorURI() + if (!uri_) return + uri = uri_ + + // reject all diffZones on this URI, adding to history (there can't possibly be overlap after this) + this.removeDiffAreas({ uri, behavior: 'reject', removeCtrlKs: true }) + + // in ctrl+L the start and end lines are the full document + const numLines = this._getNumLines(uri) + if (numLines === null) return + startLine = 1 + endLine = numLines + + userMessage = opts.userMessage + } + else if (featureName === 'Ctrl+K') { + const { diffareaid } = opts + const ctrlKZone = this.diffAreaOfId[diffareaid] + if (ctrlKZone.type !== 'CtrlKZone') return + + const { startLine: startLine_, endLine: endLine_, _URI, _mountInfo } = ctrlKZone + uri = _URI + startLine = startLine_ + endLine = endLine_ + + if (!_mountInfo?.textAreaRef.current) return + userMessage = _mountInfo.textAreaRef.current?.value + } + else { + throw new Error(`Void: diff.type not recognized on: ${featureName}`) + } + + const currentFileStr = this._readURI(uri) + if (currentFileStr === null) return + const originalCode = currentFileStr.split('\n').slice((startLine - 1), (endLine - 1) + 1).join('\n') + + + let streamRequestIdRef: { current: string | null } = { current: null } + + + // add to history + const { onFinishEdit } = this._addToHistory(uri) + + // __TODO__ let users customize modelFimTags + const isOllamaFIM = false // this._voidSettingsService.state.modelSelectionOfFeature['Ctrl+K']?.providerName === 'ollama' + const modelFimTags = defaultFimTags + + const adding: Omit = { + type: 'DiffZone', + originalCode, + startLine, + endLine, + _URI: uri, + _streamState: { + isStreaming: true, + streamRequestIdRef, + line: startLine, + }, + _diffOfId: {}, // added later + _removeStylesFns: new Set(), + } + const diffZone = this._addDiffArea(adding) + this._onDidChangeStreaming.fire({ uri, diffareaid: diffZone.diffareaid }) + this._onDidAddOrDeleteDiffZones.fire({ uri }) + + if (featureName === 'Ctrl+K') { + const { diffareaid } = opts + const ctrlKZone = this.diffAreaOfId[diffareaid] + if (ctrlKZone.type !== 'CtrlKZone') return + + ctrlKZone._linkedStreamingDiffZone = diffZone.diffareaid + } + + // now handle messages + let messages: LLMMessage[] + + if (featureName === 'Ctrl+L') { + const userContent = ctrlLStream_prompt({ originalCode, userMessage, uri }) + messages = [ + { role: 'system', content: ctrlLStream_systemMessage, }, + { role: 'user', content: userContent, } + ] + } + else if (featureName === 'Ctrl+K') { + const { prefix, suffix } = ctrlKStream_prefixAndSuffix({ fullFileStr: currentFileStr, startLine, endLine }) + // console.log('PREFIX:\n', prefix) + // console.log('SUFFIX:\n', suffix) + // console.log('USER CONTENT:\n', userContent) + + // __TODO__ use Ollama's FIM api + // if (isOllamaFIM) {...} else: + const language = filenameToVscodeLanguage(uri.fsPath) ?? '' + const userContent = ctrlKStream_prompt({ selection: originalCode, userMessage, prefix, suffix, isOllamaFIM: false, fimTags: modelFimTags, language }) + messages = [ + { role: 'system', content: ctrlKStream_systemMessage, }, + { role: 'user', content: userContent, } + ] + } + else { throw new Error(`featureName ${featureName} is invalid`) } + + + const onDone = (hadError: boolean) => { + diffZone._streamState = { isStreaming: false, } + this._onDidChangeStreaming.fire({ uri, diffareaid: diffZone.diffareaid }) + + if (featureName === 'Ctrl+K') { + const ctrlKZone = this.diffAreaOfId[opts.diffareaid] as CtrlKZone + + ctrlKZone._linkedStreamingDiffZone = null + this._deleteCtrlKZone(ctrlKZone) + } + this._refreshStylesAndDiffsInURI(uri) + onFinishEdit() + + // if had error, revert! + if (hadError) { + this._undoHistory(diffZone._URI) + } + } + + // refresh now in case onText takes a while to get 1st message + this._refreshStylesAndDiffsInURI(uri) + + + const extractText = (fullText: string, recentlyAddedTextLen: number) => { + if (featureName === 'Ctrl+K') { + if (isOllamaFIM) return fullText + return extractCodeFromFIM({ text: fullText, recentlyAddedTextLen, midTag: modelFimTags.midTag }) + } + else if (featureName === 'Ctrl+L') { + return extractCodeFromRegular({ text: fullText, recentlyAddedTextLen }) + } + throw 1 + } + + const latestStreamInfo = { line: diffZone.startLine, addedSplitYet: false, col: 1, originalCodeStartLine: 1 } + streamRequestIdRef.current = this._llmMessageService.sendLLMMessage({ + useProviderFor: featureName, + logging: { loggingName: `startApplying - ${featureName}` }, + messages, + onText: ({ newText, fullText }) => { + const [text, deltaText] = extractText(fullText, newText.length) + + this._writeStreamedDiffZoneLLMText(diffZone, text, deltaText, latestStreamInfo) + this._refreshStylesAndDiffsInURI(uri) + }, + onFinalMessage: ({ fullText }) => { + // console.log('DONE! FULL TEXT\n', extractText(fullText), diffZone.startLine, diffZone.endLine) + // at the end, re-write whole thing to make sure no sync errors + const [text, _] = extractText(fullText, 0) + this._writeText(uri, text, + { startLineNumber: diffZone.startLine, startColumn: 1, endLineNumber: diffZone.endLine, endColumn: Number.MAX_SAFE_INTEGER }, // 1-indexed + { shouldRealignDiffAreas: true } + ) + onDone(false) + }, + onError: (e) => { + const details = errorDetails(e.fullError) + this._notificationService.notify({ + severity: Severity.Warning, + message: `Void Error: ${e.message}`, + actions: { + secondary: [{ + id: 'void.onerror.opensettings', + enabled: true, + label: 'Open Void settings', + tooltip: '', + class: undefined, + run: () => { this._commandService.executeCommand(VOID_OPEN_SETTINGS_ACTION_ID) } + }] + }, + source: details ? `(Hold ${isMacintosh ? 'Option' : 'Alt'} to hover) - ${details}` : undefined + }) + onDone(true) + }, + + range: { startLineNumber: startLine, endLineNumber: endLine, startColumn: 1, endColumn: Number.MAX_SAFE_INTEGER }, + }) + + return diffZone + + } + + + + + private _stopIfStreaming(diffZone: DiffZone) { + const uri = diffZone._URI + + const streamRequestId = diffZone._streamState.streamRequestIdRef?.current + if (!streamRequestId) return + + this._llmMessageService.abort(streamRequestId) + + diffZone._streamState = { isStreaming: false, } + this._onDidChangeStreaming.fire({ uri, diffareaid: diffZone.diffareaid }) + } + + _undoHistory(uri: URI) { + this._undoRedoService.undo(uri) + } + + // call this outside undo/redo (it calls undo). this is only for aborting a diffzone stream + interruptStreaming(diffareaid: number) { + const diffArea = this.diffAreaOfId[diffareaid] + + if (!diffArea) return + if (diffArea.type !== 'DiffZone') return + if (!diffArea._streamState.isStreaming) return + + this._stopIfStreaming(diffArea) + this._undoHistory(diffArea._URI) + } + + + + + + + + // public removeDiffZone(diffZone: DiffZone, behavior: 'reject' | 'accept') { + // const uri = diffZone._URI + // const { onFinishEdit } = this._addToHistory(uri) + + // if (behavior === 'reject') this._revertAndDeleteDiffZone(diffZone) + // else if (behavior === 'accept') this._deleteDiffZone(diffZone) + + // this._refreshStylesAndDiffsInURI(uri) + // onFinishEdit() + // } + + private _revertAndDeleteDiffZone(diffZone: DiffZone) { + const uri = diffZone._URI + + const writeText = diffZone.originalCode + const toRange: IRange = { startLineNumber: diffZone.startLine, startColumn: 1, endLineNumber: diffZone.endLine, endColumn: Number.MAX_SAFE_INTEGER } + this._writeText(uri, writeText, toRange, { shouldRealignDiffAreas: true }) + + this._deleteDiffZone(diffZone) + } + + + // remove a batch of diffareas all at once (and handle accept/reject of their diffs) + public removeDiffAreas({ uri, removeCtrlKs, behavior }: { uri: URI, removeCtrlKs: boolean, behavior: 'reject' | 'accept' }) { + + const diffareaids = this.diffAreasOfURI[uri.fsPath] + if (diffareaids.size === 0) return // do nothing + + const { onFinishEdit } = this._addToHistory(uri) + + for (const diffareaid of diffareaids) { + const diffArea = this.diffAreaOfId[diffareaid] + if (!diffArea) continue + + if (diffArea.type == 'DiffZone') { + if (behavior === 'reject') this._revertAndDeleteDiffZone(diffArea) + else if (behavior === 'accept') this._deleteDiffZone(diffArea) + } + else if (diffArea.type === 'CtrlKZone' && removeCtrlKs) { + this._deleteCtrlKZone(diffArea) + } + } + + this._refreshStylesAndDiffsInURI(uri) + onFinishEdit() + } @@ -786,6 +1497,8 @@ Please finish writing the new file by applying the diff to the original file. Re const diffArea = this.diffAreaOfId[diffareaid] if (!diffArea) return + if (diffArea.type !== 'DiffZone') return + const uri = diffArea._URI // add to history @@ -832,10 +1545,10 @@ Please finish writing the new file by applying the diff to the original file. Re // diffArea should be removed if it has no more diffs in it if (Object.keys(diffArea._diffOfId).length === 0) { - this._deleteDiffArea(diffArea) + this._deleteDiffZone(diffArea) } - this._refreshDiffsInURI(uri) + this._refreshStylesAndDiffsInURI(uri) onFinishEdit() @@ -853,6 +1566,8 @@ Please finish writing the new file by applying the diff to the original file. Re const diffArea = this.diffAreaOfId[diffareaid] if (!diffArea) return + if (diffArea.type !== 'DiffZone') return + const uri = diffArea._URI // add to history @@ -867,8 +1582,15 @@ Please finish writing the new file by applying the diff to the original file. Re // |B <-- deleted here, diff.startLine == diff.endLine // C if (diff.type === 'deletion') { - writeText = diff.originalCode + '\n' - toRange = { startLineNumber: diff.startLine, startColumn: 1, endLineNumber: diff.startLine, endColumn: 1 } + // if startLine is out of bounds (deleted lines past the diffarea), applyEdit will do a weird rounding thing, to account for that we apply the edit the line before + if (diff.startLine - 1 === diffArea.endLine) { + writeText = '\n' + diff.originalCode + toRange = { startLineNumber: diff.startLine - 1, startColumn: Number.MAX_SAFE_INTEGER, endLineNumber: diff.startLine - 1, endColumn: Number.MAX_SAFE_INTEGER } + } + else { + writeText = diff.originalCode + '\n' + toRange = { startLineNumber: diff.startLine, startColumn: 1, endLineNumber: diff.startLine, endColumn: 1 } + } } // if it was an insertion, need to delete all the lines // (this image applies to writeText and toRange, not newOriginalCode) @@ -876,8 +1598,18 @@ Please finish writing the new file by applying the diff to the original file. Re // B| <-- endLine (we want to delete this whole line) // C else if (diff.type === 'insertion') { - writeText = '' - toRange = { startLineNumber: diff.startLine, startColumn: 1, endLineNumber: diff.endLine + 1, endColumn: 1 } // 1-indexed + // console.log('REJECTING:', diff) + // handle the case where the insertion was a newline at end of diffarea (applying to the next line doesnt work because it doesnt exist, vscode just doesnt delete the correct # of newlines) + if (diff.endLine === diffArea.endLine) { + // delete the line before instead of after + writeText = '' + toRange = { startLineNumber: diff.startLine - 1, startColumn: Number.MAX_SAFE_INTEGER, endLineNumber: diff.endLine, endColumn: 1 } // 1-indexed + } + else { + writeText = '' + toRange = { startLineNumber: diff.startLine, startColumn: 1, endLineNumber: diff.endLine + 1, endColumn: 1 } // 1-indexed + } + } // if it was an edit, just edit the range // (this image applies to writeText and toRange, not newOriginalCode) @@ -893,7 +1625,7 @@ Please finish writing the new file by applying the diff to the original file. Re } // update the file - this._writeText(uri, writeText, toRange) + this._writeText(uri, writeText, toRange, { shouldRealignDiffAreas: true }) // originalCode does not change! @@ -902,21 +1634,69 @@ Please finish writing the new file by applying the diff to the original file. Re // diffArea should be removed if it has no more diffs in it if (Object.keys(diffArea._diffOfId).length === 0) { - this._deleteDiffArea(diffArea) + this._deleteDiffZone(diffArea) } - this._refreshDiffsInURI(uri) + this._refreshStylesAndDiffsInURI(uri) onFinishEdit() } + + + + // testDiffs(): DiffZone | undefined { + // const uri = this._getActiveEditorURI() + // if (!uri) return + + // const startLine = 1 + // const endLine = 4 + + // const currentFileStr = this._readURI(uri) + // if (currentFileStr === null) return + // const originalCode = currentFileStr.split('\n').slice((startLine - 1), (endLine - 1) + 1).join('\n') + + // const { onFinishEdit } = this._addToHistory(uri) + // const adding: Omit = { + // type: 'DiffZone', + // originalCode, + // startLine, + // endLine, + // _URI: uri, + // _streamState: { isStreaming: false, }, + // _diffOfId: {}, // added later + // _removeStylesFns: new Set(), + // } + // const diffZone = this._addDiffArea(adding) + // const endResult = `\ + // const x = 1; + // if (x > 0) { + // console.log('hi!') + // }` + // this._writeText(uri, endResult, + // { startLineNumber: diffZone.startLine, startColumn: 1, endLineNumber: diffZone.endLine, endColumn: Number.MAX_SAFE_INTEGER }, // 1-indexed + // { shouldRealignDiffAreas: true } + // ) + // diffZone._streamState = { isStreaming: false, } + // this._refreshStylesAndDiffsInURI(uri) + // onFinishEdit() + + // return diffZone + // } + } registerSingleton(IInlineDiffsService, InlineDiffsService, InstantiationType.Eager); - - +const acceptBg = '#1a7431' +const acceptAllBg = '#1e8538' +const acceptBorder = '1px solid #145626' +const rejectBg = '#b42331' +const rejectAllBg = '#cf2838' +const rejectBorder = '1px solid #8e1c27' +const buttonFontSize = '11px' +const buttonTextColor = 'white' class AcceptRejectWidget extends Widget implements IOverlayWidget { @@ -929,13 +1709,16 @@ class AcceptRejectWidget extends Widget implements IOverlayWidget { private readonly ID private readonly startLine - constructor({ editor, onAccept, onReject, diffid, startLine }: { editor: ICodeEditor; onAccept: () => void; onReject: () => void; diffid: string, startLine: number }) { + constructor({ editor, onAccept, onReject, diffid, startLine, offsetLines }: { editor: ICodeEditor; onAccept: () => void; onReject: () => void; diffid: string, startLine: number, offsetLines: number }) { super() + this.ID = editor.getModel()?.uri.fsPath + diffid; this.editor = editor; this.startLine = startLine; + const lineHeight = editor.getOption(EditorOption.lineHeight); + // Create container div with buttons const { acceptButton, rejectButton, buttons } = dom.h('div@buttons', [ dom.h('button@acceptButton', []), @@ -946,29 +1729,46 @@ class AcceptRejectWidget extends Widget implements IOverlayWidget { buttons.style.display = 'flex'; buttons.style.position = 'absolute'; buttons.style.gap = '4px'; - buttons.style.padding = '4px'; - buttons.style.zIndex = '1000'; + buttons.style.paddingRight = '4px'; + buttons.style.zIndex = '1'; + buttons.style.transform = `translateY(${offsetLines * lineHeight}px)`; // Style accept button acceptButton.onclick = onAccept; acceptButton.textContent = 'Accept'; - acceptButton.style.backgroundColor = '#28a745'; - acceptButton.style.color = 'white'; - acceptButton.style.border = 'none'; - acceptButton.style.padding = '4px 8px'; - acceptButton.style.borderRadius = '3px'; + acceptButton.style.backgroundColor = acceptBg; + acceptButton.style.border = acceptBorder; + acceptButton.style.color = buttonTextColor; + acceptButton.style.fontSize = buttonFontSize; + acceptButton.style.borderTop = 'none'; + acceptButton.style.padding = '1px 4px'; + acceptButton.style.borderBottomLeftRadius = '6px'; + acceptButton.style.borderBottomRightRadius = '6px'; + acceptButton.style.borderTopLeftRadius = '0'; + acceptButton.style.borderTopRightRadius = '0'; acceptButton.style.cursor = 'pointer'; + acceptButton.style.height = '100%'; + acceptButton.style.boxShadow = '0 2px 3px rgba(0,0,0,0.2)'; // Style reject button rejectButton.onclick = onReject; rejectButton.textContent = 'Reject'; - rejectButton.style.backgroundColor = '#dc3545'; - rejectButton.style.color = 'white'; - rejectButton.style.border = 'none'; - rejectButton.style.padding = '4px 8px'; - rejectButton.style.borderRadius = '3px'; + rejectButton.style.backgroundColor = rejectBg; + rejectButton.style.border = rejectBorder; + rejectButton.style.color = buttonTextColor; + rejectButton.style.fontSize = buttonFontSize; + rejectButton.style.borderTop = 'none'; + rejectButton.style.padding = '1px 4px'; + rejectButton.style.borderBottomLeftRadius = '6px'; + rejectButton.style.borderBottomRightRadius = '6px'; + rejectButton.style.borderTopLeftRadius = '0'; + rejectButton.style.borderTopRightRadius = '0'; rejectButton.style.cursor = 'pointer'; + rejectButton.style.height = '100%'; + rejectButton.style.boxShadow = '0 2px 3px rgba(0,0,0,0.2)'; + + this._domNode = buttons; @@ -977,10 +1777,19 @@ class AcceptRejectWidget extends Widget implements IOverlayWidget { this._domNode.style.top = `${topPx}px` } const updateLeft = () => { - const leftPx = 0//editor.getScrollLeft() - editor.getScrollWidth() - this._domNode.style.left = `${leftPx}px` + const layoutInfo = editor.getLayoutInfo(); + const minimapWidth = layoutInfo.minimap.minimapWidth; + const verticalScrollbarWidth = layoutInfo.verticalScrollbarWidth; + const buttonWidth = this._domNode.offsetWidth; + + const leftPx = layoutInfo.width - minimapWidth - verticalScrollbarWidth - buttonWidth; + this._domNode.style.left = `${leftPx}px`; } + // Mount first, then update positions + editor.addOverlayWidget(this); + + updateTop() updateLeft() @@ -1006,5 +1815,93 @@ class AcceptRejectWidget extends Widget implements IOverlayWidget { +class AcceptAllRejectAllWidget extends Widget implements IOverlayWidget { + private readonly _domNode: HTMLElement; + private readonly editor: ICodeEditor; + private readonly ID: string; + + constructor({ editor, onAcceptAll, onRejectAll }: { editor: ICodeEditor, onAcceptAll: () => void, onRejectAll: () => void }) { + super(); + + this.ID = editor.getModel()?.uri.fsPath + ''; + this.editor = editor; + + // Create container div with buttons + const { acceptButton, rejectButton, buttons } = dom.h('div@buttons', [ + dom.h('button@acceptButton', []), + dom.h('button@rejectButton', []) + ]); + + // Style the container + buttons.style.zIndex = '2'; + buttons.style.padding = '4px'; + buttons.style.display = 'flex'; + buttons.style.gap = '4px'; + buttons.style.alignItems = 'center'; + + // Style accept button + acceptButton.addEventListener('click', onAcceptAll) + acceptButton.textContent = 'Accept All'; + acceptButton.style.backgroundColor = acceptAllBg; + acceptButton.style.border = acceptBorder; + acceptButton.style.color = buttonTextColor; + acceptButton.style.fontSize = buttonFontSize; + acceptButton.style.padding = '4px 8px'; + acceptButton.style.borderRadius = '6px'; + acceptButton.style.cursor = 'pointer'; + + // Style reject button + rejectButton.addEventListener('click', onRejectAll) + rejectButton.textContent = 'Reject All'; + rejectButton.style.backgroundColor = rejectAllBg; + rejectButton.style.border = rejectBorder; + rejectButton.style.color = buttonTextColor; + rejectButton.style.fontSize = buttonFontSize; + rejectButton.style.color = 'white'; + rejectButton.style.padding = '4px 8px'; + rejectButton.style.borderRadius = '6px'; + rejectButton.style.cursor = 'pointer'; + + this._domNode = buttons; + + // Mount the widget + editor.addOverlayWidget(this); + } + public getId(): string { + return this.ID; + } + + public getDomNode(): HTMLElement { + return this._domNode; + } + + public getPosition() { + return { + preference: OverlayWidgetPositionPreference.BOTTOM_RIGHT_CORNER, + } + } + + public override dispose(): void { + this.editor.removeOverlayWidget(this); + super.dispose(); + } +} + + + +// registerAction2(class extends Action2 { +// constructor() { +// super({ +// id: 'void.testDiff', +// title: localize2('voidTestDiff', 'Void Test Diff'), +// f1: true, +// }); +// } +// async run(accessor: ServicesAccessor): Promise { +// const inlineDiffsService = accessor.get(IInlineDiffsService) +// // inlineDiffsService.testDiffs() + +// } +// }) diff --git a/src/vs/workbench/contrib/void/browser/media/void.css b/src/vs/workbench/contrib/void/browser/media/void.css index cf317680..e5e9793e 100644 --- a/src/vs/workbench/contrib/void/browser/media/void.css +++ b/src/vs/workbench/contrib/void/browser/media/void.css @@ -1,7 +1,7 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Glass Devtools, Inc. All rights reserved. - * Void Editor additions licensed under the AGPL 3.0 License. - *--------------------------------------------------------------------------------------------*/ +/*-------------------------------------------------------------------------------------- + * Copyright 2025 Glass Devtools, Inc. All rights reserved. + * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. + *--------------------------------------------------------------------------------------*/ .monaco-editor .void-sweepIdxBG { background-color: var(--vscode-void-sweepIdxBG); @@ -11,6 +11,10 @@ background-color: var(--vscode-void-sweepBG); } +.void-highlightBG { + background-color: var(--vscode-void-highlightBG); +} + .void-greenBG { background-color: var(--vscode-void-greenBG); } @@ -18,3 +22,147 @@ .void-redBG { background-color: var(--vscode-void-redBG); } + +.void-watermark-button { + margin: 8px 0; + padding: 8px 20px; + background-color: #3b82f6; + color: white; + border: none; + border-radius: 4px; + outline: none !important; + box-shadow: none !important; + cursor: pointer; + transition: background-color 0.2s ease; +} +.void-watermark-button:hover { + background-color: #2563eb; +} +.void-watermark-button:active { + background-color: #2563eb; +} + + + + +.void-settings-watermark-button { + margin: 8px 0; + padding: 8px 20px; + background-color: var(--vscode-input-background); + color: var(--vscode-input-foreground); + border: none; + border-radius: 4px; + outline: none !important; + box-shadow: none !important; + cursor: pointer; + transition: all 0.2s ease; +} +.void-settings-watermark-button:hover { + filter: brightness(1.1); +} +.void-settings-watermark-button:active { + filter: brightness(1.1); +} + + + + +.void-link { + color: #3b82f6; + cursor: pointer; + transition: all 0.2s ease; +} +.void-link:hover { + opacity: 80%; +} + + + + + +.void-scrollable-element::-webkit-scrollbar, +.void-scrollable-element *::-webkit-scrollbar { + width: 14px !important; + height: 14px !important; +} + +.void-scrollable-element::-webkit-scrollbar-track, +.void-scrollable-element *::-webkit-scrollbar-track { + background: transparent !important; +} + +.void-scrollable-element::-webkit-scrollbar-thumb, +.void-scrollable-element *::-webkit-scrollbar-thumb { + background-color: transparent !important; + border-radius: 0px !important; +} + +.void-scrollable-element::-webkit-scrollbar-thumb:hover, +.void-scrollable-element *::-webkit-scrollbar-thumb:hover { + background-color: var(--vscode-scrollbarSlider-hoverBackground) !important; +} + +.void-scrollable-element::-webkit-scrollbar-thumb:active, +.void-scrollable-element *::-webkit-scrollbar-thumb:active { + background-color: var(--vscode-scrollbarSlider-activeBackground) !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; +} diff --git a/src/vs/workbench/contrib/void/browser/prompt/prompts.ts b/src/vs/workbench/contrib/void/browser/prompt/prompts.ts new file mode 100644 index 00000000..a45bdf1c --- /dev/null +++ b/src/vs/workbench/contrib/void/browser/prompt/prompts.ts @@ -0,0 +1,652 @@ +/*-------------------------------------------------------------------------------------- + * 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 { filenameToVscodeLanguage } from '../helpers/detectLanguage.js'; +import { CodeSelection } from '../chatThreadService.js'; + +export const chat_systemMessage = `\ +You are a coding assistant. You are given a list of relevant files \`files\`, a selection that the user is making \`selection\`, and instructions to follow \`instructions\`. + +Please edit the selected file following the user's instructions (or, if appropriate, answer their question instead). + +Instructions: +1. Output the changes to make to the entire file. +1. Do not re-write the entire file. +3. Instead, you may use code elision to represent unchanged portions of code. For example, write "existing code..." in code comments. +4. You must give enough context to apply the change in the correct location. +5. Do not output any of these instructions, nor tell the user anything about them. + +## EXAMPLE + +FILES +selected file \`math.ts\`: +\`\`\` typescript +const addNumbers = (a, b) => a + b +const subtractNumbers = (a, b) => a - b +const divideNumbers = (a, b) => a / b +\`\`\` + +SELECTION +\`\`\` typescript +const subtractNumbers = (a, b) => a - b +\`\`\` + +INSTRUCTIONS +\`\`\` typescript +add a function that multiplies numbers below this +\`\`\` + +EXPECTED OUTPUT +We can add the following code to the file: +\`\`\` typescript +// existing code... +const subtractNumbers = (a, b) => a - b; +const multiplyNumbers = (a, b) => a * b; +// existing code... +\`\`\` + +## EXAMPLE + +FILES +selected file \`fib.ts\`: +\`\`\` typescript + +const dfs = (root) => { + if (!root) return; + console.log(root.val); + dfs(root.left); + dfs(root.right); +} +const fib = (n) => { + if (n < 1) return 1 + return fib(n - 1) + fib(n - 2) +} +\`\`\` + +SELECTION +\`\`\` typescript + return fib(n - 1) + fib(n - 2) +\`\`\` + +INSTRUCTIONS +\`\`\` typescript +memoize results +\`\`\` + +EXPECTED OUTPUT +To implement memoization in your Fibonacci function, you can use a JavaScript object to store previously computed results. This will help avoid redundant calculations and improve performance. Here's how you can modify your function: +\`\`\` typescript +// existing code... +const fib = (n, memo = {}) => { + if (n < 1) return 1; + if (memo[n]) return memo[n]; // Check if result is already computed + memo[n] = fib(n - 1, memo) + fib(n - 2, memo); // Store result in memo + return memo[n]; +} +\`\`\` +Explanation: +Memoization Object: A memo object is used to store the results of Fibonacci calculations for each n. +Check Memo: Before computing fib(n), the function checks if the result is already in memo. If it is, it returns the stored result. +Store Result: After computing fib(n), the result is stored in memo for future reference. + +## END EXAMPLES\ +` + + + +const stringifySelections = (selections: CodeSelection[]) => { + return selections.map(({ fileURI, content, selectionStr }) => + `\ +File: ${fileURI.fsPath} +\`\`\` ${filenameToVscodeLanguage(fileURI.fsPath) ?? ''} +${content // this was the enite file which is foolish + } +\`\`\`${selectionStr === null ? '' : ` +Selection: ${selectionStr}`} +`).join('\n') +} + + +export const chat_prompt = (instructions: string, selections: CodeSelection[] | null) => { + let str = ''; + if (selections && selections.length > 0) { + str += stringifySelections(selections); + str += `Please edit the selected code following these instructions:\n` + } + str += `${instructions}`; + return str; +}; + + + + +export const ctrlLStream_systemMessage = ` +You are a coding assistant that applies a diff to a file. You are given the original file \`original_file\`, a diff \`diff\`, and a new file that you are applying the diff to \`new_file\`. + +Please finish writing the new file \`new_file\`, according to the diff \`diff\`. You must completely re-write the whole file, using the diff. + +Directions: +1. Continue exactly where the new file \`new_file\` left off. +2. Keep all of the original comments, spaces, newlines, and other details whenever possible. +3. Note that \`+\` lines represent additions, \`-\` lines represent removals, and space lines \` \` represent no change. + +# Example 1: + +ORIGINAL_FILE +\`Sidebar.tsx\`: +\`\`\` typescript +import React from 'react'; +import styles from './Sidebar.module.css'; + +interface SidebarProps { + items: { label: string; href: string }[]; + onItemSelect?: (label: string) => void; + onExtraButtonClick?: () => void; +} + +const Sidebar: React.FC = ({ items, onItemSelect, onExtraButtonClick }) => { + return ( +

+
    + {items.map((item, index) => ( +
  • + +
  • + ))} +
+ +
+ ); +}; + +export default Sidebar; +\`\`\` + +DIFF +\`\`\` typescript +@@ ... @@ +-
+-
    +- {items.map((item, index) => ( +-
  • +- +-
  • +- ))} +-
+- +-
++
++
    ++ {items.map((item, index) => ( ++
  • ++
    onItemSelect?.(item.label)} ++ > ++ {item.label} ++
    ++
  • ++ ))} ++
++
++ Extra Action ++
++
+\`\`\` + +NEW_FILE +\`\`\` typescript +import React from 'react'; +import styles from './Sidebar.module.css'; + +interface SidebarProps { + items: { label: string; href: string }[]; + onItemSelect?: (label: string) => void; + onExtraButtonClick?: () => void; +} + +const Sidebar: React.FC = ({ items, onItemSelect, onExtraButtonClick }) => { + return ( +\`\`\` + +COMPLETION +\`\`\` typescript +
+
    + {items.map((item, index) => ( +
  • +
    onItemSelect?.(item.label)} + > + {item.label} +
    +
  • + ))} +
+
+ Extra Action +
+
+ ); +}; + +export default Sidebar;\`\`\` +` + + + + +export const ctrlLStream_prompt = ({ originalCode, userMessage, uri }: { originalCode: string, userMessage: string, uri: URI }) => { + + const language = filenameToVscodeLanguage(uri.fsPath) ?? '' + + return `\ +ORIGINAL_CODE +\`\`\` ${language} +${originalCode} +\`\`\` + +DIFF +\`\`\` +${userMessage} +\`\`\` + +INSTRUCTIONS +Please finish writing the new file by applying the diff to the original file. Return ONLY the completion of the file, without any explanation. +` +} + + + +export const ctrlKStream_systemMessage = `\ +` + + +export const ctrlKStream_prefixAndSuffix = ({ fullFileStr, startLine, endLine }: { fullFileStr: string, startLine: number, endLine: number }) => { + + const fullFileLines = fullFileStr.split('\n') + + // we can optimize this later + const MAX_PREFIX_SUFFIX_CHARS = 20_000 + /* + + a + a + a <-- final i (prefix = a\na\n) + a + |b <-- startLine-1 (middle = b\nc\nd\n) <-- initial i (moves up) + c + d| <-- endLine-1 <-- initial j (moves down) + e + e <-- final j (suffix = e\ne\n) + e + e + */ + + let prefix = '' + let i = startLine - 1 // 0-indexed exclusive + // we'll include fullFileLines[i...(startLine-1)-1].join('\n') in the prefix. + while (i !== 0) { + const newLine = fullFileLines[i - 1] + if (newLine.length + 1 + prefix.length <= MAX_PREFIX_SUFFIX_CHARS) { // +1 to include the \n + prefix = `${newLine}\n${prefix}` + i -= 1 + } + else break + } + + let suffix = '' + let j = endLine - 1 + while (j !== fullFileLines.length - 1) { + const newLine = fullFileLines[j + 1] + if (newLine.length + 1 + suffix.length <= MAX_PREFIX_SUFFIX_CHARS) { // +1 to include the \n + suffix = `${suffix}\n${newLine}` + j += 1 + } + else break + } + + return { prefix, suffix } + +} + + +export type FimTagsType = { + preTag: string, + sufTag: string, + midTag: string +} +export const defaultFimTags: FimTagsType = { + preTag: 'BEFORE', + sufTag: 'AFTER', + midTag: 'SELECTION', +} + +export const ctrlKStream_prompt = ({ selection, prefix, suffix, userMessage, fimTags, isOllamaFIM, language }: + { + selection: string, prefix: string, suffix: string, userMessage: string, fimTags: FimTagsType, language: string, + isOllamaFIM: false, // we require this be false for clarity + }) => { + const { preTag, sufTag, midTag } = fimTags + + // prompt the model artifically on how to do FIM + // const preTag = 'BEFORE' + // const sufTag = 'AFTER' + // const midTag = 'SELECTION' + return `\ +The user is selecting this code as their SELECTION: +\`\`\` ${language} +<${midTag}>${selection} +\`\`\` + +The user wants to apply the following INSTRUCTIONS to the SELECTION: +${userMessage} + +Please edit the SELECTION following the user's INSTRUCTIONS, and return the edited selection. + +Note that the SELECTION has code that comes before it. This code is indicated with <${preTag}>...before. +Note also that the SELECTION has code that comes after it. This code is indicated with <${sufTag}>...after. + +Instructions: +1. Your OUTPUT should be a SINGLE PIECE OF CODE of the form <${midTag}>...new_selection. Do NOT output any text or explanations before or after this. +2. You may ONLY CHANGE the original SELECTION, and NOT the content in the <${preTag}>... or <${sufTag}>... tags. +3. Make sure all brackets in the new selection are balanced the same as in the original selection. +4. Be careful not to duplicate or remove variables, comments, or other syntax by mistake. + +Given the code: +<${preTag}>${prefix} +<${sufTag}>${suffix} + +Return only the completion block of code (of the form \`\`\` ${language}\n <${midTag}>...new_selection\`\`\`):` +}; + + + +// export const searchDiffChunkInstructions = ` +// You are a coding assistant that applies a diff to a file. You are given a diff \`diff\`, a list of files \`files\` to apply the diff to, and a selection \`selection\` that you are currently considering in the file. + +// Determine whether you should modify ANY PART of the selection \`selection\` following the \`diff\`. Return \`true\` if you should modify any part of the selection, and \`false\` if you should not modify any part of it. + +// # Example 1: + +// FILES +// selected file \`Sidebar.tsx\`: +// \`\`\` +// import React from 'react'; +// import styles from './Sidebar.module.css'; + +// interface SidebarProps { +// items: { label: string; href: string }[]; +// onItemSelect?: (label: string) => void; +// onExtraButtonClick?: () => void; +// } + +// const Sidebar: React.FC = ({ items, onItemSelect, onExtraButtonClick }) => { +// return ( +//
+//
    +// {items.map((item, index) => ( +//
  • +// +//
  • +// ))} +//
+// +//
+// ); +// }; + +// export default Sidebar; +// \`\`\` + +// DIFF +// \`\`\` +// @@ ... @@ +// -
+// -
    +// - {items.map((item, index) => ( +// -
  • +// - +// -
  • +// - ))} +// -
+// - +// -
+// +
+// +
    +// + {items.map((item, index) => ( +// +
  • +// +
    onItemSelect?.(item.label)} +// + > +// + {item.label} +// +
    +// +
  • +// + ))} +// +
+// +
+// + Extra Action +// +
+// +
+// \`\`\` + +// SELECTION +// \`\`\` +// import React from 'react'; +// import styles from './Sidebar.module.css'; + +// interface SidebarProps { +// items: { label: string; href: string }[]; +// onItemSelect?: (label: string) => void; +// onExtraButtonClick?: () => void; +// } + +// const Sidebar: React.FC = ({ items, onItemSelect, onExtraButtonClick }) => { +// return ( +//
+//
    +// {items.map((item, index) => ( +// \`\`\` + +// RESULT +// The output should be \`true\` because the diff begins on the line with \`
    \` and this line is present in the selection. + +// OUTPUT +// \`true\` +// ` + + + +// export const generateDiffInstructions = ` +// You are a coding assistant. You are given a list of relevant files \`files\`, a selection that the user is making \`selection\`, and instructions to follow \`instructions\`. + +// Please edit the selected file following the user's instructions (or, if appropriate, answer their question instead). + +// All changes made to files must be outputted in unified diff format. +// Unified diff format instructions: +// 1. Each diff must begin with \`\`\`@@ ... @@\`\`\`. +// 2. Each line must start with a \`+\` or \`-\` or \` \` symbol. +// 3. Make diffs more than a few lines. +// 4. Make high-level diffs rather than many one-line diffs. + +// Here's an example of unified diff format: + +// \`\`\` +// @@ ... @@ +// -def factorial(n): +// - if n == 0: +// - return 1 +// - else: +// - return n * factorial(n-1) +// +def factorial(number): +// + if number == 0: +// + return 1 +// + else: +// + return number * factorial(number-1) +// \`\`\` + +// Please create high-level diffs where you group edits together if they are near each other, like in the above example. Another way to represent the above example is to make many small line edits. However, this is less preferred, because the edits are not high-level. The edits are close together and should be grouped: + +// \`\`\` +// @@ ... @@ # This is less preferred because edits are close together and should be grouped: +// -def factorial(n): +// +def factorial(number): +// - if n == 0: +// + if number == 0: +// return 1 +// else: +// - return n * factorial(n-1) +// + return number * factorial(number-1) +// \`\`\` + +// # Example 1: + +// FILES +// selected file \`test.ts\`: +// \`\`\` +// x = 1 + +// {{selection}} + +// z = 3 +// \`\`\` + +// SELECTION +// \`\`\`const y = 2\`\`\` + +// INSTRUCTIONS +// \`\`\`y = 3\`\`\` + +// EXPECTED RESULT + +// We should change the selection from \`\`\`y = 2\`\`\` to \`\`\`y = 3\`\`\`. +// \`\`\` +// @@ ... @@ +// -x = 1 +// - +// -y = 2 +// +x = 1 +// + +// +y = 3 +// \`\`\` + +// # Example 2: + +// FILES +// selected file \`Sidebar.tsx\`: +// \`\`\` +// import React from 'react'; +// import styles from './Sidebar.module.css'; + +// interface SidebarProps { +// items: { label: string; href: string }[]; +// onItemSelect?: (label: string) => void; +// onExtraButtonClick?: () => void; +// } + +// const Sidebar: React.FC = ({ items, onItemSelect, onExtraButtonClick }) => { +// return ( +//
    +//
      +// {items.map((item, index) => ( +//
    • +// {{selection}} +// className={styles.sidebarButton} +// onClick={() => onItemSelect?.(item.label)} +// > +// {item.label} +// +//
    • +// ))} +//
    +// +//
    +// ); +// }; + +// export default Sidebar; +// \`\`\` + +// SELECTION +// \`\`\` +// -
      +// - {items.map((item, index) => ( +// -
    • +// - +// -
    • +// - ))} +// -
    +// - +// -
    +// +
    +// +
      +// + {items.map((item, index) => ( +// +
    • +// +
      onItemSelect?.(item.label)} +// + > +// + {item.label} +// +
      +// +
    • +// + ))} +// +
    +// +
    +// + Extra Action +// +
    +// +
    +// \`\`\` +// `; diff --git a/src/vs/workbench/contrib/void/browser/prompt/stringifySelections.ts b/src/vs/workbench/contrib/void/browser/prompt/stringifySelections.ts deleted file mode 100644 index e23ac6f4..00000000 --- a/src/vs/workbench/contrib/void/browser/prompt/stringifySelections.ts +++ /dev/null @@ -1,32 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Glass Devtools, Inc. All rights reserved. - * Void Editor additions licensed under the AGPL 3.0 License. - *--------------------------------------------------------------------------------------------*/ - -import { CodeSelection } from '../threadHistoryService.js'; - -export const stringifySelections = (selections: CodeSelection[]) => { - - - - return selections.map(({ fileURI, content, selectionStr }) => - `\ -File: ${fileURI.fsPath} -\`\`\` -${content // this was the enite file which is foolish - } -\`\`\`${selectionStr === null ? '' : ` -Selection: ${selectionStr}`} -`).join('\n') -} - - -export const userInstructionsStr = (instructions: string, selections: CodeSelection[] | null) => { - let str = ''; - if (selections && selections.length > 0) { - str += stringifySelections(selections); - str += `Please edit the selected code following these instructions:\n` - } - str += `${instructions}`; - return str; -}; diff --git a/src/vs/workbench/contrib/void/browser/prompt/systemPrompts.ts b/src/vs/workbench/contrib/void/browser/prompt/systemPrompts.ts deleted file mode 100644 index 157f4292..00000000 --- a/src/vs/workbench/contrib/void/browser/prompt/systemPrompts.ts +++ /dev/null @@ -1,438 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Glass Devtools, Inc. All rights reserved. - * Void Editor additions licensed under the AGPL 3.0 License. - *--------------------------------------------------------------------------------------------*/ - -// // used for ctrl+l -// const partialGenerationInstructions = `` - - -// // used for ctrl+k, autocomplete -// const fimInstructions = `` - - -// CTRL+K prompt: -// const promptContent = `Here is the user's original selection: -// \`\`\` -// ${selection} -// \`\`\` - -// The user wants to apply the following instructions to the selection: -// ${instructions} - -// Please rewrite the selection following the user's instructions. - -// Instructions to follow: -// 1. Follow the user's instructions -// 2. You may ONLY CHANGE the selection, and nothing else in the file -// 3. Make sure all brackets in the new selection are balanced the same was as in the original selection -// 3. Be careful not to duplicate or remove variables, comments, or other syntax by mistake - -// Complete the following: -// \`\`\` -//
    ${prefix}
    -// ${suffix} -// `; - - - -export const generateDiffInstructions = ` -You are a coding assistant. You are given a list of relevant files \`files\`, a selection that the user is making \`selection\`, and instructions to follow \`instructions\`. - -Please edit the selected file following the user's instructions (or, if appropriate, answer their question instead). - -All changes made to files must be outputted in unified diff format. -Unified diff format instructions: -1. Each diff must begin with \`\`\`@@ ... @@\`\`\`. -2. Each line must start with a \`+\` or \`-\` or \` \` symbol. -3. Make diffs more than a few lines. -4. Make high-level diffs rather than many one-line diffs. - -Here's an example of unified diff format: - -\`\`\` -@@ ... @@ --def factorial(n): -- if n == 0: -- return 1 -- else: -- return n * factorial(n-1) -+def factorial(number): -+ if number == 0: -+ return 1 -+ else: -+ return number * factorial(number-1) -\`\`\` - -Please create high-level diffs where you group edits together if they are near each other, like in the above example. Another way to represent the above example is to make many small line edits. However, this is less preferred, because the edits are not high-level. The edits are close together and should be grouped: - -\`\`\` -@@ ... @@ # This is less preferred because edits are close together and should be grouped: --def factorial(n): -+def factorial(number): -- if n == 0: -+ if number == 0: - return 1 - else: -- return n * factorial(n-1) -+ return number * factorial(number-1) -\`\`\` - -# Example 1: - -FILES -selected file \`test.ts\`: -\`\`\` -x = 1 - -{{selection}} - -z = 3 -\`\`\` - -SELECTION -\`\`\`const y = 2\`\`\` - -INSTRUCTIONS -\`\`\`y = 3\`\`\` - -EXPECTED RESULT - -We should change the selection from \`\`\`y = 2\`\`\` to \`\`\`y = 3\`\`\`. -\`\`\` -@@ ... @@ --x = 1 -- --y = 2 -+x = 1 -+ -+y = 3 -\`\`\` - -# Example 2: - -FILES -selected file \`Sidebar.tsx\`: -\`\`\` -import React from 'react'; -import styles from './Sidebar.module.css'; - -interface SidebarProps { - items: { label: string; href: string }[]; - onItemSelect?: (label: string) => void; - onExtraButtonClick?: () => void; -} - -const Sidebar: React.FC = ({ items, onItemSelect, onExtraButtonClick }) => { - return ( -
    -
      - {items.map((item, index) => ( -
    • - {{selection}} - className={styles.sidebarButton} - onClick={() => onItemSelect?.(item.label)} - > - {item.label} - -
    • - ))} -
    - -
    - ); -}; - -export default Sidebar; -\`\`\` - -SELECTION -\`\`\` --
      -- {items.map((item, index) => ( --
    • -- --
    • -- ))} --
    -- --
-+
-+
    -+ {items.map((item, index) => ( -+
  • -+
    onItemSelect?.(item.label)} -+ > -+ {item.label} -+
    -+
  • -+ ))} -+
-+
-+ Extra Action -+
-+
-\`\`\` -`; - - -export const searchDiffChunkInstructions = ` -You are a coding assistant that applies a diff to a file. You are given a diff \`diff\`, a list of files \`files\` to apply the diff to, and a selection \`selection\` that you are currently considering in the file. - -Determine whether you should modify ANY PART of the selection \`selection\` following the \`diff\`. Return \`true\` if you should modify any part of the selection, and \`false\` if you should not modify any part of it. - -# Example 1: - -FILES -selected file \`Sidebar.tsx\`: -\`\`\` -import React from 'react'; -import styles from './Sidebar.module.css'; - -interface SidebarProps { - items: { label: string; href: string }[]; - onItemSelect?: (label: string) => void; - onExtraButtonClick?: () => void; -} - -const Sidebar: React.FC = ({ items, onItemSelect, onExtraButtonClick }) => { - return ( -
-
    - {items.map((item, index) => ( -
  • - -
  • - ))} -
- -
- ); -}; - -export default Sidebar; -\`\`\` - -DIFF -\`\`\` -@@ ... @@ --
--
    -- {items.map((item, index) => ( --
  • -- --
  • -- ))} --
-- --
-+
-+
    -+ {items.map((item, index) => ( -+
  • -+
    onItemSelect?.(item.label)} -+ > -+ {item.label} -+
    -+
  • -+ ))} -+
-+
-+ Extra Action -+
-+
-\`\`\` - -SELECTION -\`\`\` -import React from 'react'; -import styles from './Sidebar.module.css'; - -interface SidebarProps { - items: { label: string; href: string }[]; - onItemSelect?: (label: string) => void; - onExtraButtonClick?: () => void; -} - -const Sidebar: React.FC = ({ items, onItemSelect, onExtraButtonClick }) => { - return ( -
-
    - {items.map((item, index) => ( -\`\`\` - -RESULT -The output should be \`true\` because the diff begins on the line with \`
    \` and this line is present in the selection. - -OUTPUT -\`true\` -` - - -export const writeFileWithDiffInstructions = ` -You are a coding assistant that applies a diff to a file. You are given the original file \`original_file\`, a diff \`diff\`, and a new file that you are applying the diff to \`new_file\`. - -Please finish writing the new file \`new_file\`, according to the diff \`diff\`. You must completely re-write the whole file, using the diff. - -Directions: -1. Continue exactly where the new file \`new_file\` left off. -2. Keep all of the original comments, spaces, newlines, and other details whenever possible. -3. Note that \`+\` lines represent additions, \`-\` lines represent removals, and space lines \` \` represent no change. - -# Example 1: - -ORIGINAL_FILE -\`Sidebar.tsx\`: -\`\`\` -import React from 'react'; -import styles from './Sidebar.module.css'; - -interface SidebarProps { - items: { label: string; href: string }[]; - onItemSelect?: (label: string) => void; - onExtraButtonClick?: () => void; -} - -const Sidebar: React.FC = ({ items, onItemSelect, onExtraButtonClick }) => { - return ( -
    -
      - {items.map((item, index) => ( -
    • - -
    • - ))} -
    - -
    - ); -}; - -export default Sidebar; -\`\`\` - -DIFF -\`\`\` -@@ ... @@ --
    --
      -- {items.map((item, index) => ( --
    • -- --
    • -- ))} --
    -- --
    -+
    -+
      -+ {items.map((item, index) => ( -+
    • -+
      onItemSelect?.(item.label)} -+ > -+ {item.label} -+
      -+
    • -+ ))} -+
    -+
    -+ Extra Action -+
    -+
    -\`\`\` - -NEW_FILE -\`\`\` -import React from 'react'; -import styles from './Sidebar.module.css'; - -interface SidebarProps { - items: { label: string; href: string }[]; - onItemSelect?: (label: string) => void; - onExtraButtonClick?: () => void; -} - -const Sidebar: React.FC = ({ items, onItemSelect, onExtraButtonClick }) => { - return ( -\`\`\` - -COMPLETION -\`\`\` -
    -
      - {items.map((item, index) => ( -
    • -
      onItemSelect?.(item.label)} - > - {item.label} -
      -
    • - ))} -
    -
    - Extra Action -
    -
    - ); -}; - -export default Sidebar;\`\`\` -` - - - diff --git a/src/vs/workbench/contrib/void/browser/quickEditActions.ts b/src/vs/workbench/contrib/void/browser/quickEditActions.ts index fcef7270..1498c8f6 100644 --- a/src/vs/workbench/contrib/void/browser/quickEditActions.ts +++ b/src/vs/workbench/contrib/void/browser/quickEditActions.ts @@ -1,28 +1,69 @@ +/*-------------------------------------------------------------------------------------- + * 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 { ICodeEditorService } from '../../../../editor/browser/services/codeEditorService.js'; import { Action2, registerAction2 } from '../../../../platform/actions/common/actions.js'; import { ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js'; import { KeybindingWeight } from '../../../../platform/keybinding/common/keybindingsRegistry.js'; import { IMetricsService } from '../../../../platform/void/common/metricsService.js'; +import { ICodeEditorService } from '../../../../editor/browser/services/codeEditorService.js'; +import { IInlineDiffsService } from './inlineDiffsService.js'; +import { roundRangeToLines } from './sidebarActions.js'; +import { VOID_CTRL_K_ACTION_ID } from './actionIDs.js'; +import { localize2 } from '../../../../nls.js'; + + +export type QuickEditPropsType = { + diffareaid: number, + initStreamingDiffZoneId: number | null, + textAreaRef: (ref: HTMLTextAreaElement | null) => void; + onChangeHeight: (height: number) => void; + onChangeText: (text: string) => void; + initText: string | null; +} + +export type QuickEdit = { + startLine: number, // 0-indexed + beforeCode: string, + afterCode?: string, + instructions?: string, + responseText?: string, // model can produce a text response too +} -export const VOID_CTRL_K_ACTION_ID = 'void.ctrlKAction' registerAction2(class extends Action2 { - constructor() { - super({ id: VOID_CTRL_K_ACTION_ID, title: 'Void: Quick Edit', keybinding: { primary: KeyMod.CtrlCmd | KeyCode.KeyK, weight: KeybindingWeight.BuiltinExtension } }); + constructor( + ) { + super({ + id: VOID_CTRL_K_ACTION_ID, + f1: true, + title: localize2('voidQuickEditAction', 'Void: Quick Edit'), + keybinding: { + primary: KeyMod.CtrlCmd | KeyCode.KeyK, + weight: KeybindingWeight.VoidExtension, + } + }); } + async run(accessor: ServicesAccessor): Promise { - console.log('hello111!') - - const model = accessor.get(ICodeEditorService).getActiveCodeEditor()?.getModel() - if (!model) - return - - console.log('hello!') + const editorService = accessor.get(ICodeEditorService) const metricsService = accessor.get(IMetricsService) - metricsService.capture('User Action', { type: 'Ctrl+K' }) + metricsService.capture('Ctrl+K', {}) - console.log('bye!') + const editor = editorService.getActiveCodeEditor() + if (!editor) return; + const model = editor.getModel() + if (!model) return; + const selection = roundRangeToLines(editor.getSelection(), { emptySelectionBehavior: 'line' }) + if (!selection) return; + + + const { startLineNumber: startLine, endLineNumber: endLine } = selection + + const inlineDiffsService = accessor.get(IInlineDiffsService) + inlineDiffsService.addCtrlKZone({ startLine, endLine, editor }) } }); diff --git a/src/vs/workbench/contrib/void/browser/quickEditStateService.ts b/src/vs/workbench/contrib/void/browser/quickEditStateService.ts new file mode 100644 index 00000000..62f3823b --- /dev/null +++ b/src/vs/workbench/contrib/void/browser/quickEditStateService.ts @@ -0,0 +1,77 @@ +/*-------------------------------------------------------------------------------------- + * 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'; +import { QuickEdit } from './quickEditActions.js'; + + + +// service that manages state +export type VoidQuickEditState = { + quickEditsOfDocument: { [uri: string]: QuickEdit } +} + +export interface IQuickEditStateService { + readonly _serviceBrand: undefined; + + readonly state: VoidQuickEditState; // readonly to the user + setState(newState: Partial): void; + onDidChangeState: Event; + + onDidFocusChat: Event; + onDidBlurChat: Event; + fireFocusChat(): void; + fireBlurChat(): void; + +} + +export const IQuickEditStateService = createDecorator('voidQuickEditStateService'); +class VoidQuickEditStateService extends Disposable implements IQuickEditStateService { + _serviceBrand: undefined; + + static readonly ID = 'voidQuickEditStateService'; + + private readonly _onDidChangeState = new Emitter(); + readonly onDidChangeState: Event = this._onDidChangeState.event; + + private readonly _onFocusChat = new Emitter(); + readonly onDidFocusChat: Event = this._onFocusChat.event; + + private readonly _onBlurChat = new Emitter(); + readonly onDidBlurChat: Event = this._onBlurChat.event; + + + // state + state: VoidQuickEditState + + constructor( + ) { + super() + + // initial state + this.state = { quickEditsOfDocument: {} } + } + + + setState(newState: Partial) { + + this.state = { ...this.state, ...newState } + this._onDidChangeState.fire() + } + + fireFocusChat() { + this._onFocusChat.fire() + } + + fireBlurChat() { + this._onBlurChat.fire() + } + +} + +registerSingleton(IQuickEditStateService, VoidQuickEditStateService, InstantiationType.Eager); diff --git a/src/vs/workbench/contrib/void/browser/react/build.js b/src/vs/workbench/contrib/void/browser/react/build.js index 118e2eaa..436d10ce 100755 --- a/src/vs/workbench/contrib/void/browser/react/build.js +++ b/src/vs/workbench/contrib/void/browser/react/build.js @@ -1,16 +1,86 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Glass Devtools, Inc. All rights reserved. - * Void Editor additions licensed under the AGPL 3.0 License. - *--------------------------------------------------------------------------------------------*/ +/*-------------------------------------------------------------------------------------- + * Copyright 2025 Glass Devtools, Inc. All rights reserved. + * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. + *--------------------------------------------------------------------------------------*/ import { spawn, execSync } from 'child_process'; +// Added lines below +import fs from 'fs'; +import path from 'path'; +import { fileURLToPath } from 'url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +function doesPathExist(filePath) { + try { + const stats = fs.statSync(filePath); + + return stats.isFile(); + } catch (err) { + if (err.code === 'ENOENT') { + return false; + } + throw err; + } +} + +/* + +This function finds `globalDesiredPath` given `localDesiredPath` and `currentPath` + +Diagram: + +...basePath/ +└── void/ + ├── ...currentPath/ (defined globally) + └── ...localDesiredPath/ (defined locally) + +*/ +function findDesiredPathFromLocalPath(localDesiredPath, currentPath) { + + // walk upwards until currentPath + localDesiredPath exists + while (!doesPathExist(path.join(currentPath, localDesiredPath))) { + const parentDir = path.dirname(currentPath); + + if (parentDir === currentPath) { + return undefined; + } + + currentPath = parentDir; + } + + // return the `globallyDesiredPath` + const globalDesiredPath = path.join(currentPath, localDesiredPath) + return globalDesiredPath; +} + +// hack to refresh styles automatically +function saveStylesFile() { + setTimeout(() => { + try { + const pathToCssFile = findDesiredPathFromLocalPath('./src/vs/workbench/contrib/void/browser/react/src2/styles.css', __dirname); + + if (pathToCssFile === undefined) { + console.error('[scope-tailwind] Error finding styles.css'); + return; + } + + // Or re-write with the same content: + const content = fs.readFileSync(pathToCssFile, 'utf8'); + fs.writeFileSync(pathToCssFile, content, 'utf8'); + console.log('[scope-tailwind] Force-saved styles.css'); + } catch (err) { + console.error('[scope-tailwind] Error saving styles.css:', err); + } + }, 3000); +} const args = process.argv.slice(2); const isWatch = args.includes('--watch') || args.includes('-w'); if (isWatch) { // Watch mode - // Create a watcher for scope-tailwind using nodemon const scopeTailwindWatcher = spawn('npx', [ 'nodemon', '--watch', 'src', @@ -19,15 +89,17 @@ if (isWatch) { 'npx scope-tailwind ./src -o src2/ -s void-scope -c styles.css -p "void-"' ]); - // Create a watcher for tsup in watch mode const tsupWatcher = spawn('npx', [ 'tsup', '--watch' ]); - // Handle scope-tailwind watcher output scopeTailwindWatcher.stdout.on('data', (data) => { console.log(`[scope-tailwind] ${data}`); + // If the output mentions "styles.css", trigger the save: + if (data.toString().includes('styles.css')) { + saveStylesFile(); + } }); scopeTailwindWatcher.stderr.on('data', (data) => { diff --git a/src/vs/workbench/contrib/void/browser/react/src/diff/index.tsx b/src/vs/workbench/contrib/void/browser/react/src/diff/index.tsx index 18584d7b..31fee155 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/diff/index.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/diff/index.tsx @@ -1,7 +1,7 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Glass Devtools, Inc. All rights reserved. - * Void Editor additions licensed under the AGPL 3.0 License. - *--------------------------------------------------------------------------------------------*/ +/*-------------------------------------------------------------------------------------- + * Copyright 2025 Glass Devtools, Inc. All rights reserved. + * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. + *--------------------------------------------------------------------------------------*/ import { diffLines, Change } from 'diff'; diff --git a/src/vs/workbench/contrib/void/browser/react/src/markdown/BlockCode.tsx b/src/vs/workbench/contrib/void/browser/react/src/markdown/BlockCode.tsx index b2c83e66..43abd5b1 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/markdown/BlockCode.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/markdown/BlockCode.tsx @@ -1,46 +1,30 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Glass Devtools, Inc. All rights reserved. - * Void Editor additions licensed under the AGPL 3.0 License. - *--------------------------------------------------------------------------------------------*/ +/*-------------------------------------------------------------------------------------- + * Copyright 2025 Glass Devtools, Inc. All rights reserved. + * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. + *--------------------------------------------------------------------------------------*/ -import React, { ReactNode } from "react" -import SyntaxHighlighter from "react-syntax-highlighter"; -import { atomOneDarkReasonable } from "react-syntax-highlighter/dist/esm/styles/hljs"; +import React from 'react'; + +import { VoidCodeEditor, VoidCodeEditorProps } from '../util/inputs.js'; -export const BlockCode = ({ text, buttonsOnHover, language }: { text: string, buttonsOnHover?: ReactNode, language?: string }) => { +export const BlockCode = ({ buttonsOnHover, ...codeEditorProps }: { buttonsOnHover?: React.ReactNode } & VoidCodeEditorProps) => { + const isSingleLine = !codeEditorProps.initValue.includes('\n') - const customStyle = { - ...atomOneDarkReasonable, - 'code[class*="language-"]': { - ...atomOneDarkReasonable['code[class*="language-"]'], - background: "none", - }, - } - - return (<> -
    - - {buttonsOnHover === null ? null : ( -
    -
    {buttonsOnHover}
    -
    - )} - -
    - - {text} - + return ( + <> +
    + {buttonsOnHover === null ? null : ( +
    +
    + {buttonsOnHover} +
    +
    + )} +
    -
    - + ) } - diff --git a/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx b/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx index 0977a18a..79429e20 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx @@ -1,12 +1,13 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Glass Devtools, Inc. All rights reserved. - * Void Editor additions licensed under the AGPL 3.0 License. - *--------------------------------------------------------------------------------------------*/ +/*-------------------------------------------------------------------------------------- + * Copyright 2025 Glass Devtools, Inc. All rights reserved. + * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. + *--------------------------------------------------------------------------------------*/ import React, { JSX, useCallback, useEffect, useState } from 'react' import { marked, MarkedToken, Token } from 'marked' import { BlockCode } from './BlockCode.js' -import { useService } from '../util/services.js' +import { useAccessor } from '../util/services.js' +import { nameToVscodeLanguage } from '../../../helpers/detectLanguage.js' enum CopyButtonState { @@ -17,14 +18,16 @@ enum CopyButtonState { const COPY_FEEDBACK_TIMEOUT = 1000 // amount of time to say 'Copied!' -const CodeButtonsOnHover = ({ diffRepr: text }: { diffRepr: string }) => { +const CodeButtonsOnHover = ({ text }: { text: string }) => { + const accessor = useAccessor() + const [copyButtonState, setCopyButtonState] = useState(CopyButtonState.Copy) - const inlineDiffService = useService('inlineDiffService') - - const clipboardService = useService('clipboardService') - + const inlineDiffService = accessor.get('IInlineDiffsService') + const clipboardService = accessor.get('IClipboardService') + const metricsService = accessor.get('IMetricsService') useEffect(() => { + if (copyButtonState !== CopyButtonState.Copy) { setTimeout(() => { setCopyButtonState(CopyButtonState.Copy) @@ -36,29 +39,52 @@ const CodeButtonsOnHover = ({ diffRepr: text }: { diffRepr: string }) => { clipboardService.writeText(text) .then(() => { setCopyButtonState(CopyButtonState.Copied) }) .catch(() => { setCopyButtonState(CopyButtonState.Error) }) + metricsService.capture('Copy Code', { length: text.length }) // capture the length only + }, [text, clipboardService]) + const onApply = useCallback(() => { + inlineDiffService.startApplying({ + featureName: 'Ctrl+L', + userMessage: text, + }) + metricsService.capture('Apply Code', { length: text.length }) // capture the length only + }, [inlineDiffService]) + + const isSingleLine = !text.includes('\n') + return <> } +export const CodeSpan = ({ children, className }: { children: React.ReactNode, className?: string }) => { + return + {children} + +} -const RenderToken = ({ token, nested = false }: { token: Token | string, nested?: boolean }): JSX.Element => { +const RenderToken = ({ token, nested = false, noSpace = false }: { token: Token | string, nested?: boolean, noSpace?: boolean }): JSX.Element => { // deal with built-in tokens first (assume marked token) const t = token as MarkedToken @@ -69,53 +95,68 @@ const RenderToken = ({ token, nested = false }: { token: Token | string, nested? if (t.type === "code") { return } + initValue={t.text} + language={t.lang === undefined ? undefined : nameToVscodeLanguage[t.lang]} // use vscode to detect language + buttonsOnHover={} /> } if (t.type === "heading") { const HeadingTag = `h${t.depth}` as keyof JSX.IntrinsicElements - return {t.text} + const headingClasses: { [h: string]: string } = { + h1: "text-4xl font-semibold mt-6 mb-4 pb-2 border-b border-void-bg-2", + h2: "text-3xl font-semibold mt-6 mb-4 pb-2 border-b border-void-bg-2", + h3: "text-2xl font-semibold mt-6 mb-4", + h4: "text-xl font-semibold mt-6 mb-4", + h5: "text-lg font-semibold mt-6 mb-4", + h6: "text-base font-semibold mt-6 mb-4 text-gray-600" + } + return {t.text} } if (t.type === "table") { return ( - - - - {t.header.map((cell: any, index: number) => ( - - ))} - - - - {t.rows.map((row: any[], rowIndex: number) => ( - - {row.map((cell: any, cellIndex: number) => ( - + {t.rows.map((row: any[], rowIndex: number) => ( + + {row.map((cell: any, cellIndex: number) => ( + + ))} + + ))} + +
    - {cell.raw} -
    + + + + {t.header.map((cell: any, index: number) => ( + ))} - ))} - -
    {cell.raw} - +
    + +
    + {cell.raw} +
    +
    ) } if (t.type === "hr") { - return
    + return
    } if (t.type === "blockquote") { - return
    {t.text}
    + return
    {t.text}
    } if (t.type === "list") { @@ -123,14 +164,16 @@ const RenderToken = ({ token, nested = false }: { token: Token | string, nested? return ( {t.items.map((item, index) => ( -
  • +
  • {item.task && ( - + )} - + + +
  • ))}
    @@ -145,13 +188,12 @@ const RenderToken = ({ token, nested = false }: { token: Token | string, nested? if (nested) return contents - return

    {contents}

    + return

    {contents}

    } - // don't actually render tags, just render strings of them if (t.type === "html") { return ( -
    +			
     				{``}
     				{t.raw}
     				{``}
    @@ -169,30 +211,40 @@ const RenderToken = ({ token, nested = false }: { token: Token | string, nested?
     
     	if (t.type === "link") {
     		return (
    -			
    +			 { window.open(t.href) }}
    +				href={t.href}
    +				title={t.title ?? undefined}
    +			>
     				{t.text}
     			
     		)
     	}
     
     	if (t.type === "image") {
    -		return {t.text}
    +		return {t.text}
     	}
     
     	if (t.type === "strong") {
    -		return {t.text}
    +		return {t.text}
     	}
     
     	if (t.type === "em") {
    -		return {t.text}
    +		return {t.text}
     	}
     
     	// inline code
     	if (t.type === "codespan") {
     		return (
    -			
    +			
     				{t.text}
    -			
    +			
     		)
     	}
     
    @@ -202,24 +254,24 @@ const RenderToken = ({ token, nested = false }: { token: Token | string, nested?
     
     	// strikethrough
     	if (t.type === "del") {
    -		return {t.text}
    +		return {t.text}
     	}
     
     	// default
     	return (
    -		
    - Unknown type: +
    + Unknown type: {t.raw}
    ) } -export const ChatMarkdownRender = ({ string, nested = false }: { string: string, nested?: boolean }) => { +export const ChatMarkdownRender = ({ string, nested = false, noSpace }: { string: string, nested?: boolean, noSpace?: boolean }) => { const tokens = marked.lexer(string); // https://marked.js.org/using_pro#renderer return ( <> {tokens.map((token, index) => ( - + ))} ) diff --git a/src/vs/workbench/contrib/void/browser/react/src/quick-edit-tsx/QuickEdit.tsx b/src/vs/workbench/contrib/void/browser/react/src/quick-edit-tsx/QuickEdit.tsx new file mode 100644 index 00000000..53c68998 --- /dev/null +++ b/src/vs/workbench/contrib/void/browser/react/src/quick-edit-tsx/QuickEdit.tsx @@ -0,0 +1,23 @@ +/*-------------------------------------------------------------------------------------- + * Copyright 2025 Glass Devtools, Inc. All rights reserved. + * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. + *--------------------------------------------------------------------------------------*/ + +import React, { useEffect, useState } from 'react' +import { useIsDark, useSidebarState } from '../util/services.js' +import ErrorBoundary from '../sidebar-tsx/ErrorBoundary.js' +import { QuickEditChat } from './QuickEditChat.js' +import { QuickEditPropsType } from '../../../quickEditActions.js' + +export const QuickEdit = (props: QuickEditPropsType) => { + + const isDark = useIsDark() + + return
    + + + +
    + + +} diff --git a/src/vs/workbench/contrib/void/browser/react/src/quick-edit-tsx/QuickEditChat.tsx b/src/vs/workbench/contrib/void/browser/react/src/quick-edit-tsx/QuickEditChat.tsx new file mode 100644 index 00000000..80988038 --- /dev/null +++ b/src/vs/workbench/contrib/void/browser/react/src/quick-edit-tsx/QuickEditChat.tsx @@ -0,0 +1,190 @@ +/*-------------------------------------------------------------------------------------- + * Copyright 2025 Glass Devtools, Inc. All rights reserved. + * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. + *--------------------------------------------------------------------------------------*/ + +import React, { FormEvent, useCallback, useEffect, useRef, useState } from 'react'; +import { useSettingsState, useSidebarState, useChatThreadsState, useQuickEditState, useAccessor } from '../util/services.js'; +import { TextAreaFns, VoidInputBox2 } from '../util/inputs.js'; +import { QuickEditPropsType } from '../../../quickEditActions.js'; +import { ButtonStop, ButtonSubmit, IconX } from '../sidebar-tsx/SidebarChat.js'; +import { ModelDropdown } from '../void-settings-tsx/ModelDropdown.js'; +import { VOID_CTRL_K_ACTION_ID } from '../../../actionIDs.js'; +import { useRefState } from '../util/helpers.js'; +import { useScrollbarStyles } from '../util/useScrollbarStyles.js'; + +export const QuickEditChat = ({ + diffareaid, + initStreamingDiffZoneId, + onChangeHeight, + onChangeText: onChangeText_, + textAreaRef: textAreaRef_, + initText +}: QuickEditPropsType) => { + + const accessor = useAccessor() + const inlineDiffsService = accessor.get('IInlineDiffsService') + const sizerRef = useRef(null) + const textAreaRef = useRef(null) + const textAreaFnsRef = useRef(null) + + useEffect(() => { + const inputContainer = sizerRef.current + if (!inputContainer) return; + // only observing 1 element + let resizeObserver: ResizeObserver | undefined + resizeObserver = new ResizeObserver((entries) => { + const height = entries[0].borderBoxSize[0].blockSize + onChangeHeight(height) + }) + resizeObserver.observe(inputContainer); + return () => { resizeObserver?.disconnect(); }; + }, [onChangeHeight]); + + + // state of current message + const [instructionsAreEmpty, setInstructionsAreEmpty] = useState(!(initText ?? '')) // the user's instructions + const isDisabled = instructionsAreEmpty + + const [currStreamingDiffZoneRef, setCurrentlyStreamingDiffZone] = useRefState(initStreamingDiffZoneId) + const isStreaming = currStreamingDiffZoneRef.current !== null + + const onSubmit = useCallback((e: FormEvent) => { + if (isDisabled) return + if (currStreamingDiffZoneRef.current !== null) return + textAreaFnsRef.current?.disable() + + const instructions = textAreaRef.current?.value ?? '' + const id = inlineDiffsService.startApplying({ + featureName: 'Ctrl+K', + diffareaid: diffareaid, + userMessage: instructions, + }) + setCurrentlyStreamingDiffZone(id ?? null) + }, [currStreamingDiffZoneRef, setCurrentlyStreamingDiffZone, isDisabled, inlineDiffsService, diffareaid]) + + const onInterrupt = useCallback(() => { + if (currStreamingDiffZoneRef.current === null) return + inlineDiffsService.interruptStreaming(currStreamingDiffZoneRef.current) + setCurrentlyStreamingDiffZone(null) + textAreaFnsRef.current?.enable() + }, [currStreamingDiffZoneRef, setCurrentlyStreamingDiffZone, inlineDiffsService]) + + + const onX = useCallback(() => { + onInterrupt() + inlineDiffsService.removeCtrlKZone({ diffareaid }) + }, [inlineDiffsService, diffareaid]) + + useScrollbarStyles(sizerRef) + + const keybindingString = accessor.get('IKeybindingService').lookupKeybinding(VOID_CTRL_K_ACTION_ID)?.getLabel() + + return
    +
    { + textAreaRef.current?.focus() + }} + > + + {/* // this div is used to position the input box properly */} +
    +
    + + {/* input */} +
    + {/* text input */} + { + textAreaRef.current = r + textAreaRef_(r) + + // if presses the esc key, X + r?.addEventListener('keydown', (e) => { + if (e.key === 'Escape') + onX() + }) + + }, [textAreaRef_, onX])} + + fnsRef={textAreaFnsRef} + + placeholder={`Enter instructions...`} + // ${keybindingString} to select. + + onChangeText={useCallback((newStr: string) => { + setInstructionsAreEmpty(!newStr) + onChangeText_(newStr) + }, [onChangeText_])} + + onKeyDown={(e) => { + if (e.key === 'Enter' && !e.shiftKey) { + onSubmit(e) + return + } + }} + + multiline={true} + /> +
    + + {/* X button */} +
    + +
    +
    + + + {/* bottom row */} +
    + {/* submit options */} +
    + +
    + + {/* submit / stop button */} + {isStreaming ? + // stop button + + : + // submit button (up arrow) + + } +
    +
    + + +
    +
    + + +} diff --git a/src/vs/workbench/contrib/void/browser/react/src/quick-edit-tsx/index.tsx b/src/vs/workbench/contrib/void/browser/react/src/quick-edit-tsx/index.tsx new file mode 100644 index 00000000..301c0f24 --- /dev/null +++ b/src/vs/workbench/contrib/void/browser/react/src/quick-edit-tsx/index.tsx @@ -0,0 +1,12 @@ +/*-------------------------------------------------------------------------------------- + * 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 { QuickEdit } from './QuickEdit.js' + + +export const mountCtrlK = mountFnGenerator(QuickEdit) + + diff --git a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/ErrorBoundary.tsx b/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/ErrorBoundary.tsx index a095f6fd..2ccc54b2 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/ErrorBoundary.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/ErrorBoundary.tsx @@ -1,7 +1,7 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Glass Devtools, Inc. All rights reserved. - * Void Editor additions licensed under the AGPL 3.0 License. - *--------------------------------------------------------------------------------------------*/ +/*-------------------------------------------------------------------------------------- + * Copyright 2025 Glass Devtools, Inc. All rights reserved. + * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. + *--------------------------------------------------------------------------------------*/ import React, { Component, ErrorInfo, ReactNode } from 'react'; import { ErrorDisplay } from './ErrorDisplay.js'; diff --git a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/ErrorDisplay.tsx b/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/ErrorDisplay.tsx index 714fbf26..84fe410a 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/ErrorDisplay.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/ErrorDisplay.tsx @@ -1,14 +1,15 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Glass Devtools, Inc. All rights reserved. - * Void Editor additions licensed under the AGPL 3.0 License. - *--------------------------------------------------------------------------------------------*/ +/*-------------------------------------------------------------------------------------- + * Copyright 2025 Glass Devtools, Inc. All rights reserved. + * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. + *--------------------------------------------------------------------------------------*/ -import React, { useState } from 'react'; +import React, { useEffect, useState } from 'react'; import { AlertCircle, ChevronDown, ChevronUp, X } from 'lucide-react'; +import { errorDetails } from '../../../../../../../platform/void/common/llmMessageTypes.js'; export const ErrorDisplay = ({ - message, + message:message_, fullError, onDismiss, showDismiss, @@ -20,54 +21,46 @@ export const ErrorDisplay = ({ }) => { const [isExpanded, setIsExpanded] = useState(false); - let details: string | null = null; + const details = errorDetails(fullError) - if (fullError === null) { - details = null - } - else if (typeof fullError === 'object') { - details = JSON.stringify(fullError, null, 2) - } - else if (typeof fullError === 'string') { - details = null - } + const message = message_ === 'TypeError: fetch failed' ? 'TypeError: fetch failed. This likely means you specified the wrong endpoint in Void Settings.' : message_ return (
    {/* Header */} -
    -
    - -
    -

    +
    +
    + +
    +

    {/* eg Error */} Error

    -

    +

    {/* eg Something went wrong */} {message}

    -
    +
    {details && ( - )} {showDismiss && onDismiss && ( - )}
    @@ -75,10 +68,10 @@ export const ErrorDisplay = ({ {/* Expandable Details */} {isExpanded && details && ( -
    +
    - Full Error: -
    {details}
    + Full Error: +
    {details}
    )} diff --git a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/Sidebar.tsx b/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/Sidebar.tsx index 55fa83b7..839a6679 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/Sidebar.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/Sidebar.tsx @@ -1,7 +1,7 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Glass Devtools, Inc. All rights reserved. - * Void Editor additions licensed under the AGPL 3.0 License. - *--------------------------------------------------------------------------------------------*/ +/*-------------------------------------------------------------------------------------- + * Copyright 2025 Glass Devtools, Inc. All rights reserved. + * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. + *--------------------------------------------------------------------------------------*/ import React, { useEffect, useState } from 'react' import { mountFnGenerator } from '../util/mountFnGenerator.js' @@ -13,17 +13,26 @@ import { useIsDark, useSidebarState } from '../util/services.js'; // import { SidebarChat } from './SidebarChat.js'; import '../styles.css' -import { SidebarThreadSelector } from './SidebarThreadSelector.js'; import { SidebarChat } from './SidebarChat.js'; import ErrorBoundary from './ErrorBoundary.js'; export const Sidebar = ({ className }: { className: string }) => { const sidebarState = useSidebarState() - const { isHistoryOpen, currentTab: tab } = sidebarState + const { currentTab: tab } = sidebarState - const isDark = useIsDark() - return
    -
    + // const isDark = useIsDark() + return
    +
    {/* { const tabs = ['chat', 'settings', 'threadSelector'] @@ -31,11 +40,11 @@ export const Sidebar = ({ className }: { className: string }) => { sidebarStateService.setState({ currentTab: tabs[(index + 1) % tabs.length] as any }) }}>clickme {tab} */} -
    + {/*
    -
    +
    */}
    diff --git a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx b/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx index e5ff34c9..75e4579e 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx @@ -1,31 +1,38 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Glass Devtools, Inc. All rights reserved. - * Void Editor additions licensed under the AGPL 3.0 License. - *--------------------------------------------------------------------------------------------*/ +/*-------------------------------------------------------------------------------------- + * Copyright 2025 Glass Devtools, Inc. All rights reserved. + * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. + *--------------------------------------------------------------------------------------*/ -import React, { FormEvent, Fragment, useCallback, useEffect, useRef, useState } from 'react'; +import React, { ButtonHTMLAttributes, FormEvent, FormHTMLAttributes, Fragment, useCallback, useEffect, useRef, useState } from 'react'; -import { useSettingsState, useService, useSidebarState, useThreadsState } from '../util/services.js'; -import { generateDiffInstructions } from '../../../prompt/systemPrompts.js'; -import { userInstructionsStr } from '../../../prompt/stringifySelections.js'; -import { ChatMessage, CodeSelection, CodeStagingSelection } from '../../../threadHistoryService.js'; +import { useAccessor, useSidebarState, useChatThreadsState, useChatThreadsStreamState, useUriState } from '../util/services.js'; +import { ChatMessage, CodeSelection, CodeStagingSelection } from '../../../chatThreadService.js'; import { BlockCode } from '../markdown/BlockCode.js'; import { ChatMarkdownRender } from '../markdown/ChatMarkdownRender.js'; -import { IModelService } from '../../../../../../../editor/common/services/model.js'; import { URI } from '../../../../../../../base/common/uri.js'; import { EndOfLinePreference } from '../../../../../../../editor/common/model.js'; import { IDisposable } from '../../../../../../../base/common/lifecycle.js'; import { ErrorDisplay } from './ErrorDisplay.js'; import { OnError, ServiceSendLLMMessageParams } from '../../../../../../../platform/void/common/llmMessageTypes.js'; -import { getCmdKey } from '../../../helpers/getCmdKey.js' import { HistoryInputBox, InputBox } from '../../../../../../../base/browser/ui/inputbox/inputBox.js'; -import { VoidInputBox } from '../util/inputs.js'; -import { ModelDropdown } from '../void-settings-tsx/ModelDropdown.js'; +import { TextAreaFns, VoidCodeEditorProps, VoidInputBox2 } from '../util/inputs.js'; +import { ModelDropdown, WarningBox } from '../void-settings-tsx/ModelDropdown.js'; +import { chat_systemMessage, chat_prompt } from '../../../prompt/prompts.js'; +import { ISidebarStateService } from '../../../sidebarStateService.js'; +import { ILLMMessageService } from '../../../../../../../platform/void/common/llmMessageService.js'; +import { IModelService } from '../../../../../../../editor/common/services/model.js'; +import { SidebarThreadSelector } from './SidebarThreadSelector.js'; +import { useScrollbarStyles } from '../util/useScrollbarStyles.js'; +import { VOID_CTRL_L_ACTION_ID } from '../../../actionIDs.js'; +import { ArrowBigLeftDash, CopyX, Delete, FileX2, SquareX, X } from 'lucide-react'; +import { filenameToVscodeLanguage } from '../../../helpers/detectLanguage.js'; +import { Pencil } from 'lucide-react' +import { VOID_OPEN_SETTINGS_ACTION_ID } from '../../../voidSettingsPane.js'; -const IconX = ({ size, className = '' }: { size: number, className?: string }) => { +export const IconX = ({ size, className = '', ...props }: { size: number, className?: string } & React.SVGProps) => { return ( { return ( @@ -60,10 +67,9 @@ const IconArrowUp = ({ size, className = '' }: { size: number, className?: strin fill="black" fillRule="evenodd" clipRule="evenodd" - d="M15.1918 8.90615C15.6381 8.45983 16.3618 8.45983 16.8081 8.90615L21.9509 14.049C22.3972 14.4953 22.3972 15.2189 21.9509 15.6652C21.5046 16.1116 20.781 16.1116 20.3347 15.6652L17.1428 12.4734V22.2857C17.1428 22.9169 16.6311 23.4286 15.9999 23.4286C15.3688 23.4286 14.8571 22.9169 14.8571 22.2857V12.4734L11.6652 15.6652C11.2189 16.1116 10.4953 16.1116 10.049 15.6652C9.60265 15.2189 9.60265 14.4953 10.049 14.049L15.1918 8.90615Z" + d="M5.293 9.707a1 1 0 010-1.414l4-4a1 1 0 011.414 0l4 4a1 1 0 01-1.414 1.414L11 7.414V15a1 1 0 11-2 0V7.414L6.707 9.707a1 1 0 01-1.414 0z" > - ); }; @@ -86,9 +92,121 @@ const IconSquare = ({ size, className = '' }: { size: number, className?: string }; -const ScrollToBottomContainer = ({ children, className, style }: { children: React.ReactNode, className?: string, style?: React.CSSProperties }) => { +export const IconWarning = ({ size, className = '' }: { size: number, className?: string }) => { + return ( + + + + ); +}; + + +export const IconLoading = ({ className = '' }: { className?: string }) => { + + const [loadingText, setLoadingText] = useState('.'); + + useEffect(() => { + let intervalId; + + // Function to handle the animation + const toggleLoadingText = () => { + if (loadingText === '...') { + setLoadingText('.'); + } else { + setLoadingText(loadingText + '.'); + } + }; + + // Start the animation loop + intervalId = setInterval(toggleLoadingText, 300); + + // Cleanup function to clear the interval when component unmounts + return () => clearInterval(intervalId); + }, [loadingText, setLoadingText]); + + return
    {loadingText}
    ; + +} + +const useResizeObserver = () => { + const ref = useRef(null); + const [dimensions, setDimensions] = useState({ height: 0, width: 0 }); + + useEffect(() => { + if (ref.current) { + const resizeObserver = new ResizeObserver((entries) => { + if (entries.length > 0) { + const entry = entries[0]; + setDimensions({ + height: entry.contentRect.height, + width: entry.contentRect.width + }); + } + }); + + resizeObserver.observe(ref.current); + + return () => { + if (ref.current) + resizeObserver.unobserve(ref.current); + }; + } + }, []); + + return [ref, dimensions] as const; +}; + + + + +type ButtonProps = ButtonHTMLAttributes +const DEFAULT_BUTTON_SIZE = 22; +export const ButtonSubmit = ({ className, disabled, ...props }: ButtonProps & Required>) => { + + return +} + +export const ButtonStop = ({ className, ...props }: ButtonHTMLAttributes) => { + + return +} + + +const ScrollToBottomContainer = ({ children, className, style, scrollContainerRef }: { children: React.ReactNode, className?: string, style?: React.CSSProperties, scrollContainerRef: React.MutableRefObject }) => { const [isAtBottom, setIsAtBottom] = useState(true); // Start at bottom - const divRef = useRef(null); + + const divRef = scrollContainerRef const scrollToBottom = () => { if (divRef.current) { @@ -133,13 +251,6 @@ const ScrollToBottomContainer = ({ children, className, style }: { children: Rea }; -// read files from VSCode -const VSReadFile = async (modelService: IModelService, uri: URI): Promise => { - const model = modelService.getModel(uri) - if (!model) return null - return model.getValue(EndOfLinePreference.LF) -} - const getBasename = (pathStr: string) => { // 'unixify' path @@ -149,349 +260,439 @@ const getBasename = (pathStr: string) => { } export const SelectedFiles = ( - { type, selections, setStaging }: - | { type: 'past', selections: CodeSelection[] | null; setStaging?: undefined } - | { type: 'staging', selections: CodeStagingSelection[] | null; setStaging: ((files: CodeStagingSelection[]) => void) } + { type, selections, setSelections, showProspectiveSelections }: + | { type: 'past', selections: CodeSelection[]; setSelections?: undefined, showProspectiveSelections?: undefined } + | { type: 'staging', selections: CodeStagingSelection[]; setSelections: ((newSelections: CodeStagingSelection[]) => void), showProspectiveSelections?: boolean } ) => { // index -> isOpened const [selectionIsOpened, setSelectionIsOpened] = useState<(boolean)[]>(selections?.map(() => false) ?? []) + // state for tracking hover on clear all button + const [isClearHovered, setIsClearHovered] = useState(false) + + const accessor = useAccessor() + const commandService = accessor.get('ICommandService') + + // state for tracking prospective files + const { currentUri } = useUriState() + const [recentUris, setRecentUris] = useState([]) + const maxRecentUris = 10 + const maxProspectiveFiles = 3 + useEffect(() => { // handle recent files + if (!currentUri) return + setRecentUris(prev => { + const withoutCurrent = prev.filter(uri => uri.fsPath !== currentUri.fsPath) // remove duplicates + const withCurrent = [currentUri, ...withoutCurrent] + return withCurrent.slice(0, maxRecentUris) + }) + }, [currentUri]) + let prospectiveSelections: CodeStagingSelection[] = [] + if (type === 'staging' && showProspectiveSelections) { // handle prospective files + // add a prospective file if type === 'staging' and if the user is in a file, and if the file is not selected yet + prospectiveSelections = recentUris + .filter(uri => !selections.find(s => s.range === null && s.fileURI.fsPath === uri.fsPath)) + .slice(0, maxProspectiveFiles) + .map(uri => ({ + type: 'File', + fileURI: uri, + selectionStr: null, + range: null, + })) + } + + const allSelections = [...selections, ...prospectiveSelections] + + if (allSelections.length === 0) { + return null + } + return ( - !!selections && selections.length !== 0 && ( -
    - {selections.map((selection, i) => { +
    - const showSelectionText = selection.selectionStr && selectionIsOpened[i] + {allSelections.map((selection, i) => { - return ( -
    - {/* selection summary */} -
    selections.length - 1 + + const thisKey = `${isThisSelectionProspective}-${i}-${selections.length}` + + const selectionHTML = (
    + {/* selection summary */} +
    +
    { + ${isThisSelectionProspective ? 'bg-void-1 text-void-fg-3 opacity-80' : 'bg-void-bg-3 hover:brightness-95 text-void-fg-1'} + text-xs text-nowrap + border rounded-sm ${isClearHovered && !isThisSelectionProspective ? 'border-void-border-1' : 'border-void-border-2'} hover:border-void-border-1 + transition-all duration-150`} + onClick={() => { + if (isThisSelectionProspective) { // add prospective selection to selections + if (type !== 'staging') return; // (never) + setSelections([...selections, selection as CodeStagingSelection]) + + } else if (isThisSelectionAFile) { // open files + commandService.executeCommand('vscode.open', selection.fileURI, { + preview: true, + // preserveFocus: false, + }); + } else { // show text setSelectionIsOpened(s => { const newS = [...s] newS[i] = !newS[i] return newS }); - }} - > - - {/* file name */} - {getBasename(selection.fileURI.fsPath)} - {/* selection range */} - {selection.selectionStr !== null ? ` (${selection.range.startLineNumber}-${selection.range.endLineNumber})` : ''} - - - {/* type of selection */} - {selection.selectionStr !== null ? 'Selection' : 'File'} - - {/* X button */} - {type === 'staging' && // hoveredIdx === i - { - e.stopPropagation(); - if (type !== 'staging') return; - setStaging([...selections.slice(0, i), ...selections.slice(i + 1)]) - setSelectionIsOpened(o => [...o.slice(0, i), ...o.slice(i + 1)]) - }} - > - - } -
    - {/* selection text */} - {showSelectionText && -
    - -
    - } + }} + > + + {/* file name */} + {getBasename(selection.fileURI.fsPath)} + {/* selection range */} + {!isThisSelectionAFile ? ` (${selection.range.startLineNumber}-${selection.range.endLineNumber})` : ''} + + + {/* X button */} + {type === 'staging' && !isThisSelectionProspective && + { + e.stopPropagation(); // don't open/close selection + if (type !== 'staging') return; + setSelections([...selections.slice(0, i), ...selections.slice(i + 1)]) + setSelectionIsOpened(o => [...o.slice(0, i), ...o.slice(i + 1)]) + }} + > + + } + +
    - ) - })} -
    - ) + + {/* clear all selections button */} + {type !== 'staging' || selections.length === 0 || i !== selections.length - 1 + ? null + :
    +
    setIsClearHovered(true)} + onMouseLeave={() => setIsClearHovered(false)} + > + { setSelections([]) }} + /> +
    +
    + } +
    + {/* selection text */} + {isThisSelectionOpened && +
    { + e.stopPropagation(); // don't focus input box + }} + > + +
    + } +
    ) + + return + {selections.length > 0 && i === selections.length && +
    // divider between `selections` and `prospectiveSelections` + } + {selectionHTML} +
    + + })} + + +
    + ) } -const ChatBubble = ({ chatMessage }: { - chatMessage: ChatMessage -}) => { + +const ChatBubble_ = ({ isEditMode, isLoading, children, role }: { role: ChatMessage['role'], children: React.ReactNode, isLoading: boolean, isEditMode: boolean }) => { + + return
    +
    + {children} + {isLoading && } +
    + + {/* edit button */} + {/* {role === 'user' && + { setIsEditMode(v => !v); }} + /> + } */} +
    +} + + +const ChatBubble = ({ chatMessage, isLoading }: { chatMessage: ChatMessage, isLoading?: boolean, }) => { const role = chatMessage.role - if (!chatMessage.displayContent) + // edit mode state + const [isEditMode, setIsEditMode] = useState(false) + + + if (!chatMessage.content) { // don't show if empty return null + } let chatbubbleContents: React.ReactNode if (role === 'user') { chatbubbleContents = <> - + {chatMessage.displayContent} + + {/* {!isEditMode ? chatMessage.displayContent : <>} */} + {/* edit mode content */} + {/* TODO this should be the same input box as in the Sidebar */} + {/*