Compare commits
540 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 93d40be1ac | |||
| cfc39b83bb | |||
| 9c76a35837 | |||
| 4a00bd3e00 | |||
| 6d6e2fa974 | |||
| 971ce88df2 | |||
| 7e34037ffe | |||
| aa835d468b | |||
| 661eba3ae9 | |||
| de17b3add1 | |||
| 6fa3901ab0 | |||
| d14a2f1fff | |||
| c1f3b92c51 | |||
| 0de63a088d | |||
| 0de1c20551 | |||
| a769eb4db0 | |||
| 3e7e54b88f | |||
| 1b43799371 | |||
| 8438f80834 | |||
| 63b15af385 | |||
| 57c9184f4e | |||
| 0b6dd721e9 | |||
| 88ac766f53 | |||
| 58466afe72 | |||
| 04efe1d235 | |||
| aa9a102b71 | |||
| dc0f9bdada | |||
| 75f30bf7cd | |||
| 4a43b628d4 | |||
| 38370c751e | |||
| 764b1a2ccf | |||
| 5c047b78f3 | |||
| 5cbd0a19a4 | |||
| d2fb0fb4ff | |||
| 1dffbfb061 | |||
| 3ae8f75641 | |||
| df6de2d7fc | |||
| 4ebfa91a83 | |||
| 9f20476eea | |||
| fd5e523434 | |||
| 2c2714273e | |||
| c650091418 | |||
| d96a9d5f6b | |||
| a9b3cc146b | |||
| bdb897d032 | |||
| 2a876d8efe | |||
| bf4392a52f | |||
| dd24c3180d | |||
| f40acd76f1 | |||
| 96de4f028f | |||
| 2ecce822f3 | |||
| ea10765abd | |||
| 16f6181395 | |||
| 19ebf4a1a2 | |||
| 63b71dec24 | |||
| 1079893527 | |||
| 64ac6d4a12 | |||
| 9005de65a6 | |||
| 86dfc5521d | |||
| b005b1e95a | |||
| ce14986d2f | |||
| 06ce9e1017 | |||
| 09f5d3e14b | |||
| 73c3a8133a | |||
| 8918a14404 | |||
| 1959c53d75 | |||
| 33d80bed80 | |||
| 970f0bdb9d | |||
| 7711c74ef0 | |||
| e14aab632d | |||
| 62c2622ced | |||
| 0f26ee2288 | |||
| 6a44d668db | |||
| 858b6f6a91 | |||
| 7206209743 | |||
| 02f64b7ff6 | |||
| ac1788ae9a | |||
| 5699cf19f4 | |||
| 9ab9196b38 | |||
| baa89cc17d | |||
| 667769c987 | |||
| d3aa0bc3cc | |||
| d3547134e7 | |||
| b92420012c | |||
| 0fd10f404e | |||
| 137e0068a3 | |||
| 366dbf0b52 | |||
| 89d08071fc | |||
| 8a8ed1ac56 | |||
| 491312218f | |||
| b74b031906 | |||
| 5608aca168 | |||
| fecfbd924a | |||
| a78a6169f8 | |||
| 628adedaec | |||
| 432a1766af | |||
| 74f8303803 | |||
| 2bc3d67e39 | |||
| 41fe5c50e2 | |||
| 36b1b56690 | |||
| 249eee341d | |||
| 2c61310077 | |||
| a519100852 | |||
| a38fc81a58 | |||
| af41d6a439 | |||
| 7244d433dd | |||
| 131493b5e1 | |||
| 4c5e444bcb | |||
| 2f55580e67 | |||
| b24cd1ecb7 | |||
| a519bd7690 | |||
| 8591d06244 | |||
| a26816d597 | |||
| ea6e025511 | |||
| 7849b0a107 | |||
| 152e605856 | |||
| bc6150aeac | |||
| 05d8b3a982 | |||
| d91ec9da2e | |||
| 198a948f6c | |||
| 1839acab1f | |||
| 6ad48ffa20 | |||
| 0975f1bf5f | |||
| 9cfcf396c1 | |||
| 0bcd88dad6 | |||
| 343ee5eb94 | |||
| d9e4679b65 | |||
| 4950de2cb5 | |||
| 28bf838b55 | |||
| b01684393a | |||
| 34b8027fb9 | |||
| f7af9c336b | |||
| a479a32ad5 | |||
| 7cdb003c47 | |||
| 4f9f16b93f | |||
| 7dcb08b09a | |||
| 7fbe8d905f | |||
| 79fb8f63a7 | |||
| 0dfa81f637 | |||
| d504daffa5 | |||
| fc97949887 | |||
| 0346e90ca7 | |||
| efeb85b48b | |||
| d698e6f3c1 | |||
| 2161d740df | |||
| a3e6deb1a8 | |||
| 96b5b72d30 | |||
| e2e8279fce | |||
| 5ed1f465a2 | |||
| 995357710e | |||
| a98106072c | |||
| c1be466467 | |||
| 6f6f43c46e | |||
| 9cbe83b57c | |||
| e46b295449 | |||
| 3fd3634257 | |||
| 231990088a | |||
| 20da82762e | |||
| 0d9be68aff | |||
| 165b5a4fd2 | |||
| 812014d6d3 | |||
| d3e73b92e0 | |||
| 00da05f24f | |||
| e4694256eb | |||
| 18b278577d | |||
| a252bb7a30 | |||
| c240360710 | |||
| 893308c38e | |||
| 48defe849d | |||
| 3c03f58488 | |||
| 2470e0de7f | |||
| 387fd64db0 | |||
| 4ffd269db5 | |||
| 5ce25e20c3 | |||
| 63efa219f5 | |||
| 826d148305 | |||
| 323d80ef55 | |||
| 5fa21be9d2 | |||
| 902588638d | |||
| a1ab094652 | |||
| 902b460b9c | |||
| 073205ace3 | |||
| 98c1d81875 | |||
| 2f49650e15 | |||
| 1be9300dc4 | |||
| d9b77b67b2 | |||
| 2f674f5323 | |||
| c8fa39e552 | |||
| 3b73e3ab26 | |||
| c606ac4361 | |||
| da4c9c8c40 | |||
| 31ed11393f | |||
| 1c5157d97e | |||
| cf241867d8 | |||
| 5a9f9f70d9 | |||
| 19ff19fb80 | |||
| 87076ed586 | |||
| 473250d8bc | |||
| cd983f0dd9 | |||
| 5c4753555e | |||
| 8f79222b8b | |||
| 145fd9614b | |||
| 41057c20fc | |||
| 528b8a6b9b | |||
| e3941d5565 | |||
| 4d11a4fd29 | |||
| 3baaf8d6c4 | |||
| 0ec0c324b1 | |||
| aa580351d9 | |||
| 570e13127d | |||
| fb7b50b36b | |||
| 9cd8295dab | |||
| 9ec13d2ad8 | |||
| d9cf21448c | |||
| 27d009314b | |||
| 5fdd21f12b | |||
| dc5c7e9a46 | |||
| d1c5cf518e | |||
| c1123f2cfd | |||
| fb0bb0ca31 | |||
| 4863802713 | |||
| 5589b2e302 | |||
| 327dbb937b | |||
| a3e62ee6f4 | |||
| d7a6611d89 | |||
| e4bb15ef64 | |||
| a98c77572e | |||
| e4d747d0a6 | |||
| 01b1358a01 | |||
| c7a6bddfbe | |||
| 55ea8e61c6 | |||
| 4f7fd66c2f | |||
| 4547363dde | |||
| a1dfeb0f00 | |||
| b036a8c59c | |||
| 72cd23f0ac | |||
| 49c2bcb848 | |||
| 97ea0f77f9 | |||
| 0cd6e0fb67 | |||
| 7c227e237f | |||
| 3089bb31b5 | |||
| 018fd016c8 | |||
| f69f90b4c8 | |||
| 000747eb88 | |||
| 74022e0306 | |||
| 21708fd7e0 | |||
| 5593ff5d79 | |||
| 197067af5a | |||
| eec4f6c95e | |||
| 3e045117d6 | |||
| 67c557b7b9 | |||
| 5ffd569fd8 | |||
| ff83bb5edc | |||
| a3f145328b | |||
| 02894c12de | |||
| 52b6c81507 | |||
| 5df5f37a6a | |||
| f247b76a6e | |||
| d1c659be9d | |||
| a6b4955f8d | |||
| 03b0b98eed | |||
| 7f7d30e40b | |||
| f68bc52023 | |||
| 520c971e3e | |||
| 657e752987 | |||
| 458a5160b0 | |||
| a90bbffeb9 | |||
| 99ff885ce2 | |||
| a97047ed43 | |||
| 00f10c283e | |||
| 57628ec244 | |||
| c54dcd0b0c | |||
| e957932f39 | |||
| fea9d380cc | |||
| f2318cc8a2 | |||
| 8264cccd04 | |||
| a1ca0f9291 | |||
| 58f06d3ebb | |||
| e06eda1cc4 | |||
| f4d72e3ee8 | |||
| b3837a3099 | |||
| c6ae6d5152 | |||
| 2d43dbded1 | |||
| a34d652a1d | |||
| a0569b1ead | |||
| b3ebe85c04 | |||
| 0919775e77 | |||
| da6636a948 | |||
| 6327c584cc | |||
| f7cf44136b | |||
| c8635a1bdb | |||
| d7501112f5 | |||
| d7ca99e23d | |||
| 387be4db76 | |||
| 2aec37fc57 | |||
| 7e5cc27349 | |||
| 151cdb13e6 | |||
| d54ccff4a2 | |||
| c2f358bd5e | |||
| 198f04d87a | |||
| 326e75eb45 | |||
| 7891a908ff | |||
| 6970ec9fd2 | |||
| fd4f3c3ab6 | |||
| aa2394e207 | |||
| 3aa6d3d28a | |||
| 98c1a7e528 | |||
| c94dda65f7 | |||
| 24e276ba47 | |||
| 028061c879 | |||
| 4e0dd8f8db | |||
| c239682ad8 | |||
| cd9bb6a07d | |||
| fd989b2c13 | |||
| 3bfb50e6ee | |||
| 362d481ec4 | |||
| 1b4943f39e | |||
| 8c443ab780 | |||
| 7519f05238 | |||
| c2fc2577e0 | |||
| 6e1f8a08b3 | |||
| d9be67d17d | |||
| 60d22674e2 | |||
| e8c0ee631c | |||
| 6ba9d10a4a | |||
| 4eff8f8e76 | |||
| 66e349c72e | |||
| 146d63c41e | |||
| 82439406a0 | |||
| efeb0d28b7 | |||
| aba7d7bf90 | |||
| f517dc79a9 | |||
| dc33d4d0c0 | |||
| d3647bf93c | |||
| 29c4603909 | |||
| 85426e28bd | |||
| c6ff563e1b | |||
| 7ec574eeee | |||
| 66c880945c | |||
| 6f1905a962 | |||
| 268d2ae99a | |||
| 1521038e03 | |||
| dce0378eb2 | |||
| ce7ee62abe | |||
| 015943329f | |||
| 4b15885147 | |||
| 2e3f608805 | |||
| 789016709a | |||
| 835a2bdf92 | |||
| c02f1de758 | |||
| ddd9cf857f | |||
| e4fd6d05a4 | |||
| 9756ab6d16 | |||
| 433884b36c | |||
| 0d48db1a0c | |||
| 10adc13b60 | |||
| 2103a95930 | |||
| f8c454544c | |||
| e3da020c0a | |||
| 7dec57ae04 | |||
| f8db386c3b | |||
| 2d583b87dc | |||
| b9dba7d08b | |||
| 4e18dd4ac3 | |||
| 471333ec16 | |||
| d43453e5ed | |||
| 3abafa76ec | |||
| e48e798afc | |||
| 4619e4146a | |||
| f522281c6d | |||
| 1e3668058b | |||
| 88c921b059 | |||
| 8cfe494632 | |||
| d4fc59034a | |||
| 9ba70cdc4a | |||
| 12d08eb709 | |||
| f3270a2219 | |||
| bd30f14980 | |||
| 725de600cf | |||
| ccfd54fcfe | |||
| 071c4893c2 | |||
| 8f15c67960 | |||
| fd546ff7b7 | |||
| 895628bb91 | |||
| 4898875532 | |||
| 9ddc22b63c | |||
| 44438fe1fe | |||
| 64e43fa53e | |||
| 07f4a59ce6 | |||
| c7d5d5d183 | |||
| 9f96ebcfc2 | |||
| 01839654d4 | |||
| 37200dacbd | |||
| 86612fd5ed | |||
| 937816519c | |||
| 35e366a5ba | |||
| d2fdbd7ffd | |||
| 33a003fc2d | |||
| 9589134830 | |||
| 164684a39b | |||
| accff821f8 | |||
| 00fc9964e5 | |||
| 4b8afc1771 | |||
| 3e5ead2547 | |||
| d067966b58 | |||
| 278c52bd35 | |||
| 52d89e5815 | |||
| 4cf13b67b0 | |||
| fc327e3fcb | |||
| 59afe2d6ff | |||
| 1d590510ae | |||
| c5a6f476ef | |||
| ae4ece1826 | |||
| ec2f04be91 | |||
| 8a56ea9755 | |||
| 261736bde5 | |||
| 7dd5171bd2 | |||
| becd109776 | |||
| 0d9f441967 | |||
| 6386fc95ee | |||
| 06688228ae | |||
| b3d2453e7f | |||
| 29e1f312c3 | |||
| b9b645522a | |||
| 83bf660b76 | |||
| 268f08c51d | |||
| 046942be75 | |||
| 6fed97eb83 | |||
| e888f261fe | |||
| 6e8498414c | |||
| bcda1dbe2b | |||
| be3128acb4 | |||
| 29cbb15911 | |||
| 8da71dc1b7 | |||
| a69b99b562 | |||
| 6868a8b1d8 | |||
| 10f8fcc17d | |||
| ee9f84a475 | |||
| c056214684 | |||
| fcf03f2710 | |||
| ed50ed5319 | |||
| f15f5ce8ed | |||
| 11578d6288 | |||
| d904dbb93e | |||
| 6df0c0ac69 | |||
| 0f98af7fc1 | |||
| e1b8e49b13 | |||
| e6f6c7500d | |||
| 951da8e31a | |||
| 2701226ef3 | |||
| 9abc3ad3dc | |||
| 966470cf68 | |||
| b2cf709685 | |||
| ab0bf4bef0 | |||
| db20584a19 | |||
| 7d1d638dae | |||
| 0d1c4944c1 | |||
| f7c4d28d55 | |||
| a2ae7a9674 | |||
| 262e15764b | |||
| 26f97f5939 | |||
| a9dc35e5db | |||
| 326dcae24f | |||
| f880ddd6dc | |||
| 7925e438c6 | |||
| 2f5e954ebf | |||
| 6a8ac1a68e | |||
| c5b6fd9b75 | |||
| 7e68a485f2 | |||
| af85949561 | |||
| a1da8bd12f | |||
| ebd78a902d | |||
| f96c03c846 | |||
| fed865e753 | |||
| 8d2917d331 | |||
| f4c1fc8917 | |||
| 0a3f31acae | |||
| 924c953697 | |||
| 446dede5e4 | |||
| 7bb0993249 | |||
| 4968452003 | |||
| 037f9f38e5 | |||
| e132c87792 | |||
| 07d87c8bf0 | |||
| c7ad7d3761 | |||
| ff9d21eee0 | |||
| e4953057ef | |||
| 865c34f282 | |||
| ec65903692 | |||
| a08e6078f2 | |||
| 20e1b287f8 | |||
| f2f9b6ccca | |||
| c4e4701dc3 | |||
| 37af31f958 | |||
| b5dff4cf0f | |||
| 3621bb63f4 | |||
| da247c6ed4 | |||
| ce634eadaa | |||
| 910ed7faa9 | |||
| 9e852355d9 | |||
| 8ae4700bb2 | |||
| f13c30ae0d | |||
| af244938b8 | |||
| a53f92432b | |||
| ca2b18d9da | |||
| 5293095eb8 | |||
| 86346f1bf0 | |||
| a0672f845d | |||
| 5083b8e971 | |||
| 85c2c80db0 | |||
| b677d3dfc1 | |||
| cc177b9cd4 | |||
| dd204978e5 | |||
| fb48c4e67e | |||
| 74fa5cb505 | |||
| fae03c3060 | |||
| 5453acdb19 | |||
| 7047459e57 | |||
| 68722ee5c3 | |||
| 4a993a716b | |||
| 23d1d4169a | |||
| f688e1ffae | |||
| a443315c14 | |||
| eeacb2d3c9 | |||
| cdbdf3c5c7 | |||
| 825c40bfff | |||
| a3d3309b9d | |||
| b0d21dfdb1 | |||
| 6feb713682 | |||
| 667dc8bd7e | |||
| bdb0c65346 | |||
| 9543bf8eb3 | |||
| 4065c5b504 | |||
| 4729e4dac3 | |||
| d85a316536 | |||
| 1e975ecc3b | |||
| 8476a99903 | |||
| 85314f8d47 | |||
| 2d80a29275 | |||
| d8e9b50890 |
@@ -11,6 +11,7 @@
|
||||
**/extensions/markdown-language-features/notebook-out/**
|
||||
**/extensions/markdown-math/notebook-out/**
|
||||
**/extensions/notebook-renderers/renderer-out/index.js
|
||||
**/extensions/open-remote-ssh/out/extension.js
|
||||
**/extensions/simple-browser/media/index.js
|
||||
**/extensions/typescript-language-features/test-workspace/**
|
||||
**/extensions/typescript-language-features/extension.webpack.config.js
|
||||
|
||||
@@ -22,3 +22,5 @@ product.overrides.json
|
||||
*.snap.actual
|
||||
.vscode-test
|
||||
.tmp/
|
||||
.tmp2/
|
||||
.tool-versions
|
||||
|
||||
@@ -29,6 +29,34 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "npm",
|
||||
"script": "watchreactd",
|
||||
"label": "React - Build",
|
||||
"isBackground": true,
|
||||
"presentation": {
|
||||
"reveal": "never",
|
||||
"group": "buildWatchers",
|
||||
"close": false
|
||||
},
|
||||
"problemMatcher": {
|
||||
"owner": "typescript",
|
||||
"applyTo": "closedDocuments",
|
||||
"fileLocation": [
|
||||
"absolute"
|
||||
],
|
||||
"pattern": {
|
||||
"regexp": "Error: ([^(]+)\\((\\d+|\\d+,\\d+|\\d+,\\d+,\\d+,\\d+)\\): (.*)$",
|
||||
"file": 1,
|
||||
"location": 2,
|
||||
"message": 3
|
||||
},
|
||||
"background": {
|
||||
"beginsPattern": "Starting compilation",
|
||||
"endsPattern": "Finished compilation"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "npm",
|
||||
"script": "watch-extensionsd",
|
||||
@@ -61,7 +89,8 @@
|
||||
"label": "VS Code - Build",
|
||||
"dependsOn": [
|
||||
"Core - Build",
|
||||
"Ext - Build"
|
||||
"Ext - Build",
|
||||
"React - Build"
|
||||
],
|
||||
"group": {
|
||||
"kind": "build",
|
||||
|
||||
@@ -1,18 +1,29 @@
|
||||
# Contributing to Void
|
||||
|
||||
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!
|
||||
### Welcome! 👋
|
||||
This is the official guide on how to contribute to Void. We want to make it as easy as possible to contribute, so if you have any questions or comments, reach out via email or discord!
|
||||
|
||||
There are a few ways to contribute:
|
||||
|
||||
- 👨💻 Build new features - see [Issues](https://github.com/voideditor/void/issues).
|
||||
- 💫 Complete items on the [Roadmap](https://github.com/orgs/voideditor/projects/2).
|
||||
- 💡 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/`.
|
||||
- 🪴 Start new Issues - see [Issues](https://github.com/voideditor/void/issues).
|
||||
|
||||
|
||||
|
||||
## Building the full IDE
|
||||
### Codebase Guide
|
||||
|
||||
We highly recommend reading [this](https://github.com/microsoft/vscode/wiki/Source-Code-Organization) article on VSCode's sourcecode organization.
|
||||
|
||||
<!-- ADD BLOG HERE
|
||||
We wrote a [guide to working in VSCode].
|
||||
-->
|
||||
|
||||
Most of Void's code lives in the folder `src/vs/workbench/contrib/void/`.
|
||||
|
||||
|
||||
|
||||
|
||||
## Building Void
|
||||
|
||||
### a. Build Prerequisites - Mac
|
||||
|
||||
@@ -41,21 +52,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 open your terminal and run:
|
||||
|
||||
1. `npm install` to install all dependencies.
|
||||
2. `npm run watchreact` to build Void's browser dependencies like React.
|
||||
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 <kbd>Cmd+Shift+B</kbd> (Mac).
|
||||
- Press <kbd>Ctrl+Shift+B</kbd> (Windows/Linux).
|
||||
- This step can take ~5 min. The build is done when you see two check marks.
|
||||
4. Run Void.
|
||||
- Run `./scripts/code.sh` (Mac/Linux).
|
||||
- Run `./scripts/code.sh` (Mac/Linux).
|
||||
- Run `./scripts/code.bat` (Windows).
|
||||
- This command should open up the built IDE. You can always press <kbd>Ctrl+R</kbd> (<kbd>Cmd+R</kbd>) inside the new window to see changes without re-building, or press or <kbd>Ctrl+Shift+P</kbd> in the new window and run "Reload Window".
|
||||
- If you would like to reset Void back to its default settings, you can run `./scripts/code.sh --user-data-dir ./.tmp/user-data --extensions-dir ./.tmp/extensions` (mac). This will save all data and extensions to the `.tmp` folder. You can delete this folder to reset your settings.
|
||||
6. Nice-to-knows.
|
||||
- You can always press <kbd>Ctrl+R</kbd> (<kbd>Cmd+R</kbd>) inside the new window to reload and see your new changes. It's faster than <kbd>Ctrl+Shift+P</kbd> 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 +82,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 have the same NodeJS version as `.nvmrc`.
|
||||
- Make sure your `npm run watchreact` is running if you change any React files, or else you'll need to re-build.
|
||||
- If you see missing styles, go to `src2/styles.css` and re-save the file.
|
||||
- If you get `"TypeError: Failed to fetch dynamically imported module: vscode-file://vscode-app/.../workbench.desktop.main.js", source: file:///.../bootstrap-window.js`, make sure all imports end with `.js`.
|
||||
- If you have any questions, feel free to [submit an issue](https://github.com/voideditor/void/issues/new). For building questions, you can also refer to VSCode's full [How to Contribute](https://github.com/microsoft/vscode/wiki/How-to-Contribute) page.
|
||||
- Make sure you followed the prerequisite steps.
|
||||
- Make sure you have Node version `20.16.0` (the version in `.nvmrc`)!
|
||||
- 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.
|
||||
|
||||
|
||||
|
||||
## Bundling
|
||||
## Packaging
|
||||
|
||||
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 +111,27 @@ 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
|
||||
```
|
||||
|
||||
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).
|
||||
### 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 🙂
|
||||
|
||||
|
||||
## 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 🙂.
|
||||
|
||||
|
||||
<!--
|
||||
@@ -116,6 +139,8 @@ We're always glad to talk about new ideas, help you get set up, and make sure yo
|
||||
|
||||
We keep track of all the files we've changed with Void so it's easy to rebase:
|
||||
|
||||
Edit: far too many changes to track... this is old
|
||||
|
||||
- README.md
|
||||
- CONTRIBUTING.md
|
||||
- VOID_USEFUL_LINKS.md
|
||||
@@ -135,8 +160,5 @@ We keep track of all the files we've changed with Void so it's easy to rebase:
|
||||
- build/npm/dirs.js
|
||||
|
||||
- vscode.proposed.editorInsets.d.ts - not modified, but code copied
|
||||
|
||||
-->
|
||||
|
||||
## References
|
||||
|
||||
For some useful links we've compiled on VSCode, see [`VOID_USEFUL_LINKS.md`](https://github.com/voideditor/void/blob/main/VOID_USEFUL_LINKS.md).
|
||||
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
|
||||
@@ -1,16 +1,39 @@
|
||||
# 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.
|
||||
<div align="center">
|
||||
<img
|
||||
src="./src/vs/workbench/browser/parts/editor/media/slice_of_void.png"
|
||||
alt="Void Welcome"
|
||||
width="300"
|
||||
height="300"
|
||||
/>
|
||||
</div>
|
||||
|
||||
Void is the open-source Cursor alternative.
|
||||
|
||||
This repo contains the full sourcecode for Void. We are currently in [open beta](https://voideditor.com/email) for Discord members (see the `announcements` channel), with a waitlist for our official release. If you're new, welcome!
|
||||
|
||||
- 👋 [Discord](https://discord.gg/RSNjgaugJs)
|
||||
|
||||
- 🔨 [Contribute](https://github.com/voideditor/void/blob/main/CONTRIBUTING.md)
|
||||
|
||||
- 🚙 [Roadmap](https://github.com/orgs/voideditor/projects/2)
|
||||
|
||||
- 📝 [Changelog](https://voideditor.com/changelog)
|
||||
|
||||
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. Feel free to attend a weekly meeting in our Discord channel if you'd like to contribute!
|
||||
|
||||
2. To get started working on Void, see [Contributing](https://github.com/voideditor/void/blob/main/CONTRIBUTING.md).
|
||||
|
||||
3. We're open to collaborations and suggestions 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: hello@voideditor.com.
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
<!-- BEGIN MICROSOFT SECURITY.MD V0.0.9 BLOCK -->
|
||||
|
||||
## 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).
|
||||
|
||||
<!-- END MICROSOFT SECURITY.MD BLOCK -->
|
||||
@@ -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).
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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}`
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -89,24 +89,24 @@ async function main(buildDir?: string): Promise<void> {
|
||||
// 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}`
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -134,6 +134,7 @@ module.exports.indentationFilter = [
|
||||
'!extensions/markdown-math/notebook-out/*.js',
|
||||
'!extensions/ipynb/notebook-out/**',
|
||||
'!extensions/notebook-renderers/renderer-out/*.js',
|
||||
'!extensions/open-remote-ssh/out/*.js',
|
||||
'!extensions/simple-browser/media/*.js',
|
||||
];
|
||||
|
||||
|
||||
@@ -55,6 +55,7 @@ const compilations = [
|
||||
'extensions/microsoft-authentication/tsconfig.json',
|
||||
'extensions/notebook-renderers/tsconfig.json',
|
||||
'extensions/npm/tsconfig.json',
|
||||
'extensions/open-remote-ssh/tsconfig.json',
|
||||
'extensions/php-language-features/tsconfig.json',
|
||||
'extensions/references-view/tsconfig.json',
|
||||
'extensions/search-result/tsconfig.json',
|
||||
|
||||
@@ -516,7 +516,7 @@ function tweakProductForServerWeb(product) {
|
||||
|
||||
['', 'min'].forEach(minified => {
|
||||
const sourceFolderName = `out-vscode-${type}${dashed(minified)}`;
|
||||
const destinationFolderName = `vscode-${type}${dashed(platform)}${dashed(arch)}`;
|
||||
const destinationFolderName = `void-${type}${dashed(platform)}${dashed(arch)}`;
|
||||
|
||||
const serverTaskCI = task.define(`vscode-${type}${dashed(platform)}${dashed(arch)}${dashed(minified)}-ci`, task.series(
|
||||
gulp.task(`node-${platform}-${arch}`),
|
||||
|
||||
@@ -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 }))
|
||||
|
||||
@@ -36,6 +36,7 @@ const dirs = [
|
||||
'extensions/microsoft-authentication',
|
||||
'extensions/notebook-renderers',
|
||||
'extensions/npm',
|
||||
'extensions/open-remote-ssh',
|
||||
'extensions/php-language-features',
|
||||
'extensions/references-view',
|
||||
'extensions/search-result',
|
||||
|
||||
@@ -23,7 +23,7 @@ AppMutex={code:GetAppMutex}
|
||||
SetupMutex={#AppMutex}setup
|
||||
; 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"
|
||||
; WizardImageFile="{#RepoDir}\resources\win32\inno-void.bmp"
|
||||
WizardSmallImageFile="{#RepoDir}\resources\win32\inno-void.bmp"
|
||||
SetupIconFile={#RepoDir}\resources\win32\code.ico
|
||||
UninstallDisplayIcon={app}\{#ExeBasename}.exe
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
#!/bin/bash
|
||||
set -e # Exit on error
|
||||
set -x # Print commands as they are executed
|
||||
|
||||
# Configuration
|
||||
APP_NAME="void"
|
||||
APP_VERSION="1.0.0"
|
||||
ARCH="x86_64"
|
||||
|
||||
export ARCH
|
||||
|
||||
# Check if void binary exists in current directory
|
||||
if [ ! -f "./void" ]; then
|
||||
echo "Error: void binary not found in current directory"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if icon exists
|
||||
if [ ! -f "./void.png" ]; then
|
||||
echo "Error: void.png icon not found in current directory"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Create temporary directory
|
||||
TEMP_DIR="$(mktemp -d)"
|
||||
echo "Created temporary directory: $TEMP_DIR"
|
||||
APP_DIR="$TEMP_DIR/$APP_NAME.AppDir"
|
||||
|
||||
# Create basic AppDir structure
|
||||
mkdir -pv "$APP_DIR/usr/bin"
|
||||
mkdir -pv "$APP_DIR/usr/lib"
|
||||
mkdir -pv "$APP_DIR/usr/share/applications"
|
||||
mkdir -pv "$APP_DIR/usr/share/icons/hicolor/256x256/apps"
|
||||
|
||||
# Exclude create-appimage.sh and appimagetool-x86_64.AppImage from being copied
|
||||
echo "Copying files excluding create-appimage.sh and appimagetool-x86_64.AppImage..."
|
||||
for file in ./*; do
|
||||
if [[ "$file" != "./create-appimage.sh" && "$file" != "./appimagetool-x86_64.AppImage" ]]; then
|
||||
cp -rv "$file" "$APP_DIR/usr/bin/"
|
||||
fi
|
||||
done
|
||||
|
||||
# Copy the icon to required locations
|
||||
cp -v ./void.png "$APP_DIR/void.png"
|
||||
cp -v ./void.png "$APP_DIR/usr/share/icons/hicolor/256x256/apps/void.png"
|
||||
|
||||
# Copy dependencies with error checking
|
||||
echo "Copying dependencies..."
|
||||
for lib in $(ldd ./void | grep "=> /" | awk '{print $3}'); do
|
||||
if [ -f "$lib" ]; then
|
||||
cp -v "$lib" "$APP_DIR/usr/lib/" || echo "Failed to copy $lib"
|
||||
else
|
||||
echo "Warning: Library $lib not found"
|
||||
fi
|
||||
done
|
||||
|
||||
# Create desktop file with error checking
|
||||
echo "Creating desktop file..."
|
||||
if ! cat > "$APP_DIR/$APP_NAME.desktop" <<EOF
|
||||
[Desktop Entry]
|
||||
Name=$APP_NAME
|
||||
Exec=void
|
||||
Icon=void
|
||||
Type=Application
|
||||
Categories=Utility;
|
||||
Comment=Void Linux Application
|
||||
EOF
|
||||
then
|
||||
echo "Error creating desktop file"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Make desktop file executable
|
||||
chmod +x "$APP_DIR/$APP_NAME.desktop"
|
||||
|
||||
# Copy the desktop file to the applications directory
|
||||
cp -v "$APP_DIR/$APP_NAME.desktop" "$APP_DIR/usr/share/applications/"
|
||||
|
||||
# Create AppRun with error checking
|
||||
echo "Creating AppRun..."
|
||||
if ! cat > "$APP_DIR/AppRun" <<EOF
|
||||
#!/bin/bash
|
||||
cd "\$(dirname "\$0")/usr/bin"
|
||||
export LD_LIBRARY_PATH="\$APPDIR/usr/lib:\$LD_LIBRARY_PATH"
|
||||
exec ./void "\$@"
|
||||
EOF
|
||||
then
|
||||
echo "Error creating AppRun"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Make AppRun executable
|
||||
chmod +x "$APP_DIR/AppRun"
|
||||
|
||||
# Download appimagetool if not present in the current directory
|
||||
if [ ! -f "./appimagetool-x86_64.AppImage" ]; then
|
||||
echo "Downloading appimagetool-x86_64.AppImage..."
|
||||
wget "https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage"
|
||||
chmod +x appimagetool-x86_64.AppImage
|
||||
else
|
||||
echo "appimagetool-x86_64.AppImage is already present."
|
||||
fi
|
||||
|
||||
# Create the AppImage
|
||||
echo "Creating AppImage..."
|
||||
ARCH=x86_64 ./appimagetool-x86_64.AppImage "$APP_DIR" "${APP_NAME}-${APP_VERSION}-${ARCH}.AppImage"
|
||||
|
||||
# Cleanup
|
||||
echo "Cleaning up..."
|
||||
rm -rf "$TEMP_DIR"
|
||||
|
||||
echo "AppImage creation complete!"
|
||||
@@ -0,0 +1,74 @@
|
||||
## 0.0.48
|
||||
- Support `%n` in ProxyCommand
|
||||
- fix: add missing direct @types/ssh2-stream dependency (#177)
|
||||
- fix Win32 internal error (#178)
|
||||
|
||||
## 0.0.47
|
||||
- Add support for loong64 (#175)
|
||||
- Add s390x support (#174)
|
||||
- Support vscodium alpine reh (#142)
|
||||
|
||||
## 0.0.46
|
||||
- Add riscv64 support (#147)
|
||||
|
||||
## 0.0.45
|
||||
- Use windows-x64 server on windows-arm64
|
||||
|
||||
## 0.0.44
|
||||
- Update ssh2 lib
|
||||
- Properly set extensionHost env variables
|
||||
|
||||
## 0.0.43
|
||||
- Fix parsing multiple include directives
|
||||
|
||||
## 0.0.42
|
||||
- Fix remote label to show port when connecting to a port other than 22
|
||||
|
||||
## 0.0.41
|
||||
- Take into account parsed port from ssh destination. Fixes (#110)
|
||||
|
||||
## 0.0.40
|
||||
- Update ssh-config package
|
||||
|
||||
## 0.0.39
|
||||
|
||||
- output error messages when downloading vscode server (#39)
|
||||
- Add PreferredAuthentications support (#97)
|
||||
|
||||
## 0.0.38
|
||||
|
||||
- Enable remote support for ppc64le (#93)
|
||||
|
||||
## 0.0.37
|
||||
|
||||
- Default to Current OS User in Connection String if No User Provided (#91)
|
||||
- Add support for (unofficial) DragonFly reh (#86)
|
||||
|
||||
## 0.0.36
|
||||
|
||||
- Make wget support continue download (#85)
|
||||
|
||||
## 0.0.35
|
||||
|
||||
- Fixes hardcoded agentsock for windows breaks pageant compatibility (#81)
|
||||
|
||||
## 0.0.34
|
||||
|
||||
- Add remote.SSH.connectTimeout setting
|
||||
- adding %r username replacement to proxycommand (#77)
|
||||
|
||||
## 0.0.33
|
||||
|
||||
- feat: support %r user substitution in proxycommand
|
||||
|
||||
## 0.0.32
|
||||
|
||||
- feat: use serverDownloadUrlTemplate from product.json (#59)
|
||||
|
||||
## 0.0.31
|
||||
|
||||
- feat: support glob patterns in SSH include directives
|
||||
|
||||
## 0.0.30
|
||||
|
||||
- feat: support file patterns in SSH include directives
|
||||
@@ -0,0 +1,48 @@
|
||||
# Open Remote - SSH
|
||||
|
||||
## SSH Host Requirements
|
||||
|
||||
You can connect to a running SSH server on the following platforms.
|
||||
|
||||
**Supported**:
|
||||
|
||||
- x86_64 Debian 8+, Ubuntu 16.04+, CentOS / RHEL 7+ Linux.
|
||||
- ARMv7l (AArch32) Raspbian Stretch/9+ (32-bit).
|
||||
- ARMv8l (AArch64) Ubuntu 18.04+ (64-bit).
|
||||
- macOS 10.14+ (Mojave)
|
||||
- Windows 10+
|
||||
- FreeBSD 13 (Requires manual remote-extension-host installation)
|
||||
- DragonFlyBSD (Requires manual remote-extension-host installation)
|
||||
|
||||
## Requirements
|
||||
|
||||
**Activation**
|
||||
|
||||
Enable the extension in your `argv.json`
|
||||
|
||||
```json
|
||||
{
|
||||
...
|
||||
"enable-proposed-api": [
|
||||
...,
|
||||
"jeanp413.open-remote-ssh",
|
||||
]
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
which you can open by running the `Preferences: Configure Runtime Arguments` command.
|
||||
The file is located in `~/.vscode-oss/argv.json`.
|
||||
|
||||
**Alpine linux**
|
||||
|
||||
When running on alpine linux, the packages `libstdc++` and `bash` are necessary and can be installed via
|
||||
running
|
||||
|
||||
```bash
|
||||
sudo apk add bash libstdc++
|
||||
```
|
||||
|
||||
## SSH configuration file
|
||||
|
||||
[OpenSSH](https://www.openssh.com/) supports using a [configuration file](https://linuxize.com/post/using-the-ssh-config-file/) to store all your different SSH connections. To use an SSH config file, run the `Remote-SSH: Open SSH Configuration File...` command.
|
||||
@@ -0,0 +1,17 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
//@ts-check
|
||||
|
||||
'use strict';
|
||||
|
||||
const withBrowserDefaults = require('../shared.webpack.config').browser;
|
||||
|
||||
module.exports = withBrowserDefaults({
|
||||
context: __dirname,
|
||||
entry: {
|
||||
extension: './src/extension.ts'
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,34 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
//@ts-check
|
||||
|
||||
'use strict';
|
||||
|
||||
const withDefaults = require('../shared.webpack.config');
|
||||
const { IgnorePlugin } = require('webpack');
|
||||
|
||||
module.exports = withDefaults({
|
||||
context: __dirname,
|
||||
resolve: {
|
||||
mainFields: ['module', 'main']
|
||||
},
|
||||
entry: {
|
||||
extension: './src/extension.ts',
|
||||
},
|
||||
externals: {
|
||||
vscode: "commonjs vscode",
|
||||
bufferutil: "commonjs bufferutil",
|
||||
"utf-8-validate": "commonjs utf-8-validate",
|
||||
},
|
||||
plugins: [
|
||||
new IgnorePlugin({
|
||||
resourceRegExp: /crypto\/build\/Release\/sshcrypto\.node$/,
|
||||
}),
|
||||
new IgnorePlugin({
|
||||
resourceRegExp: /cpu-features/,
|
||||
})
|
||||
]
|
||||
});
|
||||
@@ -0,0 +1,370 @@
|
||||
{
|
||||
"name": "open-remote-ssh",
|
||||
"version": "0.0.48",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "open-remote-ssh",
|
||||
"version": "0.0.48",
|
||||
"dependencies": {
|
||||
"@jeanp413/ssh-config": "^4.3.1",
|
||||
"glob": "^9.3.1",
|
||||
"simple-socks": "git+https://github.com/jeanp413/simple-socks#main",
|
||||
"socks": "^2.5.0",
|
||||
"ssh2": "git+https://github.com/jeanp413/ssh2#master"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/ssh2": "^0.5.52",
|
||||
"@types/ssh2-streams": "0.1.12"
|
||||
},
|
||||
"engines": {
|
||||
"vscode": "^1.70.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/runtime-corejs3": {
|
||||
"version": "7.26.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.26.0.tgz",
|
||||
"integrity": "sha512-YXHu5lN8kJCb1LOb9PgV6pvak43X2h4HvRApcN5SdWeaItQOzfn1hgP6jasD6KWQyJDBxrVmA9o9OivlnNJK/w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"core-js-pure": "^3.30.2",
|
||||
"regenerator-runtime": "^0.14.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@jeanp413/ssh-config": {
|
||||
"version": "4.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@jeanp413/ssh-config/-/ssh-config-4.3.1.tgz",
|
||||
"integrity": "sha512-x0EaWRdjs5sPDNmYr11wVB1GdwWQgRekc7SbueuO5FK7YZUav98qZKtZZU5iSDKyxJkooCs3rgVizB1wIWrF7g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "22.10.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.7.tgz",
|
||||
"integrity": "sha512-V09KvXxFiutGp6B7XkpaDXlNadZxrzajcY50EuoLIpQ6WWYCSvf19lVIazzfIzQvhUN2HjX12spLojTnhuKlGg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~6.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/ssh2": {
|
||||
"version": "0.5.52",
|
||||
"resolved": "https://registry.npmjs.org/@types/ssh2/-/ssh2-0.5.52.tgz",
|
||||
"integrity": "sha512-lbLLlXxdCZOSJMCInKH2+9V/77ET2J6NPQHpFI0kda61Dd1KglJs+fPQBchizmzYSOJBgdTajhPqBO1xxLywvg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node": "*",
|
||||
"@types/ssh2-streams": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/ssh2-streams": {
|
||||
"version": "0.1.12",
|
||||
"resolved": "https://registry.npmjs.org/@types/ssh2-streams/-/ssh2-streams-0.1.12.tgz",
|
||||
"integrity": "sha512-Sy8tpEmCce4Tq0oSOYdfqaBpA3hDM8SoxoFh5vzFsu2oL+znzGz8oVWW7xb4K920yYMUY+PIG31qZnFMfPWNCg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/asn1": {
|
||||
"version": "0.2.6",
|
||||
"resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz",
|
||||
"integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"safer-buffer": "~2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/balanced-match": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/bcrypt-pbkdf": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz",
|
||||
"integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"tweetnacl": "^0.14.3"
|
||||
}
|
||||
},
|
||||
"node_modules/binary": {
|
||||
"version": "0.3.0",
|
||||
"resolved": "https://registry.npmjs.org/binary/-/binary-0.3.0.tgz",
|
||||
"integrity": "sha512-D4H1y5KYwpJgK8wk1Cue5LLPgmwHKYSChkbspQg5JtVuR5ulGckxfR62H3AE9UDkdMC8yyXlqYihuz3Aqg2XZg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"buffers": "~0.1.1",
|
||||
"chainsaw": "~0.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/brace-expansion": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
|
||||
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/buffers": {
|
||||
"version": "0.1.1",
|
||||
"resolved": "https://registry.npmjs.org/buffers/-/buffers-0.1.1.tgz",
|
||||
"integrity": "sha512-9q/rDEGSb/Qsvv2qvzIzdluL5k7AaJOTrw23z9reQthrbF7is4CtlT0DXyO1oei2DCp4uojjzQ7igaSHp1kAEQ==",
|
||||
"engines": {
|
||||
"node": ">=0.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/buildcheck": {
|
||||
"version": "0.0.6",
|
||||
"resolved": "https://registry.npmjs.org/buildcheck/-/buildcheck-0.0.6.tgz",
|
||||
"integrity": "sha512-8f9ZJCUXyT1M35Jx7MkBgmBMo3oHTTBIPLiY9xyL0pl3T5RwcPEY8cUHr5LBNfu/fk6c2T4DJZuVM/8ZZT2D2A==",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/chainsaw": {
|
||||
"version": "0.1.0",
|
||||
"resolved": "https://registry.npmjs.org/chainsaw/-/chainsaw-0.1.0.tgz",
|
||||
"integrity": "sha512-75kWfWt6MEKNC8xYXIdRpDehRYY/tNSgwKaJq+dbbDcxORuVrrQ+SEHoWsniVn9XPYfP4gmdWIeDk/4YNp1rNQ==",
|
||||
"license": "MIT/X11",
|
||||
"dependencies": {
|
||||
"traverse": ">=0.3.0 <0.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/core-js-pure": {
|
||||
"version": "3.40.0",
|
||||
"resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.40.0.tgz",
|
||||
"integrity": "sha512-AtDzVIgRrmRKQai62yuSIN5vNiQjcJakJb4fbhVw3ehxx7Lohphvw9SGNWKhLFqSxC4ilD0g/L1huAYFQU3Q6A==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/core-js"
|
||||
}
|
||||
},
|
||||
"node_modules/cpu-features": {
|
||||
"version": "0.0.10",
|
||||
"resolved": "https://registry.npmjs.org/cpu-features/-/cpu-features-0.0.10.tgz",
|
||||
"integrity": "sha512-9IkYqtX3YHPCzoVg1Py+o9057a3i0fp7S530UWokCSaFVTc7CwXPRiOjRjBQQ18ZCNafx78YfnG+HALxtVmOGA==",
|
||||
"hasInstallScript": true,
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"buildcheck": "~0.0.6",
|
||||
"nan": "^2.19.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/fs.realpath": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
|
||||
"integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/glob": {
|
||||
"version": "9.3.5",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-9.3.5.tgz",
|
||||
"integrity": "sha512-e1LleDykUz2Iu+MTYdkSsuWX8lvAjAcs0Xef0lNIu0S2wOAzuTxCJtcd9S3cijlwYF18EsU3rzb8jPVobxDh9Q==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"fs.realpath": "^1.0.0",
|
||||
"minimatch": "^8.0.2",
|
||||
"minipass": "^4.2.4",
|
||||
"path-scurry": "^1.6.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16 || 14 >=14.17"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/ip-address": {
|
||||
"version": "9.0.5",
|
||||
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz",
|
||||
"integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"jsbn": "1.1.0",
|
||||
"sprintf-js": "^1.1.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 12"
|
||||
}
|
||||
},
|
||||
"node_modules/jsbn": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz",
|
||||
"integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lru-cache": {
|
||||
"version": "10.4.3",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
|
||||
"integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/minimatch": {
|
||||
"version": "8.0.4",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-8.0.4.tgz",
|
||||
"integrity": "sha512-W0Wvr9HyFXZRGIDgCicunpQ299OKXs9RgZfaukz4qAW/pJhcpUfupc9c+OObPOFueNy8VSrZgEmDtk6Kh4WzDA==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16 || 14 >=14.17"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/minipass": {
|
||||
"version": "4.2.8",
|
||||
"resolved": "https://registry.npmjs.org/minipass/-/minipass-4.2.8.tgz",
|
||||
"integrity": "sha512-fNzuVyifolSLFL4NzpF+wEF4qrgqaaKX0haXPQEdQ7NKAN+WecoKMHV09YcuL/DHxrUsYQOK3MiuDf7Ip2OXfQ==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/nan": {
|
||||
"version": "2.22.0",
|
||||
"resolved": "https://registry.npmjs.org/nan/-/nan-2.22.0.tgz",
|
||||
"integrity": "sha512-nbajikzWTMwsW+eSsNm3QwlOs7het9gGJU5dDZzRTQGk03vyBOauxgI4VakDzE0PtsGTmXPsXTbbjVhRwR5mpw==",
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/path-scurry": {
|
||||
"version": "1.11.1",
|
||||
"resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz",
|
||||
"integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==",
|
||||
"license": "BlueOak-1.0.0",
|
||||
"dependencies": {
|
||||
"lru-cache": "^10.2.0",
|
||||
"minipass": "^5.0.0 || ^6.0.2 || ^7.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16 || 14 >=14.18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/path-scurry/node_modules/minipass": {
|
||||
"version": "7.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
|
||||
"integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=16 || 14 >=14.17"
|
||||
}
|
||||
},
|
||||
"node_modules/regenerator-runtime": {
|
||||
"version": "0.14.1",
|
||||
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz",
|
||||
"integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/safer-buffer": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
||||
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/simple-socks": {
|
||||
"version": "2.2.2",
|
||||
"resolved": "git+ssh://git@github.com/jeanp413/simple-socks.git#2ac739301a82d6baff04804ed494436a026acb60",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime-corejs3": "^7.16.8",
|
||||
"binary": "^0.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/smart-buffer": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz",
|
||||
"integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 6.0.0",
|
||||
"npm": ">= 3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/socks": {
|
||||
"version": "2.8.3",
|
||||
"resolved": "https://registry.npmjs.org/socks/-/socks-2.8.3.tgz",
|
||||
"integrity": "sha512-l5x7VUUWbjVFbafGLxPWkYsHIhEvmF85tbIeFZWc8ZPtoMyybuEhL7Jye/ooC4/d48FgOjSJXgsF/AJPYCW8Zw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ip-address": "^9.0.5",
|
||||
"smart-buffer": "^4.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 10.0.0",
|
||||
"npm": ">= 3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/sprintf-js": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz",
|
||||
"integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/ssh2": {
|
||||
"version": "1.14.0",
|
||||
"resolved": "git+ssh://git@github.com/jeanp413/ssh2.git#a169f627213aa663e0aa2fd2f0ef5c8931890c26",
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"asn1": "^0.2.6",
|
||||
"bcrypt-pbkdf": "^1.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.16.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"cpu-features": "~0.0.9",
|
||||
"nan": "^2.17.0"
|
||||
}
|
||||
},
|
||||
"node_modules/traverse": {
|
||||
"version": "0.3.9",
|
||||
"resolved": "https://registry.npmjs.org/traverse/-/traverse-0.3.9.tgz",
|
||||
"integrity": "sha512-iawgk0hLP3SxGKDfnDJf8wTz4p2qImnyihM5Hh/sGvQ3K37dPi/w8sRhdNIxYA1TwFwc5mDhIJq+O0RsvXBKdQ==",
|
||||
"license": "MIT/X11",
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/tweetnacl": {
|
||||
"version": "0.14.5",
|
||||
"resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz",
|
||||
"integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==",
|
||||
"license": "Unlicense"
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "6.20.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz",
|
||||
"integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,351 @@
|
||||
{
|
||||
"name": "open-remote-ssh",
|
||||
"displayName": "Open Remote - SSH",
|
||||
"publisher": "voideditor",
|
||||
"description": "Use any remote machine with a SSH server as your development environment.",
|
||||
"version": "0.0.48",
|
||||
"icon": "resources/icon.png",
|
||||
"engines": {
|
||||
"vscode": "^1.70.2"
|
||||
},
|
||||
"extensionKind": [
|
||||
"ui"
|
||||
],
|
||||
"enabledApiProposals": [
|
||||
"resolvers",
|
||||
"contribViewsRemote"
|
||||
],
|
||||
"keywords": [
|
||||
"remote development",
|
||||
"remote",
|
||||
"ssh"
|
||||
],
|
||||
"api": "none",
|
||||
"activationEvents": [
|
||||
"onCommand:openremotessh.openEmptyWindow",
|
||||
"onCommand:openremotessh.openEmptyWindowInCurrentWindow",
|
||||
"onCommand:openremotessh.openConfigFile",
|
||||
"onCommand:openremotessh.showLog",
|
||||
"onResolveRemoteAuthority:ssh-remote",
|
||||
"onView:sshHosts"
|
||||
],
|
||||
"main": "./out/extension.js",
|
||||
"contributes": {
|
||||
"configuration": {
|
||||
"title": "Remote - SSH",
|
||||
"properties": {
|
||||
"remote.SSH.configFile": {
|
||||
"type": "string",
|
||||
"description": "The absolute file path to a custom SSH config file.",
|
||||
"default": "",
|
||||
"scope": "application"
|
||||
},
|
||||
"remote.SSH.connectTimeout": {
|
||||
"type": "number",
|
||||
"description": "Specifies the timeout in seconds used for the SSH command that connects to the remote.",
|
||||
"default": 60,
|
||||
"scope": "application",
|
||||
"minimum": 1
|
||||
},
|
||||
"remote.SSH.defaultExtensions": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "List of extensions that should be installed automatically on all SSH hosts.",
|
||||
"scope": "application"
|
||||
},
|
||||
"remote.SSH.enableDynamicForwarding": {
|
||||
"type": "boolean",
|
||||
"description": "Whether to use SSH dynamic forwarding to allow setting up new port tunnels over an existing SSH connection.",
|
||||
"scope": "application",
|
||||
"default": true
|
||||
},
|
||||
"remote.SSH.enableAgentForwarding": {
|
||||
"type": "boolean",
|
||||
"markdownDescription": "Enable fixing the remote environment so that the SSH config option `ForwardAgent` will take effect as expected from VS Code's remote extension host.",
|
||||
"scope": "application",
|
||||
"default": true
|
||||
},
|
||||
"remote.SSH.serverDownloadUrlTemplate": {
|
||||
"type": "string",
|
||||
"description": "The URL from where the vscode server will be downloaded. You can use the following variables and they will be replaced dynamically:\n- ${quality}: vscode server quality, e.g. stable or insiders\n- ${version}: vscode server version, e.g. 1.69.0\n- ${commit}: vscode server release commit\n- ${arch}: vscode server arch, e.g. x64, armhf, arm64\n- ${release}: release number",
|
||||
"scope": "application",
|
||||
"default": "https://github.com/voideditor/${NAME_OF_REPO}/releases/download/${version}.${release}/void-server-${os}-${arch}-${version}.${release}.tar.gz"
|
||||
},
|
||||
"remote.SSH.remotePlatform": {
|
||||
"type": "object",
|
||||
"description": "A map of the remote hostname to the platform for that remote. Valid values: linux, macos, windows.",
|
||||
"scope": "application",
|
||||
"default": {},
|
||||
"additionalProperties": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"linux",
|
||||
"macos",
|
||||
"windows"
|
||||
]
|
||||
}
|
||||
},
|
||||
"remote.SSH.remoteServerListenOnSocket": {
|
||||
"type": "boolean",
|
||||
"description": "When true, the remote vscode server will listen on a socket path instead of opening a port. Only valid for Linux and macOS remotes. Requires `AllowStreamLocalForwarding` to be enabled for the SSH server.",
|
||||
"default": false
|
||||
},
|
||||
"remote.SSH.experimental.serverBinaryName": {
|
||||
"type": "string",
|
||||
"description": "**Experimental:** The name of the server binary, use this **only if** you are using a client without a corresponding server release",
|
||||
"scope": "application",
|
||||
"default": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
"views": {
|
||||
"remote": [
|
||||
{
|
||||
"id": "sshHosts",
|
||||
"name": "SSH Targets",
|
||||
"group": "targets@1",
|
||||
"remoteName": "ssh-remote"
|
||||
}
|
||||
]
|
||||
},
|
||||
"commands": [
|
||||
{
|
||||
"command": "openremotessh.openEmptyWindow",
|
||||
"title": "Connect to Host...",
|
||||
"category": "Remote-SSH"
|
||||
},
|
||||
{
|
||||
"command": "openremotessh.openEmptyWindowInCurrentWindow",
|
||||
"title": "Connect Current Window to Host...",
|
||||
"category": "Remote-SSH"
|
||||
},
|
||||
{
|
||||
"command": "openremotessh.openConfigFile",
|
||||
"title": "Open SSH Configuration File...",
|
||||
"category": "Remote-SSH"
|
||||
},
|
||||
{
|
||||
"command": "openremotessh.showLog",
|
||||
"title": "Show Log",
|
||||
"category": "Remote-SSH"
|
||||
},
|
||||
{
|
||||
"command": "openremotessh.explorer.emptyWindowInNewWindow",
|
||||
"title": "Connect to Host in New Window",
|
||||
"icon": "$(empty-window)"
|
||||
},
|
||||
{
|
||||
"command": "openremotessh.explorer.emptyWindowInCurrentWindow",
|
||||
"title": "Connect to Host in Current Window"
|
||||
},
|
||||
{
|
||||
"command": "openremotessh.explorer.reopenFolderInCurrentWindow",
|
||||
"title": "Open on SSH Host in Current Window"
|
||||
},
|
||||
{
|
||||
"command": "openremotessh.explorer.reopenFolderInNewWindow",
|
||||
"title": "Open on SSH Host in New Window",
|
||||
"icon": "$(folder-opened)"
|
||||
},
|
||||
{
|
||||
"command": "openremotessh.explorer.deleteFolderHistoryItem",
|
||||
"title": "Remove From Recent List",
|
||||
"icon": "$(x)"
|
||||
},
|
||||
{
|
||||
"command": "openremotessh.explorer.refresh",
|
||||
"title": "Refresh",
|
||||
"icon": "$(refresh)"
|
||||
},
|
||||
{
|
||||
"command": "openremotessh.explorer.configure",
|
||||
"title": "Configure",
|
||||
"icon": "$(gear)"
|
||||
},
|
||||
{
|
||||
"command": "openremotessh.explorer.add",
|
||||
"title": "Add New",
|
||||
"icon": "$(plus)"
|
||||
}
|
||||
],
|
||||
"resourceLabelFormatters": [
|
||||
{
|
||||
"scheme": "vscode-remote",
|
||||
"authority": "ssh-remote+*",
|
||||
"formatting": {
|
||||
"label": "${path}",
|
||||
"separator": "/",
|
||||
"tildify": true,
|
||||
"workspaceSuffix": "SSH"
|
||||
}
|
||||
}
|
||||
],
|
||||
"menus": {
|
||||
"statusBar/remoteIndicator": [
|
||||
{
|
||||
"command": "openremotessh.openEmptyWindow",
|
||||
"when": "remoteName =~ /^ssh-remote$/ && remoteConnectionState == connected",
|
||||
"group": "remote_20_ssh_1general@1"
|
||||
},
|
||||
{
|
||||
"command": "openremotessh.openEmptyWindowInCurrentWindow",
|
||||
"when": "remoteName =~ /^ssh-remote$/ && remoteConnectionState == connected",
|
||||
"group": "remote_20_ssh_1general@2"
|
||||
},
|
||||
{
|
||||
"command": "openremotessh.openConfigFile",
|
||||
"when": "remoteName =~ /^ssh-remote$/ && remoteConnectionState == connected",
|
||||
"group": "remote_20_ssh_1general@3"
|
||||
},
|
||||
{
|
||||
"command": "openremotessh.showLog",
|
||||
"when": "remoteName =~ /^ssh-remote$/ && remoteConnectionState == connected",
|
||||
"group": "remote_20_ssh_1general@4"
|
||||
},
|
||||
{
|
||||
"command": "openremotessh.openEmptyWindow",
|
||||
"when": "remoteConnectionState == disconnected",
|
||||
"group": "remote_20_ssh_3local@1"
|
||||
},
|
||||
{
|
||||
"command": "openremotessh.openEmptyWindowInCurrentWindow",
|
||||
"when": "remoteConnectionState == disconnected",
|
||||
"group": "remote_20_ssh_3local@2"
|
||||
},
|
||||
{
|
||||
"command": "openremotessh.openConfigFile",
|
||||
"when": "remoteConnectionState == disconnected",
|
||||
"group": "remote_20_ssh_3local@3"
|
||||
},
|
||||
{
|
||||
"command": "openremotessh.openEmptyWindow",
|
||||
"when": "!remoteName && !virtualWorkspace",
|
||||
"group": "remote_20_ssh_3local@5"
|
||||
},
|
||||
{
|
||||
"command": "openremotessh.openEmptyWindowInCurrentWindow",
|
||||
"when": "!remoteName && !virtualWorkspace",
|
||||
"group": "remote_20_ssh_3local@6"
|
||||
},
|
||||
{
|
||||
"command": "openremotessh.openConfigFile",
|
||||
"when": "!remoteName && !virtualWorkspace",
|
||||
"group": "remote_20_ssh_3local@7"
|
||||
}
|
||||
],
|
||||
"commandPalette": [
|
||||
{
|
||||
"command": "openremotessh.explorer.refresh",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "openremotessh.explorer.configure",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "openremotessh.explorer.add",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "openremotessh.explorer.emptyWindowInNewWindow",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "openremotessh.explorer.emptyWindowInCurrentWindow",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "openremotessh.explorer.reopenFolderInCurrentWindow",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "openremotessh.explorer.reopenFolderInNewWindow",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "openremotessh.explorer.deleteFolderHistoryItem",
|
||||
"when": "false"
|
||||
}
|
||||
],
|
||||
"view/title": [
|
||||
{
|
||||
"command": "openremotessh.explorer.add",
|
||||
"when": "view == sshHosts",
|
||||
"group": "navigation"
|
||||
},
|
||||
{
|
||||
"command": "openremotessh.explorer.configure",
|
||||
"when": "view == sshHosts",
|
||||
"group": "navigation"
|
||||
},
|
||||
{
|
||||
"command": "openremotessh.explorer.refresh",
|
||||
"when": "view == sshHosts",
|
||||
"group": "navigation"
|
||||
}
|
||||
],
|
||||
"view/item/context": [
|
||||
{
|
||||
"command": "openremotessh.explorer.emptyWindowInNewWindow",
|
||||
"when": "viewItem =~ /^openremotessh.explorer.host$/",
|
||||
"group": "inline@1"
|
||||
},
|
||||
{
|
||||
"command": "openremotessh.explorer.emptyWindowInNewWindow",
|
||||
"when": "viewItem =~ /^openremotessh.explorer.host$/",
|
||||
"group": "navigation@2"
|
||||
},
|
||||
{
|
||||
"command": "openremotessh.explorer.emptyWindowInCurrentWindow",
|
||||
"when": "viewItem =~ /^openremotessh.explorer.host$/",
|
||||
"group": "navigation@1"
|
||||
},
|
||||
{
|
||||
"command": "openremotessh.explorer.reopenFolderInNewWindow",
|
||||
"when": "viewItem == openremotessh.explorer.folder",
|
||||
"group": "inline@1"
|
||||
},
|
||||
{
|
||||
"command": "openremotessh.explorer.reopenFolderInNewWindow",
|
||||
"when": "viewItem == openremotessh.explorer.folder",
|
||||
"group": "navigation@2"
|
||||
},
|
||||
{
|
||||
"command": "openremotessh.explorer.reopenFolderInCurrentWindow",
|
||||
"when": "viewItem == openremotessh.explorer.folder",
|
||||
"group": "navigation@1"
|
||||
},
|
||||
{
|
||||
"command": "openremotessh.explorer.deleteFolderHistoryItem",
|
||||
"when": "viewItem =~ /^openremotessh.explorer.folder/",
|
||||
"group": "navigation@3"
|
||||
},
|
||||
{
|
||||
"command": "openremotessh.explorer.deleteFolderHistoryItem",
|
||||
"when": "viewItem =~ /^openremotessh.explorer.folder/",
|
||||
"group": "inline@2"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"vscode:prepublish": "npm run compile",
|
||||
"compile": "gulp compile-extension:open-remote-ssh",
|
||||
"compile-web": "npx webpack-cli --config extension-browser.webpack.config --mode none",
|
||||
"watch": "gulp watch-extension:open-remote-ssh",
|
||||
"watch-web": "npx webpack-cli --config extension-browser.webpack.config --mode none --watch --info-verbosity verbose"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/ssh2": "^0.5.52",
|
||||
"@types/ssh2-streams": "0.1.12"
|
||||
},
|
||||
"dependencies": {
|
||||
"glob": "^9.3.1",
|
||||
"simple-socks": "git+https://github.com/jeanp413/simple-socks#main",
|
||||
"socks": "^2.5.0",
|
||||
"@jeanp413/ssh-config": "^4.3.1",
|
||||
"ssh2": "git+https://github.com/jeanp413/ssh2#master"
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 17 KiB |
@@ -0,0 +1,464 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
import * as cp from 'child_process';
|
||||
import * as fs from 'fs';
|
||||
import * as net from 'net';
|
||||
import * as stream from 'stream';
|
||||
import { SocksClient, SocksClientOptions } from 'socks';
|
||||
import * as vscode from 'vscode';
|
||||
import * as ssh2 from 'ssh2';
|
||||
import type { ParsedKey } from 'ssh2-streams';
|
||||
import Log from './common/logger';
|
||||
import SSHDestination from './ssh/sshDestination';
|
||||
import SSHConnection, { SSHTunnelConfig } from './ssh/sshConnection';
|
||||
import SSHConfiguration from './ssh/sshConfig';
|
||||
import { gatherIdentityFiles } from './ssh/identityFiles';
|
||||
import { untildify, exists as fileExists } from './common/files';
|
||||
import { findRandomPort } from './common/ports';
|
||||
import { disposeAll } from './common/disposable';
|
||||
import { installCodeServer, ServerInstallError } from './serverSetup';
|
||||
import { isWindows } from './common/platform';
|
||||
import * as os from 'os';
|
||||
|
||||
const PASSWORD_RETRY_COUNT = 3;
|
||||
const PASSPHRASE_RETRY_COUNT = 3;
|
||||
|
||||
export const REMOTE_SSH_AUTHORITY = 'ssh-remote';
|
||||
|
||||
export function getRemoteAuthority(host: string) {
|
||||
return `${REMOTE_SSH_AUTHORITY}+${host}`;
|
||||
}
|
||||
|
||||
class TunnelInfo implements vscode.Disposable {
|
||||
constructor(
|
||||
readonly localPort: number,
|
||||
readonly remotePortOrSocketPath: number | string,
|
||||
private disposables: vscode.Disposable[]
|
||||
) {
|
||||
}
|
||||
|
||||
dispose() {
|
||||
disposeAll(this.disposables);
|
||||
}
|
||||
}
|
||||
|
||||
interface SSHKey {
|
||||
filename: string;
|
||||
parsedKey: ParsedKey;
|
||||
fingerprint: string;
|
||||
agentSupport?: boolean;
|
||||
isPrivate?: boolean;
|
||||
}
|
||||
|
||||
export class RemoteSSHResolver implements vscode.RemoteAuthorityResolver, vscode.Disposable {
|
||||
|
||||
private proxyConnections: SSHConnection[] = [];
|
||||
private sshConnection: SSHConnection | undefined;
|
||||
private sshAgentSock: string | undefined;
|
||||
private proxyCommandProcess: cp.ChildProcessWithoutNullStreams | undefined;
|
||||
|
||||
private socksTunnel: SSHTunnelConfig | undefined;
|
||||
private tunnels: TunnelInfo[] = [];
|
||||
|
||||
private labelFormatterDisposable: vscode.Disposable | undefined;
|
||||
|
||||
constructor(
|
||||
readonly context: vscode.ExtensionContext,
|
||||
readonly logger: Log
|
||||
) {
|
||||
}
|
||||
|
||||
resolve(authority: string, context: vscode.RemoteAuthorityResolverContext): Thenable<vscode.ResolverResult> {
|
||||
const [type, dest] = authority.split('+');
|
||||
if (type !== REMOTE_SSH_AUTHORITY) {
|
||||
throw new Error(`Invalid authority type for SSH resolver: ${type}`);
|
||||
}
|
||||
|
||||
this.logger.info(`Resolving ssh remote authority '${authority}' (attemp #${context.resolveAttempt})`);
|
||||
|
||||
const sshDest = SSHDestination.parseEncoded(dest);
|
||||
|
||||
// It looks like default values are not loaded yet when resolving a remote,
|
||||
// so let's hardcode the default values here
|
||||
const remoteSSHconfig = vscode.workspace.getConfiguration('remote.SSH');
|
||||
const enableDynamicForwarding = remoteSSHconfig.get<boolean>('enableDynamicForwarding', true)!;
|
||||
const enableAgentForwarding = remoteSSHconfig.get<boolean>('enableAgentForwarding', true)!;
|
||||
const serverDownloadUrlTemplate = remoteSSHconfig.get<string>('serverDownloadUrlTemplate');
|
||||
const defaultExtensions = remoteSSHconfig.get<string[]>('defaultExtensions', []);
|
||||
const remotePlatformMap = remoteSSHconfig.get<Record<string, string>>('remotePlatform', {});
|
||||
const remoteServerListenOnSocket = remoteSSHconfig.get<boolean>('remoteServerListenOnSocket', false)!;
|
||||
const connectTimeout = remoteSSHconfig.get<number>('connectTimeout', 60)!;
|
||||
|
||||
return vscode.window.withProgress({
|
||||
title: `Setting up SSH Host ${sshDest.hostname}`,
|
||||
location: vscode.ProgressLocation.Notification,
|
||||
cancellable: false
|
||||
}, async () => {
|
||||
try {
|
||||
const sshconfig = await SSHConfiguration.loadFromFS();
|
||||
const sshHostConfig = sshconfig.getHostConfiguration(sshDest.hostname);
|
||||
const sshHostName = sshHostConfig['HostName'] ? sshHostConfig['HostName'].replace('%h', sshDest.hostname) : sshDest.hostname;
|
||||
const sshUser = sshHostConfig['User'] || sshDest.user || os.userInfo().username || ''; // https://github.com/openssh/openssh-portable/blob/5ec5504f1d328d5bfa64280cd617c3efec4f78f3/sshconnect.c#L1561-L1562
|
||||
const sshPort = sshHostConfig['Port'] ? parseInt(sshHostConfig['Port'], 10) : (sshDest.port || 22);
|
||||
|
||||
this.sshAgentSock = sshHostConfig['IdentityAgent'] || process.env['SSH_AUTH_SOCK'] || (isWindows ? '\\\\.\\pipe\\openssh-ssh-agent' : undefined);
|
||||
this.sshAgentSock = this.sshAgentSock ? untildify(this.sshAgentSock) : undefined;
|
||||
const agentForward = enableAgentForwarding && (sshHostConfig['ForwardAgent'] || 'no').toLowerCase() === 'yes';
|
||||
const agent = agentForward && this.sshAgentSock ? new ssh2.OpenSSHAgent(this.sshAgentSock) : undefined;
|
||||
|
||||
const preferredAuthentications = sshHostConfig['PreferredAuthentications'] ? sshHostConfig['PreferredAuthentications'].split(',').map(s => s.trim()) : ['publickey', 'password', 'keyboard-interactive'];
|
||||
|
||||
const identityFiles: string[] = (sshHostConfig['IdentityFile'] as unknown as string[]) || [];
|
||||
const identitiesOnly = (sshHostConfig['IdentitiesOnly'] || 'no').toLowerCase() === 'yes';
|
||||
const identityKeys = await gatherIdentityFiles(identityFiles, this.sshAgentSock, identitiesOnly, this.logger);
|
||||
|
||||
// Create proxy jump connections if any
|
||||
let proxyStream: ssh2.ClientChannel | stream.Duplex | undefined;
|
||||
if (sshHostConfig['ProxyJump']) {
|
||||
const proxyJumps = sshHostConfig['ProxyJump'].split(',').filter(i => !!i.trim())
|
||||
.map(i => {
|
||||
const proxy = SSHDestination.parse(i);
|
||||
const proxyHostConfig = sshconfig.getHostConfiguration(proxy.hostname);
|
||||
return [proxy, proxyHostConfig] as [SSHDestination, Record<string, string>];
|
||||
});
|
||||
for (let i = 0; i < proxyJumps.length; i++) {
|
||||
const [proxy, proxyHostConfig] = proxyJumps[i];
|
||||
const proxyHostName = proxyHostConfig['HostName'] || proxy.hostname;
|
||||
const proxyUser = proxyHostConfig['User'] || proxy.user || sshUser;
|
||||
const proxyPort = proxyHostConfig['Port'] ? parseInt(proxyHostConfig['Port'], 10) : (proxy.port || sshPort);
|
||||
|
||||
const proxyAgentForward = enableAgentForwarding && (proxyHostConfig['ForwardAgent'] || 'no').toLowerCase() === 'yes';
|
||||
const proxyAgent = proxyAgentForward && this.sshAgentSock ? new ssh2.OpenSSHAgent(this.sshAgentSock) : undefined;
|
||||
|
||||
const proxyIdentityFiles: string[] = (proxyHostConfig['IdentityFile'] as unknown as string[]) || [];
|
||||
const proxyIdentitiesOnly = (proxyHostConfig['IdentitiesOnly'] || 'no').toLowerCase() === 'yes';
|
||||
const proxyIdentityKeys = await gatherIdentityFiles(proxyIdentityFiles, this.sshAgentSock, proxyIdentitiesOnly, this.logger);
|
||||
|
||||
const proxyAuthHandler = this.getSSHAuthHandler(proxyUser, proxyHostName, proxyIdentityKeys, preferredAuthentications);
|
||||
const proxyConnection = new SSHConnection({
|
||||
host: !proxyStream ? proxyHostName : undefined,
|
||||
port: !proxyStream ? proxyPort : undefined,
|
||||
sock: proxyStream,
|
||||
username: proxyUser,
|
||||
readyTimeout: connectTimeout * 1000,
|
||||
strictVendor: false,
|
||||
agentForward: proxyAgentForward,
|
||||
agent: proxyAgent,
|
||||
authHandler: (arg0, arg1, arg2) => (proxyAuthHandler(arg0, arg1, arg2), undefined)
|
||||
});
|
||||
this.proxyConnections.push(proxyConnection);
|
||||
|
||||
const nextProxyJump = i < proxyJumps.length - 1 ? proxyJumps[i + 1] : undefined;
|
||||
const destIP = nextProxyJump ? (nextProxyJump[1]['HostName'] || nextProxyJump[0].hostname) : sshHostName;
|
||||
const destPort = nextProxyJump ? ((nextProxyJump[1]['Port'] && parseInt(nextProxyJump[1]['Port'], 10)) || nextProxyJump[0].port || 22) : sshPort;
|
||||
proxyStream = await proxyConnection.forwardOut('127.0.0.1', 0, destIP, destPort);
|
||||
}
|
||||
} else if (sshHostConfig['ProxyCommand']) {
|
||||
let proxyArgs = (sshHostConfig['ProxyCommand'] as unknown as string[])
|
||||
.map((arg) => arg.replace('%h', sshHostName).replace('%n', sshDest.hostname).replace('%p', sshPort.toString()).replace('%r', sshUser));
|
||||
let proxyCommand = proxyArgs.shift()!;
|
||||
|
||||
let options = {};
|
||||
if (isWindows && /\.(bat|cmd)$/.test(proxyCommand)) {
|
||||
proxyCommand = `"${proxyCommand}"`;
|
||||
proxyArgs = proxyArgs.map((arg) => arg.includes(' ') ? `"${arg}"` : arg);
|
||||
options = { shell: true, windowsHide: true, windowsVerbatimArguments: true };
|
||||
}
|
||||
|
||||
this.logger.trace(`Spawning ProxyCommand: ${proxyCommand} ${proxyArgs.join(' ')}`);
|
||||
|
||||
const child = cp.spawn(proxyCommand, proxyArgs, options);
|
||||
proxyStream = stream.Duplex.from({ readable: child.stdout, writable: child.stdin });
|
||||
this.proxyCommandProcess = child;
|
||||
}
|
||||
|
||||
// Create final shh connection
|
||||
const sshAuthHandler = this.getSSHAuthHandler(sshUser, sshHostName, identityKeys, preferredAuthentications);
|
||||
|
||||
this.sshConnection = new SSHConnection({
|
||||
host: !proxyStream ? sshHostName : undefined,
|
||||
port: !proxyStream ? sshPort : undefined,
|
||||
sock: proxyStream,
|
||||
username: sshUser,
|
||||
readyTimeout: connectTimeout * 1000,
|
||||
strictVendor: false,
|
||||
agentForward,
|
||||
agent,
|
||||
authHandler: (arg0, arg1, arg2) => (sshAuthHandler(arg0, arg1, arg2), undefined),
|
||||
});
|
||||
await this.sshConnection.connect();
|
||||
|
||||
const envVariables: Record<string, string | null> = {};
|
||||
if (agentForward) {
|
||||
envVariables['SSH_AUTH_SOCK'] = null;
|
||||
}
|
||||
|
||||
const installResult = await installCodeServer(this.sshConnection, serverDownloadUrlTemplate, defaultExtensions, Object.keys(envVariables), remotePlatformMap[sshDest.hostname], remoteServerListenOnSocket, this.logger);
|
||||
|
||||
for (const key of Object.keys(envVariables)) {
|
||||
if (installResult[key] !== undefined) {
|
||||
envVariables[key] = installResult[key];
|
||||
}
|
||||
}
|
||||
|
||||
// Update terminal env variables
|
||||
this.context.environmentVariableCollection.persistent = false;
|
||||
for (const [key, value] of Object.entries(envVariables)) {
|
||||
if (value) {
|
||||
this.context.environmentVariableCollection.replace(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
if (enableDynamicForwarding) {
|
||||
const socksPort = await findRandomPort();
|
||||
this.socksTunnel = await this.sshConnection!.addTunnel({
|
||||
name: `ssh_tunnel_socks_${socksPort}`,
|
||||
localPort: socksPort,
|
||||
socks: true
|
||||
});
|
||||
}
|
||||
|
||||
const tunnelConfig = await this.openTunnel(0, installResult.listeningOn);
|
||||
this.tunnels.push(tunnelConfig);
|
||||
|
||||
// Enable ports view
|
||||
vscode.commands.executeCommand('setContext', 'forwardedPortsViewEnabled', true);
|
||||
|
||||
this.labelFormatterDisposable?.dispose();
|
||||
this.labelFormatterDisposable = vscode.workspace.registerResourceLabelFormatter({
|
||||
scheme: 'vscode-remote',
|
||||
authority: `${REMOTE_SSH_AUTHORITY}+*`,
|
||||
formatting: {
|
||||
label: '${path}',
|
||||
separator: '/',
|
||||
tildify: true,
|
||||
workspaceSuffix: `SSH: ${sshDest.hostname}` + (sshDest.port && sshDest.port !== 22 ? `:${sshDest.port}` : '')
|
||||
}
|
||||
});
|
||||
|
||||
const resolvedResult: vscode.ResolverResult = new vscode.ResolvedAuthority('127.0.0.1', tunnelConfig.localPort, installResult.connectionToken);
|
||||
resolvedResult.extensionHostEnv = envVariables;
|
||||
return resolvedResult;
|
||||
} catch (e: unknown) {
|
||||
this.logger.error(`Error resolving authority`, e);
|
||||
|
||||
// Initial connection
|
||||
if (context.resolveAttempt === 1) {
|
||||
this.logger.show();
|
||||
|
||||
const closeRemote = 'Close Remote';
|
||||
const retry = 'Retry';
|
||||
const result = await vscode.window.showErrorMessage(`Could not establish connection to "${sshDest.hostname}"`, { modal: true }, closeRemote, retry);
|
||||
if (result === closeRemote) {
|
||||
await vscode.commands.executeCommand('workbench.action.remote.close');
|
||||
} else if (result === retry) {
|
||||
await vscode.commands.executeCommand('workbench.action.reloadWindow');
|
||||
}
|
||||
}
|
||||
|
||||
if (e instanceof ServerInstallError || !(e instanceof Error)) {
|
||||
throw vscode.RemoteAuthorityResolverError.NotAvailable(e instanceof Error ? e.message : String(e));
|
||||
} else {
|
||||
throw vscode.RemoteAuthorityResolverError.TemporarilyNotAvailable(e.message);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private async openTunnel(localPort: number, remotePortOrSocketPath: number | string) {
|
||||
localPort = localPort > 0 ? localPort : await findRandomPort();
|
||||
|
||||
const disposables: vscode.Disposable[] = [];
|
||||
const remotePort = typeof remotePortOrSocketPath === 'number' ? remotePortOrSocketPath : undefined;
|
||||
const remoteSocketPath = typeof remotePortOrSocketPath === 'string' ? remotePortOrSocketPath : undefined;
|
||||
if (this.socksTunnel && remotePort) {
|
||||
const forwardingServer = await new Promise<net.Server>((resolve, reject) => {
|
||||
this.logger.trace(`Creating forwarding server ${localPort}(local) => ${this.socksTunnel!.localPort!}(socks) => ${remotePort}(remote)`);
|
||||
const socksOptions: SocksClientOptions = {
|
||||
proxy: {
|
||||
host: '127.0.0.1',
|
||||
port: this.socksTunnel!.localPort!,
|
||||
type: 5
|
||||
},
|
||||
command: 'connect',
|
||||
destination: {
|
||||
host: '127.0.0.1',
|
||||
port: remotePort
|
||||
}
|
||||
};
|
||||
const server: net.Server = net.createServer()
|
||||
.on('error', reject)
|
||||
.on('connection', async (socket: net.Socket) => {
|
||||
try {
|
||||
const socksConn = await SocksClient.createConnection(socksOptions);
|
||||
socket.pipe(socksConn.socket);
|
||||
socksConn.socket.pipe(socket);
|
||||
} catch (error) {
|
||||
this.logger.error(`Error while creating SOCKS connection`, error);
|
||||
}
|
||||
})
|
||||
.on('listening', () => resolve(server))
|
||||
.listen(localPort);
|
||||
});
|
||||
disposables.push({
|
||||
dispose: () => forwardingServer.close(() => {
|
||||
this.logger.trace(`SOCKS forwading server closed`);
|
||||
}),
|
||||
});
|
||||
} else {
|
||||
this.logger.trace(`Opening tunnel ${localPort}(local) => ${remotePortOrSocketPath}(remote)`);
|
||||
const tunnelConfig = await this.sshConnection!.addTunnel({
|
||||
name: `ssh_tunnel_${localPort}_${remotePortOrSocketPath}`,
|
||||
remoteAddr: '127.0.0.1',
|
||||
remotePort,
|
||||
remoteSocketPath,
|
||||
localPort
|
||||
});
|
||||
disposables.push({
|
||||
dispose: () => {
|
||||
this.sshConnection?.closeTunnel(tunnelConfig.name);
|
||||
this.logger.trace(`Tunnel ${tunnelConfig.name} closed`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return new TunnelInfo(localPort, remotePortOrSocketPath, disposables);
|
||||
}
|
||||
|
||||
private getSSHAuthHandler(sshUser: string, sshHostName: string, identityKeys: SSHKey[], preferredAuthentications: string[]) {
|
||||
let passwordRetryCount = PASSWORD_RETRY_COUNT;
|
||||
let keyboardRetryCount = PASSWORD_RETRY_COUNT;
|
||||
identityKeys = identityKeys.slice();
|
||||
return async (methodsLeft: string[] | null, _partialSuccess: boolean | null, callback: (nextAuth: ssh2.AuthHandlerResult) => void) => {
|
||||
if (methodsLeft === null) {
|
||||
this.logger.info(`Trying no-auth authentication`);
|
||||
|
||||
return callback({
|
||||
type: 'none',
|
||||
username: sshUser,
|
||||
});
|
||||
}
|
||||
if (methodsLeft.includes('publickey') && identityKeys.length && preferredAuthentications.includes('publickey')) {
|
||||
const identityKey = identityKeys.shift()!;
|
||||
|
||||
this.logger.info(`Trying publickey authentication: ${identityKey.filename} ${identityKey.parsedKey.type} SHA256:${identityKey.fingerprint}`);
|
||||
|
||||
if (identityKey.agentSupport) {
|
||||
return callback({
|
||||
type: 'agent',
|
||||
username: sshUser,
|
||||
agent: new class extends ssh2.OpenSSHAgent {
|
||||
// Only return the current key
|
||||
override getIdentities(callback: (err: Error | undefined, publicKeys?: ParsedKey[]) => void): void {
|
||||
callback(undefined, [identityKey.parsedKey]);
|
||||
}
|
||||
}(this.sshAgentSock!)
|
||||
});
|
||||
}
|
||||
if (identityKey.isPrivate) {
|
||||
return callback({
|
||||
type: 'publickey',
|
||||
username: sshUser,
|
||||
key: identityKey.parsedKey
|
||||
});
|
||||
}
|
||||
if (!await fileExists(identityKey.filename)) {
|
||||
// Try next identity file
|
||||
return callback(null as any);
|
||||
}
|
||||
|
||||
const keyBuffer = await fs.promises.readFile(identityKey.filename);
|
||||
let result = ssh2.utils.parseKey(keyBuffer); // First try without passphrase
|
||||
if (result instanceof Error && result.message === 'Encrypted private OpenSSH key detected, but no passphrase given') {
|
||||
let passphraseRetryCount = PASSPHRASE_RETRY_COUNT;
|
||||
while (result instanceof Error && passphraseRetryCount > 0) {
|
||||
const passphrase = await vscode.window.showInputBox({
|
||||
title: `Enter passphrase for ${identityKey.filename}`,
|
||||
password: true,
|
||||
ignoreFocusOut: true
|
||||
});
|
||||
if (!passphrase) {
|
||||
break;
|
||||
}
|
||||
result = ssh2.utils.parseKey(keyBuffer, passphrase);
|
||||
passphraseRetryCount--;
|
||||
}
|
||||
}
|
||||
if (!result || result instanceof Error) {
|
||||
// Try next identity file
|
||||
return callback(null as any);
|
||||
}
|
||||
|
||||
const key = Array.isArray(result) ? result[0] : result;
|
||||
return callback({
|
||||
type: 'publickey',
|
||||
username: sshUser,
|
||||
key
|
||||
});
|
||||
}
|
||||
if (methodsLeft.includes('password') && passwordRetryCount > 0 && preferredAuthentications.includes('password')) {
|
||||
if (passwordRetryCount === PASSWORD_RETRY_COUNT) {
|
||||
this.logger.info(`Trying password authentication`);
|
||||
}
|
||||
|
||||
const password = await vscode.window.showInputBox({
|
||||
title: `Enter password for ${sshUser}@${sshHostName}`,
|
||||
password: true,
|
||||
ignoreFocusOut: true
|
||||
});
|
||||
passwordRetryCount--;
|
||||
|
||||
return callback(password
|
||||
? {
|
||||
type: 'password',
|
||||
username: sshUser,
|
||||
password
|
||||
}
|
||||
: false);
|
||||
}
|
||||
if (methodsLeft.includes('keyboard-interactive') && keyboardRetryCount > 0 && preferredAuthentications.includes('keyboard-interactive')) {
|
||||
if (keyboardRetryCount === PASSWORD_RETRY_COUNT) {
|
||||
this.logger.info(`Trying keyboard-interactive authentication`);
|
||||
}
|
||||
|
||||
return callback({
|
||||
type: 'keyboard-interactive',
|
||||
username: sshUser,
|
||||
prompt: async (_name, _instructions, _instructionsLang, prompts, finish) => {
|
||||
const responses: string[] = [];
|
||||
for (const prompt of prompts) {
|
||||
const response = await vscode.window.showInputBox({
|
||||
title: `(${sshUser}@${sshHostName}) ${prompt.prompt}`,
|
||||
password: !prompt.echo,
|
||||
ignoreFocusOut: true
|
||||
});
|
||||
if (response === undefined) {
|
||||
keyboardRetryCount = 0;
|
||||
break;
|
||||
}
|
||||
responses.push(response);
|
||||
}
|
||||
keyboardRetryCount--;
|
||||
finish(responses);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
callback(false);
|
||||
};
|
||||
}
|
||||
|
||||
dispose() {
|
||||
disposeAll(this.tunnels);
|
||||
// If there's proxy connections then just close the parent connection
|
||||
if (this.proxyConnections.length) {
|
||||
this.proxyConnections[0].close();
|
||||
} else {
|
||||
this.sshConnection?.close();
|
||||
}
|
||||
this.proxyCommandProcess?.kill();
|
||||
this.labelFormatterDisposable?.dispose();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
import * as vscode from 'vscode';
|
||||
import * as fs from 'fs';
|
||||
import { getRemoteAuthority } from './authResolver';
|
||||
import { getSSHConfigPath } from './ssh/sshConfig';
|
||||
import { exists as fileExists } from './common/files';
|
||||
import SSHDestination from './ssh/sshDestination';
|
||||
|
||||
export async function promptOpenRemoteSSHWindow(reuseWindow: boolean) {
|
||||
const host = await vscode.window.showInputBox({
|
||||
title: 'Enter [user@]hostname[:port]'
|
||||
});
|
||||
|
||||
if (!host) {
|
||||
return;
|
||||
}
|
||||
|
||||
const sshDest = new SSHDestination(host);
|
||||
openRemoteSSHWindow(sshDest.toEncodedString(), reuseWindow);
|
||||
}
|
||||
|
||||
export function openRemoteSSHWindow(host: string, reuseWindow: boolean) {
|
||||
vscode.commands.executeCommand('vscode.newWindow', { remoteAuthority: getRemoteAuthority(host), reuseWindow });
|
||||
}
|
||||
|
||||
export function openRemoteSSHLocationWindow(host: string, path: string, reuseWindow: boolean) {
|
||||
vscode.commands.executeCommand('vscode.openFolder', vscode.Uri.from({ scheme: 'vscode-remote', authority: getRemoteAuthority(host), path }), { forceNewWindow: !reuseWindow });
|
||||
}
|
||||
|
||||
export async function addNewHost() {
|
||||
const sshConfigPath = getSSHConfigPath();
|
||||
if (!await fileExists(sshConfigPath)) {
|
||||
await fs.promises.appendFile(sshConfigPath, '');
|
||||
}
|
||||
|
||||
await vscode.commands.executeCommand('vscode.open', vscode.Uri.file(sshConfigPath), { preview: false });
|
||||
|
||||
const textEditor = vscode.window.activeTextEditor;
|
||||
if (textEditor?.document.uri.fsPath !== sshConfigPath) {
|
||||
return;
|
||||
}
|
||||
|
||||
const textDocument = textEditor.document;
|
||||
const lastLine = textDocument.lineAt(textDocument.lineCount - 1);
|
||||
|
||||
if (!lastLine.isEmptyOrWhitespace) {
|
||||
await textEditor.edit((editBuilder: vscode.TextEditorEdit) => {
|
||||
editBuilder.insert(lastLine.range.end, '\n');
|
||||
});
|
||||
}
|
||||
|
||||
const snippet = '\nHost ${1:dev}\n\tHostName ${2:dev.example.com}\n\tUser ${3:john}';
|
||||
await textEditor.insertSnippet(
|
||||
new vscode.SnippetString(snippet),
|
||||
new vscode.Position(textDocument.lineCount, 0)
|
||||
);
|
||||
}
|
||||
|
||||
export async function openSSHConfigFile() {
|
||||
const sshConfigPath = getSSHConfigPath();
|
||||
if (!await fileExists(sshConfigPath)) {
|
||||
await fs.promises.appendFile(sshConfigPath, '');
|
||||
}
|
||||
vscode.commands.executeCommand('vscode.open', vscode.Uri.file(sshConfigPath));
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
import * as vscode from 'vscode';
|
||||
|
||||
export function disposeAll(disposables: vscode.Disposable[]): void {
|
||||
while (disposables.length) {
|
||||
const item = disposables.pop();
|
||||
if (item) {
|
||||
item.dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export abstract class Disposable {
|
||||
private _isDisposed = false;
|
||||
|
||||
protected _disposables: vscode.Disposable[] = [];
|
||||
|
||||
public dispose(): any {
|
||||
if (this._isDisposed) {
|
||||
return;
|
||||
}
|
||||
this._isDisposed = true;
|
||||
disposeAll(this._disposables);
|
||||
}
|
||||
|
||||
protected _register<T extends vscode.Disposable>(value: T): T {
|
||||
if (this._isDisposed) {
|
||||
value.dispose();
|
||||
} else {
|
||||
this._disposables.push(value);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
protected get isDisposed(): boolean {
|
||||
return this._isDisposed;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
import * as fs from 'fs';
|
||||
import * as os from 'os';
|
||||
|
||||
const homeDir = os.homedir();
|
||||
|
||||
export async function exists(path: string) {
|
||||
try {
|
||||
await fs.promises.access(path);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function untildify(path: string) {
|
||||
return path.replace(/^~(?=$|\/|\\)/, homeDir);
|
||||
}
|
||||
|
||||
export function normalizeToSlash(path: string) {
|
||||
return path.replace(/\\/g, '/');
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
import * as vscode from 'vscode';
|
||||
|
||||
type LogLevel = 'Trace' | 'Info' | 'Error';
|
||||
|
||||
export default class Log {
|
||||
private output: vscode.OutputChannel;
|
||||
|
||||
constructor(name: string) {
|
||||
this.output = vscode.window.createOutputChannel(name);
|
||||
}
|
||||
|
||||
private data2String(data: any): string {
|
||||
if (data instanceof Error) {
|
||||
return data.stack || data.message;
|
||||
}
|
||||
if (data.success === false && data.message) {
|
||||
return data.message;
|
||||
}
|
||||
return data.toString();
|
||||
}
|
||||
|
||||
public trace(message: string, data?: any): void {
|
||||
this.logLevel('Trace', message, data);
|
||||
}
|
||||
|
||||
public info(message: string, data?: any): void {
|
||||
this.logLevel('Info', message, data);
|
||||
}
|
||||
|
||||
public error(message: string, data?: any): void {
|
||||
this.logLevel('Error', message, data);
|
||||
}
|
||||
|
||||
public logLevel(level: LogLevel, message: string, data?: any): void {
|
||||
this.output.appendLine(`[${level} - ${this.now()}] ${message}`);
|
||||
if (data) {
|
||||
this.output.appendLine(this.data2String(data));
|
||||
}
|
||||
}
|
||||
|
||||
private now(): string {
|
||||
const now = new Date();
|
||||
return padLeft(now.getUTCHours() + '', 2, '0')
|
||||
+ ':' + padLeft(now.getMinutes() + '', 2, '0')
|
||||
+ ':' + padLeft(now.getUTCSeconds() + '', 2, '0') + '.' + now.getMilliseconds();
|
||||
}
|
||||
|
||||
public show() {
|
||||
this.output.show();
|
||||
}
|
||||
|
||||
public dispose() {
|
||||
this.output.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
function padLeft(s: string, n: number, pad = ' ') {
|
||||
return pad.repeat(Math.max(0, n - s.length)) + s;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
export const isWindows = process.platform === 'win32';
|
||||
export const isMacintosh = process.platform === 'darwin';
|
||||
export const isLinux = process.platform === 'linux';
|
||||
@@ -0,0 +1,133 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
import * as net from 'net';
|
||||
|
||||
/**
|
||||
* Finds a random unused port assigned by the operating system. Will reject in case no free port can be found.
|
||||
*/
|
||||
export function findRandomPort(): Promise<number> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const server = net.createServer({ pauseOnConnect: true });
|
||||
server.on('error', reject);
|
||||
server.on('listening', () => {
|
||||
const port = (server.address() as net.AddressInfo).port;
|
||||
server.close(() => resolve(port));
|
||||
});
|
||||
server.listen(0, '127.0.0.1');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a start point and a max number of retries, will find a port that
|
||||
* is openable. Will return 0 in case no free port can be found.
|
||||
*/
|
||||
export function findFreePort(startPort: number, giveUpAfter: number, timeout: number, stride = 1): Promise<number> {
|
||||
let done = false;
|
||||
|
||||
return new Promise(resolve => {
|
||||
const timeoutHandle = setTimeout(() => {
|
||||
if (!done) {
|
||||
done = true;
|
||||
return resolve(0);
|
||||
}
|
||||
}, timeout);
|
||||
|
||||
doFindFreePort(startPort, giveUpAfter, stride, (port) => {
|
||||
if (!done) {
|
||||
done = true;
|
||||
clearTimeout(timeoutHandle);
|
||||
return resolve(port);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function doFindFreePort(startPort: number, giveUpAfter: number, stride: number, clb: (port: number) => void): void {
|
||||
if (giveUpAfter === 0) {
|
||||
return clb(0);
|
||||
}
|
||||
|
||||
const client = new net.Socket();
|
||||
|
||||
// If we can connect to the port it means the port is already taken so we continue searching
|
||||
client.once('connect', () => {
|
||||
dispose(client);
|
||||
|
||||
return doFindFreePort(startPort + stride, giveUpAfter - 1, stride, clb);
|
||||
});
|
||||
|
||||
client.once('data', () => {
|
||||
// this listener is required since node.js 8.x
|
||||
});
|
||||
|
||||
client.once('error', (err: Error & { code?: string }) => {
|
||||
dispose(client);
|
||||
|
||||
// If we receive any non ECONNREFUSED error, it means the port is used but we cannot connect
|
||||
if (err.code !== 'ECONNREFUSED') {
|
||||
return doFindFreePort(startPort + stride, giveUpAfter - 1, stride, clb);
|
||||
}
|
||||
|
||||
// Otherwise it means the port is free to use!
|
||||
return clb(startPort);
|
||||
});
|
||||
|
||||
client.connect(startPort, '127.0.0.1');
|
||||
}
|
||||
|
||||
/**
|
||||
* Uses listen instead of connect. Is faster, but if there is another listener on 0.0.0.0 then this will take 127.0.0.1 from that listener.
|
||||
*/
|
||||
export function findFreePortFaster(startPort: number, giveUpAfter: number, timeout: number): Promise<number> {
|
||||
let resolved = false;
|
||||
let timeoutHandle: NodeJS.Timeout | undefined = undefined;
|
||||
let countTried = 1;
|
||||
const server = net.createServer({ pauseOnConnect: true });
|
||||
function doResolve(port: number, resolve: (port: number) => void) {
|
||||
if (!resolved) {
|
||||
resolved = true;
|
||||
server.removeAllListeners();
|
||||
server.close();
|
||||
if (timeoutHandle) {
|
||||
clearTimeout(timeoutHandle);
|
||||
}
|
||||
resolve(port);
|
||||
}
|
||||
}
|
||||
return new Promise<number>(resolve => {
|
||||
timeoutHandle = setTimeout(() => {
|
||||
doResolve(0, resolve);
|
||||
}, timeout);
|
||||
|
||||
server.on('listening', () => {
|
||||
doResolve(startPort, resolve);
|
||||
});
|
||||
server.on('error', err => {
|
||||
if (err && ((<any>err).code === 'EADDRINUSE' || (<any>err).code === 'EACCES') && (countTried < giveUpAfter)) {
|
||||
startPort++;
|
||||
countTried++;
|
||||
server.listen(startPort, '127.0.0.1');
|
||||
} else {
|
||||
doResolve(0, resolve);
|
||||
}
|
||||
});
|
||||
server.on('close', () => {
|
||||
doResolve(0, resolve);
|
||||
});
|
||||
server.listen(startPort, '127.0.0.1');
|
||||
});
|
||||
}
|
||||
|
||||
function dispose(socket: net.Socket): void {
|
||||
try {
|
||||
socket.removeAllListeners('connect');
|
||||
socket.removeAllListeners('error');
|
||||
socket.end();
|
||||
socket.destroy();
|
||||
socket.unref();
|
||||
} catch (error) {
|
||||
console.error(error); // otherwise this error would get lost in the callback chain
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
import * as vscode from 'vscode';
|
||||
import Log from './common/logger';
|
||||
import { RemoteSSHResolver, REMOTE_SSH_AUTHORITY } from './authResolver';
|
||||
import { openSSHConfigFile, promptOpenRemoteSSHWindow } from './commands';
|
||||
import { HostTreeDataProvider } from './hostTreeView';
|
||||
import { getRemoteWorkspaceLocationData, RemoteLocationHistory } from './remoteLocationHistory';
|
||||
|
||||
export async function activate(context: vscode.ExtensionContext) {
|
||||
const logger = new Log('Remote - SSH');
|
||||
context.subscriptions.push(logger);
|
||||
|
||||
const remoteSSHResolver = new RemoteSSHResolver(context, logger);
|
||||
context.subscriptions.push(vscode.workspace.registerRemoteAuthorityResolver(REMOTE_SSH_AUTHORITY, remoteSSHResolver));
|
||||
context.subscriptions.push(remoteSSHResolver);
|
||||
|
||||
const locationHistory = new RemoteLocationHistory(context);
|
||||
const locationData = getRemoteWorkspaceLocationData();
|
||||
if (locationData) {
|
||||
await locationHistory.addLocation(locationData[0], locationData[1]);
|
||||
}
|
||||
|
||||
const hostTreeDataProvider = new HostTreeDataProvider(locationHistory);
|
||||
context.subscriptions.push(vscode.window.createTreeView('sshHosts', { treeDataProvider: hostTreeDataProvider }));
|
||||
context.subscriptions.push(hostTreeDataProvider);
|
||||
|
||||
context.subscriptions.push(vscode.commands.registerCommand('openremotessh.openEmptyWindow', () => promptOpenRemoteSSHWindow(false)));
|
||||
context.subscriptions.push(vscode.commands.registerCommand('openremotessh.openEmptyWindowInCurrentWindow', () => promptOpenRemoteSSHWindow(true)));
|
||||
context.subscriptions.push(vscode.commands.registerCommand('openremotessh.openConfigFile', () => openSSHConfigFile()));
|
||||
context.subscriptions.push(vscode.commands.registerCommand('openremotessh.showLog', () => logger.show()));
|
||||
}
|
||||
|
||||
export function deactivate() {
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
import * as vscode from 'vscode';
|
||||
import * as path from 'path';
|
||||
import SSHConfiguration, { getSSHConfigPath } from './ssh/sshConfig';
|
||||
import { RemoteLocationHistory } from './remoteLocationHistory';
|
||||
import { Disposable } from './common/disposable';
|
||||
import { addNewHost, openRemoteSSHLocationWindow, openRemoteSSHWindow, openSSHConfigFile } from './commands';
|
||||
import SSHDestination from './ssh/sshDestination';
|
||||
|
||||
class HostItem {
|
||||
constructor(
|
||||
public hostname: string,
|
||||
public locations: string[]
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
||||
class HostLocationItem {
|
||||
constructor(
|
||||
public path: string,
|
||||
public hostname: string
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
||||
type DataTreeItem = HostItem | HostLocationItem;
|
||||
|
||||
export class HostTreeDataProvider extends Disposable implements vscode.TreeDataProvider<DataTreeItem> {
|
||||
|
||||
private readonly _onDidChangeTreeData = this._register(new vscode.EventEmitter<DataTreeItem | DataTreeItem[] | void>());
|
||||
public readonly onDidChangeTreeData = this._onDidChangeTreeData.event;
|
||||
|
||||
constructor(
|
||||
private locationHistory: RemoteLocationHistory
|
||||
) {
|
||||
super();
|
||||
|
||||
this._register(vscode.commands.registerCommand('openremotessh.explorer.add', () => addNewHost()));
|
||||
this._register(vscode.commands.registerCommand('openremotessh.explorer.configure', () => openSSHConfigFile()));
|
||||
this._register(vscode.commands.registerCommand('openremotessh.explorer.refresh', () => this.refresh()));
|
||||
this._register(vscode.commands.registerCommand('openremotessh.explorer.emptyWindowInNewWindow', e => this.openRemoteSSHWindow(e, false)));
|
||||
this._register(vscode.commands.registerCommand('openremotessh.explorer.emptyWindowInCurrentWindow', e => this.openRemoteSSHWindow(e, true)));
|
||||
this._register(vscode.commands.registerCommand('openremotessh.explorer.reopenFolderInNewWindow', e => this.openRemoteSSHLocationWindow(e, false)));
|
||||
this._register(vscode.commands.registerCommand('openremotessh.explorer.reopenFolderInCurrentWindow', e => this.openRemoteSSHLocationWindow(e, true)));
|
||||
this._register(vscode.commands.registerCommand('openremotessh.explorer.deleteFolderHistoryItem', e => this.deleteHostLocation(e)));
|
||||
|
||||
this._register(vscode.workspace.onDidChangeConfiguration(e => {
|
||||
if (e.affectsConfiguration('remote.SSH.configFile')) {
|
||||
this.refresh();
|
||||
}
|
||||
}));
|
||||
this._register(vscode.workspace.onDidSaveTextDocument(e => {
|
||||
if (e.uri.fsPath === getSSHConfigPath()) {
|
||||
this.refresh();
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
getTreeItem(element: DataTreeItem): vscode.TreeItem {
|
||||
if (element instanceof HostLocationItem) {
|
||||
const label = path.posix.basename(element.path).replace(/\.code-workspace$/, ' (Workspace)');
|
||||
const treeItem = new vscode.TreeItem(label);
|
||||
treeItem.description = path.posix.dirname(element.path);
|
||||
treeItem.iconPath = new vscode.ThemeIcon('folder');
|
||||
treeItem.contextValue = 'openremotessh.explorer.folder';
|
||||
return treeItem;
|
||||
}
|
||||
|
||||
const treeItem = new vscode.TreeItem(element.hostname);
|
||||
treeItem.collapsibleState = element.locations.length ? vscode.TreeItemCollapsibleState.Collapsed : vscode.TreeItemCollapsibleState.None;
|
||||
treeItem.iconPath = new vscode.ThemeIcon('vm');
|
||||
treeItem.contextValue = 'openremotessh.explorer.host';
|
||||
return treeItem;
|
||||
}
|
||||
|
||||
async getChildren(element?: HostItem): Promise<DataTreeItem[]> {
|
||||
if (!element) {
|
||||
const sshConfigFile = await SSHConfiguration.loadFromFS();
|
||||
const hosts = sshConfigFile.getAllConfiguredHosts();
|
||||
return hosts.map(hostname => new HostItem(hostname, this.locationHistory.getHistory(hostname)));
|
||||
}
|
||||
if (element instanceof HostItem) {
|
||||
return element.locations.map(location => new HostLocationItem(location, element.hostname));
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
private refresh() {
|
||||
this._onDidChangeTreeData.fire();
|
||||
}
|
||||
|
||||
private async deleteHostLocation(element: HostLocationItem) {
|
||||
await this.locationHistory.removeLocation(element.hostname, element.path);
|
||||
this.refresh();
|
||||
}
|
||||
|
||||
private async openRemoteSSHWindow(element: HostItem, reuseWindow: boolean) {
|
||||
const sshDest = new SSHDestination(element.hostname);
|
||||
openRemoteSSHWindow(sshDest.toEncodedString(), reuseWindow);
|
||||
}
|
||||
|
||||
private async openRemoteSSHLocationWindow(element: HostLocationItem, reuseWindow: boolean) {
|
||||
const sshDest = new SSHDestination(element.hostname);
|
||||
openRemoteSSHLocationWindow(sshDest.toEncodedString(), element.path, reuseWindow);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
import * as vscode from 'vscode';
|
||||
import { REMOTE_SSH_AUTHORITY } from './authResolver';
|
||||
import SSHDestination from './ssh/sshDestination';
|
||||
|
||||
export class RemoteLocationHistory {
|
||||
private static STORAGE_KEY = 'remoteLocationHistory_v0';
|
||||
|
||||
private remoteLocationHistory: Record<string, string[]> = {};
|
||||
|
||||
constructor(private context: vscode.ExtensionContext) {
|
||||
// context.globalState.update(RemoteLocationHistory.STORAGE_KEY, undefined);
|
||||
this.remoteLocationHistory = context.globalState.get(RemoteLocationHistory.STORAGE_KEY) || {};
|
||||
}
|
||||
|
||||
getHistory(host: string): string[] {
|
||||
return this.remoteLocationHistory[host] || [];
|
||||
}
|
||||
|
||||
async addLocation(host: string, path: string) {
|
||||
const hostLocations = this.remoteLocationHistory[host] || [];
|
||||
if (!hostLocations.includes(path)) {
|
||||
hostLocations.unshift(path);
|
||||
this.remoteLocationHistory[host] = hostLocations;
|
||||
|
||||
await this.context.globalState.update(RemoteLocationHistory.STORAGE_KEY, this.remoteLocationHistory);
|
||||
}
|
||||
}
|
||||
|
||||
async removeLocation(host: string, path: string) {
|
||||
let hostLocations = this.remoteLocationHistory[host] || [];
|
||||
hostLocations = hostLocations.filter(l => l !== path);
|
||||
this.remoteLocationHistory[host] = hostLocations;
|
||||
|
||||
await this.context.globalState.update(RemoteLocationHistory.STORAGE_KEY, this.remoteLocationHistory);
|
||||
}
|
||||
}
|
||||
|
||||
export function getRemoteWorkspaceLocationData(): [string, string] | undefined {
|
||||
let location = vscode.workspace.workspaceFile;
|
||||
if (location && location.scheme === 'vscode-remote' && location.authority.startsWith(REMOTE_SSH_AUTHORITY) && location.path.endsWith('.code-workspace')) {
|
||||
const [, host] = location.authority.split('+');
|
||||
const sshDest = SSHDestination.parseEncoded(host);
|
||||
return [sshDest.hostname, location.path];
|
||||
}
|
||||
|
||||
location = vscode.workspace.workspaceFolders?.[0].uri;
|
||||
if (location && location.scheme === 'vscode-remote' && location.authority.startsWith(REMOTE_SSH_AUTHORITY)) {
|
||||
const [, host] = location.authority.split('+');
|
||||
const sshDest = SSHDestination.parseEncoded(host);
|
||||
return [sshDest.hostname, location.path];
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
import * as vscode from 'vscode';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
let vscodeProductJson: any;
|
||||
async function getVSCodeProductJson() {
|
||||
if (!vscodeProductJson) {
|
||||
const productJsonStr = await fs.promises.readFile(path.join(vscode.env.appRoot, 'product.json'), 'utf8');
|
||||
vscodeProductJson = JSON.parse(productJsonStr);
|
||||
}
|
||||
|
||||
return vscodeProductJson;
|
||||
}
|
||||
|
||||
export interface IServerConfig {
|
||||
version: string;
|
||||
commit: string;
|
||||
quality: string;
|
||||
release?: string; // void-like specific
|
||||
serverApplicationName: string;
|
||||
serverDataFolderName: string;
|
||||
serverDownloadUrlTemplate?: string; // void-like specific
|
||||
}
|
||||
|
||||
export async function getVSCodeServerConfig(): Promise<IServerConfig> {
|
||||
const productJson = await getVSCodeProductJson();
|
||||
|
||||
const customServerBinaryName = vscode.workspace.getConfiguration('remote.SSH.experimental').get<string>('serverBinaryName', '');
|
||||
|
||||
return {
|
||||
version: vscode.version.replace('-insider', ''),
|
||||
commit: productJson.commit,
|
||||
quality: productJson.quality,
|
||||
release: productJson.release,
|
||||
serverApplicationName: customServerBinaryName || productJson.serverApplicationName,
|
||||
serverDataFolderName: productJson.serverDataFolderName,
|
||||
serverDownloadUrlTemplate: productJson.serverDownloadUrlTemplate
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,626 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
import * as crypto from 'crypto';
|
||||
import Log from './common/logger';
|
||||
import { getVSCodeServerConfig } from './serverConfig';
|
||||
import SSHConnection from './ssh/sshConnection';
|
||||
|
||||
export interface ServerInstallOptions {
|
||||
id: string;
|
||||
quality: string;
|
||||
commit: string;
|
||||
version: string;
|
||||
release?: string; // void specific
|
||||
extensionIds: string[];
|
||||
envVariables: string[];
|
||||
useSocketPath: boolean;
|
||||
serverApplicationName: string;
|
||||
serverDataFolderName: string;
|
||||
serverDownloadUrlTemplate: string;
|
||||
}
|
||||
|
||||
export interface ServerInstallResult {
|
||||
exitCode: number;
|
||||
listeningOn: number | string;
|
||||
connectionToken: string;
|
||||
logFile: string;
|
||||
osReleaseId: string;
|
||||
arch: string;
|
||||
platform: string;
|
||||
tmpDir: string;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export class ServerInstallError extends Error {
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
|
||||
const DEFAULT_DOWNLOAD_URL_TEMPLATE = 'https://github.com/voideditor/void-updates-server/releases/download/test/void-server-${os}-${arch}.tar.gz';
|
||||
|
||||
export async function installCodeServer(conn: SSHConnection, serverDownloadUrlTemplate: string | undefined, extensionIds: string[], envVariables: string[], platform: string | undefined, useSocketPath: boolean, logger: Log): Promise<ServerInstallResult> {
|
||||
let shell = 'powershell';
|
||||
|
||||
// detect platform and shell for windows
|
||||
if (!platform || platform === 'windows') {
|
||||
const result = await conn.exec('uname -s');
|
||||
|
||||
if (result.stdout) {
|
||||
if (result.stdout.includes('windows32')) {
|
||||
platform = 'windows';
|
||||
} else if (result.stdout.includes('MINGW64')) {
|
||||
platform = 'windows';
|
||||
shell = 'bash';
|
||||
}
|
||||
} else if (result.stderr) {
|
||||
if (result.stderr.includes('FullyQualifiedErrorId : CommandNotFoundException')) {
|
||||
platform = 'windows';
|
||||
}
|
||||
|
||||
if (result.stderr.includes('is not recognized as an internal or external command')) {
|
||||
platform = 'windows';
|
||||
shell = 'cmd';
|
||||
}
|
||||
}
|
||||
|
||||
if (platform) {
|
||||
logger.trace(`Detected platform: ${platform}, ${shell}`);
|
||||
}
|
||||
}
|
||||
|
||||
const scriptId = crypto.randomBytes(12).toString('hex');
|
||||
|
||||
const vscodeServerConfig = await getVSCodeServerConfig();
|
||||
const installOptions: ServerInstallOptions = {
|
||||
id: scriptId,
|
||||
version: vscodeServerConfig.version,
|
||||
commit: vscodeServerConfig.commit,
|
||||
quality: vscodeServerConfig.quality,
|
||||
release: vscodeServerConfig.release,
|
||||
extensionIds,
|
||||
envVariables,
|
||||
useSocketPath,
|
||||
serverApplicationName: vscodeServerConfig.serverApplicationName,
|
||||
serverDataFolderName: vscodeServerConfig.serverDataFolderName,
|
||||
serverDownloadUrlTemplate: serverDownloadUrlTemplate ?? vscodeServerConfig.serverDownloadUrlTemplate ?? DEFAULT_DOWNLOAD_URL_TEMPLATE,
|
||||
};
|
||||
|
||||
let commandOutput: { stdout: string; stderr: string };
|
||||
if (platform === 'windows') {
|
||||
const installServerScript = generatePowerShellInstallScript(installOptions);
|
||||
|
||||
logger.trace('Server install command:', installServerScript);
|
||||
|
||||
const installDir = `$HOME\\${vscodeServerConfig.serverDataFolderName}\\install`;
|
||||
const installScript = `${installDir}\\${vscodeServerConfig.commit}.ps1`;
|
||||
const endRegex = new RegExp(`${scriptId}: end`);
|
||||
// investigate if it's possible to use `-EncodedCommand` flag
|
||||
// https://devblogs.microsoft.com/powershell/invoking-powershell-with-complex-expressions-using-scriptblocks/
|
||||
let command = '';
|
||||
if (shell === 'powershell') {
|
||||
command = `md -Force ${installDir}; echo @'\n${installServerScript}\n'@ | Set-Content ${installScript}; powershell -ExecutionPolicy ByPass -File "${installScript}"`;
|
||||
} else if (shell === 'bash') {
|
||||
command = `mkdir -p ${installDir.replace(/\\/g, '/')} && echo '\n${installServerScript.replace(/'/g, '\'"\'"\'')}\n' > ${installScript.replace(/\\/g, '/')} && powershell -ExecutionPolicy ByPass -File "${installScript}"`;
|
||||
} else if (shell === 'cmd') {
|
||||
const script = installServerScript.trim()
|
||||
// remove comments
|
||||
.replace(/^#.*$/gm, '')
|
||||
// remove empty lines
|
||||
.replace(/\n{2,}/gm, '\n')
|
||||
// remove leading spaces
|
||||
.replace(/^\s*/gm, '')
|
||||
// escape double quotes (from powershell/cmd)
|
||||
.replace(/"/g, '"""')
|
||||
// escape single quotes (from cmd)
|
||||
.replace(/'/g, `''`)
|
||||
// escape redirect (from cmd)
|
||||
.replace(/>/g, `^>`)
|
||||
// escape new lines (from powershell/cmd)
|
||||
.replace(/\n/g, '\'`n\'');
|
||||
|
||||
command = `powershell "md -Force ${installDir}" && powershell "echo '${script}'" > ${installScript.replace('$HOME', '%USERPROFILE%')} && powershell -ExecutionPolicy ByPass -File "${installScript.replace('$HOME', '%USERPROFILE%')}"`;
|
||||
|
||||
logger.trace('Command length (8191 max):', command.length);
|
||||
|
||||
if (command.length > 8191) {
|
||||
throw new ServerInstallError(`Command line too long`);
|
||||
}
|
||||
} else {
|
||||
throw new ServerInstallError(`Not supported shell: ${shell}`);
|
||||
}
|
||||
|
||||
commandOutput = await conn.execPartial(command, (stdout: string) => endRegex.test(stdout));
|
||||
} else {
|
||||
const installServerScript = generateBashInstallScript(installOptions);
|
||||
|
||||
logger.trace('Server install command:', installServerScript);
|
||||
// Fish shell does not support heredoc so let's workaround it using -c option,
|
||||
// also replace single quotes (') within the script with ('\'') as there's no quoting within single quotes, see https://unix.stackexchange.com/a/24676
|
||||
commandOutput = await conn.exec(`bash -c '${installServerScript.replace(/'/g, `'\\''`)}'`);
|
||||
}
|
||||
|
||||
if (commandOutput.stderr) {
|
||||
logger.trace('Server install command stderr:', commandOutput.stderr);
|
||||
}
|
||||
logger.trace('Server install command stdout:', commandOutput.stdout);
|
||||
|
||||
const resultMap = parseServerInstallOutput(commandOutput.stdout, scriptId);
|
||||
if (!resultMap) {
|
||||
throw new ServerInstallError(`Failed parsing install script output`);
|
||||
}
|
||||
|
||||
const exitCode = parseInt(resultMap.exitCode, 10);
|
||||
if (exitCode !== 0) {
|
||||
throw new ServerInstallError(`Couldn't install vscode server on remote server, install script returned non-zero exit status`);
|
||||
}
|
||||
|
||||
const listeningOn = resultMap.listeningOn.match(/^\d+$/)
|
||||
? parseInt(resultMap.listeningOn, 10)
|
||||
: resultMap.listeningOn;
|
||||
|
||||
const remoteEnvVars = Object.fromEntries(Object.entries(resultMap).filter(([key,]) => envVariables.includes(key)));
|
||||
|
||||
return {
|
||||
exitCode,
|
||||
listeningOn,
|
||||
connectionToken: resultMap.connectionToken,
|
||||
logFile: resultMap.logFile,
|
||||
osReleaseId: resultMap.osReleaseId,
|
||||
arch: resultMap.arch,
|
||||
platform: resultMap.platform,
|
||||
tmpDir: resultMap.tmpDir,
|
||||
...remoteEnvVars
|
||||
};
|
||||
}
|
||||
|
||||
function parseServerInstallOutput(str: string, scriptId: string): { [k: string]: string } | undefined {
|
||||
const startResultStr = `${scriptId}: start`;
|
||||
const endResultStr = `${scriptId}: end`;
|
||||
|
||||
const startResultIdx = str.indexOf(startResultStr);
|
||||
if (startResultIdx < 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const endResultIdx = str.indexOf(endResultStr, startResultIdx + startResultStr.length);
|
||||
if (endResultIdx < 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const installResult = str.substring(startResultIdx + startResultStr.length, endResultIdx);
|
||||
|
||||
const resultMap: { [k: string]: string } = {};
|
||||
const resultArr = installResult.split(/\r?\n/);
|
||||
for (const line of resultArr) {
|
||||
const [key, value] = line.split('==');
|
||||
resultMap[key] = value;
|
||||
}
|
||||
|
||||
return resultMap;
|
||||
}
|
||||
|
||||
function generateBashInstallScript({ id, quality, version, commit, release, extensionIds, envVariables, useSocketPath, serverApplicationName, serverDataFolderName, serverDownloadUrlTemplate }: ServerInstallOptions) {
|
||||
const extensions = extensionIds.map(id => '--install-extension ' + id).join(' ');
|
||||
return `
|
||||
# Server installation script
|
||||
|
||||
TMP_DIR="\${XDG_RUNTIME_DIR:-"/tmp"}"
|
||||
|
||||
DISTRO_VERSION="${version}"
|
||||
DISTRO_COMMIT="${commit}"
|
||||
DISTRO_QUALITY="${quality}"
|
||||
DISTRO_VOID_RELEASE="${release ?? ''}"
|
||||
|
||||
SERVER_APP_NAME="${serverApplicationName}"
|
||||
SERVER_INITIAL_EXTENSIONS="${extensions}"
|
||||
SERVER_LISTEN_FLAG="${useSocketPath ? `--socket-path="$TMP_DIR/vscode-server-sock-${crypto.randomUUID()}"` : '--port=0'}"
|
||||
SERVER_DATA_DIR="$HOME/${serverDataFolderName}"
|
||||
SERVER_DIR="$SERVER_DATA_DIR/bin/$DISTRO_COMMIT"
|
||||
SERVER_SCRIPT="$SERVER_DIR/bin/$SERVER_APP_NAME"
|
||||
SERVER_LOGFILE="$SERVER_DATA_DIR/.$DISTRO_COMMIT.log"
|
||||
SERVER_PIDFILE="$SERVER_DATA_DIR/.$DISTRO_COMMIT.pid"
|
||||
SERVER_TOKENFILE="$SERVER_DATA_DIR/.$DISTRO_COMMIT.token"
|
||||
SERVER_ARCH=
|
||||
SERVER_CONNECTION_TOKEN=
|
||||
SERVER_DOWNLOAD_URL=
|
||||
|
||||
LISTENING_ON=
|
||||
OS_RELEASE_ID=
|
||||
ARCH=
|
||||
PLATFORM=
|
||||
|
||||
# Mimic output from logs of remote-ssh extension
|
||||
print_install_results_and_exit() {
|
||||
echo "${id}: start"
|
||||
echo "exitCode==$1=="
|
||||
echo "listeningOn==$LISTENING_ON=="
|
||||
echo "connectionToken==$SERVER_CONNECTION_TOKEN=="
|
||||
echo "logFile==$SERVER_LOGFILE=="
|
||||
echo "osReleaseId==$OS_RELEASE_ID=="
|
||||
echo "arch==$ARCH=="
|
||||
echo "platform==$PLATFORM=="
|
||||
echo "tmpDir==$TMP_DIR=="
|
||||
${envVariables.map(envVar => `echo "${envVar}==$${envVar}=="`).join('\n')}
|
||||
echo "${id}: end"
|
||||
exit 0
|
||||
}
|
||||
|
||||
# Check if platform is supported
|
||||
KERNEL="$(uname -s)"
|
||||
case $KERNEL in
|
||||
Darwin)
|
||||
PLATFORM="darwin"
|
||||
;;
|
||||
Linux)
|
||||
PLATFORM="linux"
|
||||
;;
|
||||
FreeBSD)
|
||||
PLATFORM="freebsd"
|
||||
;;
|
||||
DragonFly)
|
||||
PLATFORM="dragonfly"
|
||||
;;
|
||||
*)
|
||||
echo "Error platform not supported: $KERNEL"
|
||||
print_install_results_and_exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
# Check machine architecture
|
||||
ARCH="$(uname -m)"
|
||||
case $ARCH in
|
||||
x86_64 | amd64)
|
||||
SERVER_ARCH="x64"
|
||||
;;
|
||||
armv7l | armv8l)
|
||||
SERVER_ARCH="armhf"
|
||||
;;
|
||||
arm64 | aarch64)
|
||||
SERVER_ARCH="arm64"
|
||||
;;
|
||||
ppc64le)
|
||||
SERVER_ARCH="ppc64le"
|
||||
;;
|
||||
riscv64)
|
||||
SERVER_ARCH="riscv64"
|
||||
;;
|
||||
loongarch64)
|
||||
SERVER_ARCH="loong64"
|
||||
;;
|
||||
s390x)
|
||||
SERVER_ARCH="s390x"
|
||||
;;
|
||||
*)
|
||||
echo "Error architecture not supported: $ARCH"
|
||||
print_install_results_and_exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
# https://www.freedesktop.org/software/systemd/man/os-release.html
|
||||
OS_RELEASE_ID="$(grep -i '^ID=' /etc/os-release 2>/dev/null | sed 's/^ID=//gi' | sed 's/"//g')"
|
||||
if [[ -z $OS_RELEASE_ID ]]; then
|
||||
OS_RELEASE_ID="$(grep -i '^ID=' /usr/lib/os-release 2>/dev/null | sed 's/^ID=//gi' | sed 's/"//g')"
|
||||
if [[ -z $OS_RELEASE_ID ]]; then
|
||||
OS_RELEASE_ID="unknown"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Create installation folder
|
||||
if [[ ! -d $SERVER_DIR ]]; then
|
||||
mkdir -p $SERVER_DIR
|
||||
if (( $? > 0 )); then
|
||||
echo "Error creating server install directory"
|
||||
print_install_results_and_exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# adjust platform for void download, if needed
|
||||
if [[ $OS_RELEASE_ID = alpine ]]; then
|
||||
PLATFORM=$OS_RELEASE_ID
|
||||
fi
|
||||
|
||||
SERVER_DOWNLOAD_URL="$(echo "${serverDownloadUrlTemplate.replace(/\$\{/g, '\\${')}" | sed "s/\\\${quality}/$DISTRO_QUALITY/g" | sed "s/\\\${version}/$DISTRO_VERSION/g" | sed "s/\\\${commit}/$DISTRO_COMMIT/g" | sed "s/\\\${os}/$PLATFORM/g" | sed "s/\\\${arch}/$SERVER_ARCH/g" | sed "s/\\\${release}/$DISTRO_VOID_RELEASE/g")"
|
||||
|
||||
# Check if server script is already installed
|
||||
if [[ ! -f $SERVER_SCRIPT ]]; then
|
||||
case "$PLATFORM" in
|
||||
darwin | linux | alpine )
|
||||
;;
|
||||
*)
|
||||
echo "Error '$PLATFORM' needs manual installation of remote extension host"
|
||||
print_install_results_and_exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
pushd $SERVER_DIR > /dev/null
|
||||
|
||||
if [[ ! -z $(which wget) ]]; then
|
||||
wget --tries=3 --timeout=10 --continue --no-verbose -O vscode-server.tar.gz $SERVER_DOWNLOAD_URL
|
||||
elif [[ ! -z $(which curl) ]]; then
|
||||
curl --retry 3 --connect-timeout 10 --location --show-error --silent --output vscode-server.tar.gz $SERVER_DOWNLOAD_URL
|
||||
else
|
||||
echo "Error no tool to download server binary"
|
||||
print_install_results_and_exit 1
|
||||
fi
|
||||
|
||||
if (( $? > 0 )); then
|
||||
echo "Error downloading server from $SERVER_DOWNLOAD_URL"
|
||||
print_install_results_and_exit 1
|
||||
fi
|
||||
|
||||
tar -xf vscode-server.tar.gz --strip-components 1
|
||||
if (( $? > 0 )); then
|
||||
echo "Error while extracting server contents"
|
||||
print_install_results_and_exit 1
|
||||
fi
|
||||
|
||||
if [[ ! -f $SERVER_SCRIPT ]]; then
|
||||
echo "Error server contents are corrupted"
|
||||
print_install_results_and_exit 1
|
||||
fi
|
||||
|
||||
rm -f vscode-server.tar.gz
|
||||
|
||||
popd > /dev/null
|
||||
else
|
||||
echo "Server script already installed in $SERVER_SCRIPT"
|
||||
fi
|
||||
|
||||
# Try to find if server is already running
|
||||
if [[ -f $SERVER_PIDFILE ]]; then
|
||||
SERVER_PID="$(cat $SERVER_PIDFILE)"
|
||||
SERVER_RUNNING_PROCESS="$(ps -o pid,args -p $SERVER_PID | grep $SERVER_SCRIPT)"
|
||||
else
|
||||
SERVER_RUNNING_PROCESS="$(ps -o pid,args -A | grep $SERVER_SCRIPT | grep -v grep)"
|
||||
fi
|
||||
|
||||
if [[ -z $SERVER_RUNNING_PROCESS ]]; then
|
||||
if [[ -f $SERVER_LOGFILE ]]; then
|
||||
rm $SERVER_LOGFILE
|
||||
fi
|
||||
if [[ -f $SERVER_TOKENFILE ]]; then
|
||||
rm $SERVER_TOKENFILE
|
||||
fi
|
||||
|
||||
touch $SERVER_TOKENFILE
|
||||
chmod 600 $SERVER_TOKENFILE
|
||||
SERVER_CONNECTION_TOKEN="${crypto.randomUUID()}"
|
||||
echo $SERVER_CONNECTION_TOKEN > $SERVER_TOKENFILE
|
||||
|
||||
$SERVER_SCRIPT --start-server --host=127.0.0.1 $SERVER_LISTEN_FLAG $SERVER_INITIAL_EXTENSIONS --connection-token-file $SERVER_TOKENFILE --telemetry-level off --enable-remote-auto-shutdown --accept-server-license-terms &> $SERVER_LOGFILE &
|
||||
echo $! > $SERVER_PIDFILE
|
||||
else
|
||||
echo "Server script is already running $SERVER_SCRIPT"
|
||||
fi
|
||||
|
||||
if [[ -f $SERVER_TOKENFILE ]]; then
|
||||
SERVER_CONNECTION_TOKEN="$(cat $SERVER_TOKENFILE)"
|
||||
else
|
||||
echo "Error server token file not found $SERVER_TOKENFILE"
|
||||
print_install_results_and_exit 1
|
||||
fi
|
||||
|
||||
if [[ -f $SERVER_LOGFILE ]]; then
|
||||
for i in {1..5}; do
|
||||
LISTENING_ON="$(cat $SERVER_LOGFILE | grep -E 'Extension host agent listening on .+' | sed 's/Extension host agent listening on //')"
|
||||
if [[ -n $LISTENING_ON ]]; then
|
||||
break
|
||||
fi
|
||||
sleep 0.5
|
||||
done
|
||||
|
||||
if [[ -z $LISTENING_ON ]]; then
|
||||
echo "Error server did not start successfully"
|
||||
print_install_results_and_exit 1
|
||||
fi
|
||||
else
|
||||
echo "Error server log file not found $SERVER_LOGFILE"
|
||||
print_install_results_and_exit 1
|
||||
fi
|
||||
|
||||
# Finish server setup
|
||||
print_install_results_and_exit 0
|
||||
`;
|
||||
}
|
||||
|
||||
function generatePowerShellInstallScript({ id, quality, version, commit, release, extensionIds, envVariables, useSocketPath, serverApplicationName, serverDataFolderName, serverDownloadUrlTemplate }: ServerInstallOptions) {
|
||||
const extensions = extensionIds.map(id => '--install-extension ' + id).join(' ');
|
||||
const downloadUrl = serverDownloadUrlTemplate
|
||||
.replace(/\$\{quality\}/g, quality)
|
||||
.replace(/\$\{version\}/g, version)
|
||||
.replace(/\$\{commit\}/g, commit)
|
||||
.replace(/\$\{os\}/g, 'win32')
|
||||
.replace(/\$\{arch\}/g, 'x64')
|
||||
.replace(/\$\{release\}/g, release ?? '');
|
||||
|
||||
return `
|
||||
# Server installation script
|
||||
|
||||
$TMP_DIR="$env:TEMP\\$([System.IO.Path]::GetRandomFileName())"
|
||||
$ProgressPreference = "SilentlyContinue"
|
||||
|
||||
$DISTRO_VERSION="${version}"
|
||||
$DISTRO_COMMIT="${commit}"
|
||||
$DISTRO_QUALITY="${quality}"
|
||||
$DISTRO_VOID_RELEASE="${release ?? ''}"
|
||||
|
||||
$SERVER_APP_NAME="${serverApplicationName}"
|
||||
$SERVER_INITIAL_EXTENSIONS="${extensions}"
|
||||
$SERVER_LISTEN_FLAG="${useSocketPath ? `--socket-path="$TMP_DIR/vscode-server-sock-${crypto.randomUUID()}"` : '--port=0'}"
|
||||
$SERVER_DATA_DIR="$(Resolve-Path ~)\\${serverDataFolderName}"
|
||||
$SERVER_DIR="$SERVER_DATA_DIR\\bin\\$DISTRO_COMMIT"
|
||||
$SERVER_SCRIPT="$SERVER_DIR\\bin\\$SERVER_APP_NAME.cmd"
|
||||
$SERVER_LOGFILE="$SERVER_DATA_DIR\\.$DISTRO_COMMIT.log"
|
||||
$SERVER_PIDFILE="$SERVER_DATA_DIR\\.$DISTRO_COMMIT.pid"
|
||||
$SERVER_TOKENFILE="$SERVER_DATA_DIR\\.$DISTRO_COMMIT.token"
|
||||
$SERVER_ARCH=
|
||||
$SERVER_CONNECTION_TOKEN=
|
||||
$SERVER_DOWNLOAD_URL=
|
||||
|
||||
$LISTENING_ON=
|
||||
$OS_RELEASE_ID=
|
||||
$ARCH=
|
||||
$PLATFORM="win32"
|
||||
|
||||
function printInstallResults($code) {
|
||||
"${id}: start"
|
||||
"exitCode==$code=="
|
||||
"listeningOn==$LISTENING_ON=="
|
||||
"connectionToken==$SERVER_CONNECTION_TOKEN=="
|
||||
"logFile==$SERVER_LOGFILE=="
|
||||
"osReleaseId==$OS_RELEASE_ID=="
|
||||
"arch==$ARCH=="
|
||||
"platform==$PLATFORM=="
|
||||
"tmpDir==$TMP_DIR=="
|
||||
${envVariables.map(envVar => `"${envVar}==$${envVar}=="`).join('\n')}
|
||||
"${id}: end"
|
||||
}
|
||||
|
||||
# Check machine architecture
|
||||
$ARCH=$env:PROCESSOR_ARCHITECTURE
|
||||
# Use x64 version for ARM64, as it's not yet available.
|
||||
if(($ARCH -eq "AMD64") -or ($ARCH -eq "IA64") -or ($ARCH -eq "ARM64")) {
|
||||
$SERVER_ARCH="x64"
|
||||
}
|
||||
else {
|
||||
"Error architecture not supported: $ARCH"
|
||||
printInstallResults 1
|
||||
exit 0
|
||||
}
|
||||
|
||||
# Create installation folder
|
||||
if(!(Test-Path $SERVER_DIR)) {
|
||||
try {
|
||||
ni -it d $SERVER_DIR -f -ea si
|
||||
} catch {
|
||||
"Error creating server install directory - $($_.ToString())"
|
||||
exit 1
|
||||
}
|
||||
|
||||
if(!(Test-Path $SERVER_DIR)) {
|
||||
"Error creating server install directory"
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
|
||||
cd $SERVER_DIR
|
||||
|
||||
# Check if server script is already installed
|
||||
if(!(Test-Path $SERVER_SCRIPT)) {
|
||||
del vscode-server.tar.gz
|
||||
|
||||
$REQUEST_ARGUMENTS = @{
|
||||
Uri="${downloadUrl}"
|
||||
TimeoutSec=20
|
||||
OutFile="vscode-server.tar.gz"
|
||||
UseBasicParsing=$True
|
||||
}
|
||||
|
||||
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
|
||||
|
||||
Invoke-RestMethod @REQUEST_ARGUMENTS
|
||||
|
||||
if(Test-Path "vscode-server.tar.gz") {
|
||||
tar -xf vscode-server.tar.gz --strip-components 1
|
||||
|
||||
del vscode-server.tar.gz
|
||||
}
|
||||
|
||||
if(!(Test-Path $SERVER_SCRIPT)) {
|
||||
"Error while installing the server binary"
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
else {
|
||||
"Server script already installed in $SERVER_SCRIPT"
|
||||
}
|
||||
|
||||
# Try to find if server is already running
|
||||
if(Get-Process node -ErrorAction SilentlyContinue | Where-Object Path -Like "$SERVER_DIR\\*") {
|
||||
echo "Server script is already running $SERVER_SCRIPT"
|
||||
}
|
||||
else {
|
||||
if(Test-Path $SERVER_LOGFILE) {
|
||||
del $SERVER_LOGFILE
|
||||
}
|
||||
if(Test-Path $SERVER_PIDFILE) {
|
||||
del $SERVER_PIDFILE
|
||||
}
|
||||
if(Test-Path $SERVER_TOKENFILE) {
|
||||
del $SERVER_TOKENFILE
|
||||
}
|
||||
|
||||
$SERVER_CONNECTION_TOKEN="${crypto.randomUUID()}"
|
||||
[System.IO.File]::WriteAllLines($SERVER_TOKENFILE, $SERVER_CONNECTION_TOKEN)
|
||||
|
||||
$SCRIPT_ARGUMENTS="--start-server --host=127.0.0.1 $SERVER_LISTEN_FLAG $SERVER_INITIAL_EXTENSIONS --connection-token-file $SERVER_TOKENFILE --telemetry-level off --enable-remote-auto-shutdown --accept-server-license-terms *> '$SERVER_LOGFILE'"
|
||||
|
||||
$START_ARGUMENTS = @{
|
||||
FilePath = "powershell.exe"
|
||||
WindowStyle = "hidden"
|
||||
ArgumentList = @(
|
||||
"-ExecutionPolicy", "Unrestricted", "-NoLogo", "-NoProfile", "-NonInteractive", "-c", "$SERVER_SCRIPT $SCRIPT_ARGUMENTS"
|
||||
)
|
||||
PassThru = $True
|
||||
}
|
||||
|
||||
$SERVER_ID = (start @START_ARGUMENTS).ID
|
||||
|
||||
if($SERVER_ID) {
|
||||
[System.IO.File]::WriteAllLines($SERVER_PIDFILE, $SERVER_ID)
|
||||
}
|
||||
}
|
||||
|
||||
if(Test-Path $SERVER_TOKENFILE) {
|
||||
$SERVER_CONNECTION_TOKEN="$(cat $SERVER_TOKENFILE)"
|
||||
}
|
||||
else {
|
||||
"Error server token file not found $SERVER_TOKENFILE"
|
||||
printInstallResults 1
|
||||
exit 0
|
||||
}
|
||||
|
||||
sleep -Milliseconds 500
|
||||
|
||||
$SELECT_ARGUMENTS = @{
|
||||
Path = $SERVER_LOGFILE
|
||||
Pattern = "Extension host agent listening on (\\d+)"
|
||||
}
|
||||
|
||||
for($I = 1; $I -le 5; $I++) {
|
||||
if(Test-Path $SERVER_LOGFILE) {
|
||||
$GROUPS = (Select-String @SELECT_ARGUMENTS).Matches.Groups
|
||||
|
||||
if($GROUPS) {
|
||||
$LISTENING_ON = $GROUPS[1].Value
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
sleep -Milliseconds 500
|
||||
}
|
||||
|
||||
if(!(Test-Path $SERVER_LOGFILE)) {
|
||||
"Error server log file not found $SERVER_LOGFILE"
|
||||
printInstallResults 1
|
||||
exit 0
|
||||
}
|
||||
|
||||
# Finish server setup
|
||||
printInstallResults 0
|
||||
|
||||
if($SERVER_ID) {
|
||||
while($True) {
|
||||
if(!(gps -Id $SERVER_ID)) {
|
||||
"server died, exit"
|
||||
exit 0
|
||||
}
|
||||
|
||||
sleep 30
|
||||
}
|
||||
}
|
||||
`;
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
import * as os from 'os';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import * as crypto from 'crypto';
|
||||
import { exists as folderExists } from '../common/files';
|
||||
|
||||
const PATH_SSH_USER_DIR = path.join(os.homedir(), '.ssh');
|
||||
const KNOW_HOST_FILE = path.join(PATH_SSH_USER_DIR, 'known_hosts');
|
||||
const HASH_MAGIC = '|1|';
|
||||
const HASH_DELIM = '|';
|
||||
|
||||
export async function checkNewHostInHostkeys(host: string): Promise<boolean> {
|
||||
const fileContent = await fs.promises.readFile(KNOW_HOST_FILE, { encoding: 'utf8' });
|
||||
const lines = fileContent.split(/\r?\n/);
|
||||
for (let line of lines) {
|
||||
line = line.trim();
|
||||
if (!line.startsWith(HASH_MAGIC)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const [hostEncripted_] = line.split(' ');
|
||||
const [salt_, hostHash_] = hostEncripted_.substring(HASH_MAGIC.length).split(HASH_DELIM);
|
||||
const hostHash = crypto.createHmac('sha1', Buffer.from(salt_, 'base64')).update(host).digest();
|
||||
if (hostHash.toString('base64') === hostHash_) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
export async function addHostToHostFile(host: string, hostKey: Buffer, type: string): Promise<void> {
|
||||
if (!folderExists(PATH_SSH_USER_DIR)) {
|
||||
await fs.promises.mkdir(PATH_SSH_USER_DIR, 0o700);
|
||||
}
|
||||
|
||||
const salt = crypto.randomBytes(20);
|
||||
const hostHash = crypto.createHmac('sha1', salt).update(host).digest();
|
||||
|
||||
const entry = `${HASH_MAGIC}${salt.toString('base64')}${HASH_DELIM}${hostHash.toString('base64')} ${type} ${hostKey.toString('base64')}\n`;
|
||||
await fs.promises.appendFile(KNOW_HOST_FILE, entry);
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
import * as fs from 'fs';
|
||||
import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
import * as crypto from 'crypto';
|
||||
import type { ParsedKey } from 'ssh2-streams';
|
||||
import * as ssh2 from 'ssh2';
|
||||
import { untildify, exists as fileExists } from '../common/files';
|
||||
import Log from '../common/logger';
|
||||
|
||||
const homeDir = os.homedir();
|
||||
const PATH_SSH_CLIENT_ID_DSA = path.join(homeDir, '.ssh', '/id_dsa');
|
||||
const PATH_SSH_CLIENT_ID_ECDSA = path.join(homeDir, '.ssh', '/id_ecdsa');
|
||||
const PATH_SSH_CLIENT_ID_RSA = path.join(homeDir, '.ssh', '/id_rsa');
|
||||
const PATH_SSH_CLIENT_ID_ED25519 = path.join(homeDir, '.ssh', '/id_ed25519');
|
||||
const PATH_SSH_CLIENT_ID_XMSS = path.join(homeDir, '.ssh', '/id_xmss');
|
||||
const PATH_SSH_CLIENT_ID_ECDSA_SK = path.join(homeDir, '.ssh', '/id_ecdsa_sk');
|
||||
const PATH_SSH_CLIENT_ID_ED25519_SK = path.join(homeDir, '.ssh', '/id_ed25519_sk');
|
||||
|
||||
const DEFAULT_IDENTITY_FILES: string[] = [
|
||||
PATH_SSH_CLIENT_ID_RSA,
|
||||
PATH_SSH_CLIENT_ID_ECDSA,
|
||||
PATH_SSH_CLIENT_ID_ECDSA_SK,
|
||||
PATH_SSH_CLIENT_ID_ED25519,
|
||||
PATH_SSH_CLIENT_ID_ED25519_SK,
|
||||
PATH_SSH_CLIENT_ID_XMSS,
|
||||
PATH_SSH_CLIENT_ID_DSA,
|
||||
];
|
||||
|
||||
export interface SSHKey {
|
||||
filename: string;
|
||||
parsedKey: ParsedKey;
|
||||
fingerprint: string;
|
||||
agentSupport?: boolean;
|
||||
isPrivate?: boolean;
|
||||
}
|
||||
|
||||
// From https://github.com/openssh/openssh-portable/blob/acb2059febaddd71ee06c2ebf63dcf211d9ab9f2/sshconnect2.c#L1689-L1690
|
||||
export async function gatherIdentityFiles(identityFiles: string[], sshAgentSock: string | undefined, identitiesOnly: boolean, logger: Log) {
|
||||
identityFiles = identityFiles.map(untildify).map(i => i.replace(/\.pub$/, ''));
|
||||
if (identityFiles.length === 0) {
|
||||
identityFiles.push(...DEFAULT_IDENTITY_FILES);
|
||||
}
|
||||
|
||||
const identityFileContentsResult = await Promise.allSettled(identityFiles.map(async keyPath => {
|
||||
keyPath = await fileExists(keyPath + '.pub') ? keyPath + '.pub' : keyPath;
|
||||
return fs.promises.readFile(keyPath);
|
||||
}));
|
||||
const fileKeys: SSHKey[] = identityFileContentsResult.map((result, i) => {
|
||||
if (result.status === 'rejected') {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const parsedResult = ssh2.utils.parseKey(result.value);
|
||||
if (parsedResult instanceof Error || !parsedResult) {
|
||||
logger.error(`Error while parsing SSH public key ${identityFiles[i]}:`, parsedResult);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const parsedKey = Array.isArray(parsedResult) ? parsedResult[0] : parsedResult;
|
||||
const fingerprint = crypto.createHash('sha256').update(parsedKey.getPublicSSH()).digest('base64');
|
||||
|
||||
return {
|
||||
filename: identityFiles[i],
|
||||
parsedKey,
|
||||
fingerprint
|
||||
};
|
||||
}).filter(<T>(v: T | undefined): v is T => !!v);
|
||||
|
||||
let sshAgentParsedKeys: ParsedKey[] = [];
|
||||
try {
|
||||
if (!sshAgentSock) {
|
||||
throw new Error(`SSH_AUTH_SOCK environment variable not defined`);
|
||||
}
|
||||
|
||||
sshAgentParsedKeys = await new Promise<ParsedKey[]>((resolve, reject) => {
|
||||
const sshAgent = new ssh2.OpenSSHAgent(sshAgentSock);
|
||||
sshAgent.getIdentities((err, publicKeys) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve(publicKeys || []);
|
||||
}
|
||||
});
|
||||
});
|
||||
} catch (e) {
|
||||
logger.error(`Couldn't get identities from OpenSSH agent`, e);
|
||||
}
|
||||
|
||||
const sshAgentKeys: SSHKey[] = sshAgentParsedKeys.map(parsedKey => {
|
||||
const fingerprint = crypto.createHash('sha256').update(parsedKey.getPublicSSH()).digest('base64');
|
||||
return {
|
||||
filename: parsedKey.comment,
|
||||
parsedKey,
|
||||
fingerprint,
|
||||
agentSupport: true
|
||||
};
|
||||
});
|
||||
|
||||
const agentKeys: SSHKey[] = [];
|
||||
const preferredIdentityKeys: SSHKey[] = [];
|
||||
for (const agentKey of sshAgentKeys) {
|
||||
const foundIdx = fileKeys.findIndex(k => agentKey.parsedKey.type === k.parsedKey.type && agentKey.fingerprint === k.fingerprint);
|
||||
if (foundIdx >= 0) {
|
||||
preferredIdentityKeys.push({ ...fileKeys[foundIdx], agentSupport: true });
|
||||
fileKeys.splice(foundIdx, 1);
|
||||
} else if (!identitiesOnly) {
|
||||
agentKeys.push(agentKey);
|
||||
}
|
||||
}
|
||||
preferredIdentityKeys.push(...agentKeys);
|
||||
preferredIdentityKeys.push(...fileKeys);
|
||||
|
||||
logger.trace(`Identity keys:`, preferredIdentityKeys.length ? preferredIdentityKeys.map(k => `${k.filename} ${k.parsedKey.type} SHA256:${k.fingerprint}`).join('\n') : 'None');
|
||||
|
||||
return preferredIdentityKeys;
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
import * as os from 'os';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import SSHConfig, { Directive, Line, Section } from '@jeanp413/ssh-config';
|
||||
import * as vscode from 'vscode';
|
||||
import { exists as fileExists, normalizeToSlash, untildify } from '../common/files';
|
||||
import { isWindows } from '../common/platform';
|
||||
import { glob } from 'glob';
|
||||
|
||||
const systemSSHConfig = isWindows ? path.resolve(process.env.ALLUSERSPROFILE || 'C:\\ProgramData', 'ssh\\ssh_config') : '/etc/ssh/ssh_config';
|
||||
const defaultSSHConfigPath = path.resolve(os.homedir(), '.ssh/config');
|
||||
|
||||
export function getSSHConfigPath() {
|
||||
const sshConfigPath = vscode.workspace.getConfiguration('remote.SSH').get<string>('configFile');
|
||||
return sshConfigPath ? untildify(sshConfigPath) : defaultSSHConfigPath;
|
||||
}
|
||||
|
||||
function isDirective(line: Line): line is Directive {
|
||||
return line.type === SSHConfig.DIRECTIVE;
|
||||
}
|
||||
|
||||
function isHostSection(line: Line): line is Section {
|
||||
return isDirective(line) && line.param === 'Host' && !!line.value && !!(line as Section).config;
|
||||
}
|
||||
|
||||
function isIncludeDirective(line: Line): line is Section {
|
||||
return isDirective(line) && line.param === 'Include' && !!line.value;
|
||||
}
|
||||
|
||||
const SSH_CONFIG_PROPERTIES: Record<string, string> = {
|
||||
'host': 'Host',
|
||||
'hostname': 'HostName',
|
||||
'user': 'User',
|
||||
'port': 'Port',
|
||||
'identityagent': 'IdentityAgent',
|
||||
'identitiesonly': 'IdentitiesOnly',
|
||||
'identityfile': 'IdentityFile',
|
||||
'forwardagent': 'ForwardAgent',
|
||||
'preferredauthentications': 'PreferredAuthentications',
|
||||
'proxyjump': 'ProxyJump',
|
||||
'proxycommand': 'ProxyCommand',
|
||||
'include': 'Include',
|
||||
};
|
||||
|
||||
function normalizeProp(prop: Directive) {
|
||||
prop.param = SSH_CONFIG_PROPERTIES[prop.param.toLowerCase()] || prop.param;
|
||||
}
|
||||
|
||||
function normalizeSSHConfig(config: SSHConfig) {
|
||||
for (const line of config) {
|
||||
if (isDirective(line)) {
|
||||
normalizeProp(line);
|
||||
}
|
||||
if (isHostSection(line)) {
|
||||
normalizeSSHConfig(line.config);
|
||||
}
|
||||
}
|
||||
return config;
|
||||
}
|
||||
|
||||
async function parseSSHConfigFromFile(filePath: string, userConfig: boolean) {
|
||||
let content = '';
|
||||
if (await fileExists(filePath)) {
|
||||
content = (await fs.promises.readFile(filePath, 'utf8')).trim();
|
||||
}
|
||||
const config = normalizeSSHConfig(SSHConfig.parse(content));
|
||||
|
||||
const includedConfigs: [number, SSHConfig[]][] = [];
|
||||
for (let i = 0; i < config.length; i++) {
|
||||
const line = config[i];
|
||||
if (isIncludeDirective(line)) {
|
||||
const values = (line.value as string).split(',').map(s => s.trim());
|
||||
const configs: SSHConfig[] = [];
|
||||
for (const value of values) {
|
||||
const includePaths = await glob(normalizeToSlash(untildify(value)), {
|
||||
absolute: true,
|
||||
cwd: normalizeToSlash(path.dirname(userConfig ? defaultSSHConfigPath : systemSSHConfig))
|
||||
});
|
||||
for (const p of includePaths) {
|
||||
configs.push(await parseSSHConfigFromFile(p, userConfig));
|
||||
}
|
||||
}
|
||||
includedConfigs.push([i, configs]);
|
||||
}
|
||||
}
|
||||
for (const [idx, includeConfigs] of includedConfigs.reverse()) {
|
||||
config.splice(idx, 1, ...includeConfigs.flat());
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
export default class SSHConfiguration {
|
||||
|
||||
static async loadFromFS(): Promise<SSHConfiguration> {
|
||||
const config = await parseSSHConfigFromFile(getSSHConfigPath(), true);
|
||||
config.push(...await parseSSHConfigFromFile(systemSSHConfig, false));
|
||||
|
||||
return new SSHConfiguration(config);
|
||||
}
|
||||
|
||||
constructor(private sshConfig: SSHConfig) {
|
||||
}
|
||||
|
||||
getAllConfiguredHosts(): string[] {
|
||||
const hosts = new Set<string>();
|
||||
for (const line of this.sshConfig) {
|
||||
if (isHostSection(line)) {
|
||||
const value = Array.isArray(line.value) ? line.value[0] : line.value;
|
||||
const isPattern = /^!/.test(value) || /[?*]/.test(value);
|
||||
if (!isPattern) {
|
||||
hosts.add(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return [...hosts.keys()];
|
||||
}
|
||||
|
||||
getHostConfiguration(host: string): Record<string, string> {
|
||||
// Only a few directives return an array
|
||||
// https://github.com/jeanp413/ssh-config/blob/8d187bb8f1d83a51ff2b1d127e6b6269d24092b5/src/ssh-config.ts#L9C1-L9C118
|
||||
return this.sshConfig.compute(host) as Record<string, string>;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,367 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { EventEmitter } from 'events';
|
||||
import * as net from 'net';
|
||||
import * as fs from 'fs';
|
||||
import * as stream from 'stream';
|
||||
import { Client, ClientChannel, ClientErrorExtensions, ExecOptions, ShellOptions, ConnectConfig } from 'ssh2';
|
||||
import { Server } from 'net';
|
||||
import { SocksConnectionInfo, createServer as createSocksServer } from 'simple-socks';
|
||||
|
||||
export interface SSHConnectConfig extends ConnectConfig {
|
||||
/** Optional Unique ID attached to ssh connection. */
|
||||
uniqueId?: string;
|
||||
/** Automatic retry to connect, after disconnect. Default true */
|
||||
reconnect?: boolean;
|
||||
/** Number of reconnect retry, after disconnect. Default 10 */
|
||||
reconnectTries?: number;
|
||||
/** Delay after which reconnect should be done. Default 5000ms */
|
||||
reconnectDelay?: number;
|
||||
/** Path to private key */
|
||||
identity?: string | Buffer;
|
||||
}
|
||||
|
||||
export interface SSHTunnelConfig {
|
||||
/** Remote Address to connect */
|
||||
remoteAddr?: string;
|
||||
/** Local port to bind to. By default, it will bind to a random port, if not passed */
|
||||
localPort?: number;
|
||||
/** Remote Port to connect */
|
||||
remotePort?: number;
|
||||
/** Remote socket path to connect */
|
||||
remoteSocketPath?: string;
|
||||
socks?: boolean;
|
||||
/** Unique name */
|
||||
name?: string;
|
||||
}
|
||||
|
||||
const defaultOptions: Partial<SSHConnectConfig> = {
|
||||
reconnect: false,
|
||||
port: 22,
|
||||
reconnectTries: 3,
|
||||
reconnectDelay: 5000
|
||||
};
|
||||
|
||||
const SSHConstants = {
|
||||
'CHANNEL': {
|
||||
SSH: 'ssh',
|
||||
TUNNEL: 'tunnel',
|
||||
X11: 'x11'
|
||||
},
|
||||
'STATUS': {
|
||||
BEFORECONNECT: 'beforeconnect',
|
||||
CONNECT: 'connect',
|
||||
BEFOREDISCONNECT: 'beforedisconnect',
|
||||
DISCONNECT: 'disconnect'
|
||||
}
|
||||
};
|
||||
|
||||
export default class SSHConnection extends EventEmitter {
|
||||
public config: SSHConnectConfig;
|
||||
|
||||
private activeTunnels: { [index: string]: SSHTunnelConfig & { server: Server } } = {};
|
||||
private __$connectPromise: Promise<SSHConnection> | null = null;
|
||||
private __retries: number = 0;
|
||||
private __err: Error & ClientErrorExtensions & { code?: string } | null = null;
|
||||
private sshConnection: Client | null = null;
|
||||
|
||||
constructor(options: SSHConnectConfig) {
|
||||
super();
|
||||
this.config = Object.assign({}, defaultOptions, options);
|
||||
this.config.uniqueId = this.config.uniqueId || `${this.config.username}@${this.config.host}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Emit message on this channel
|
||||
*/
|
||||
override emit(channel: string, status: string, payload?: any): boolean {
|
||||
super.emit(channel, status, this, payload);
|
||||
return super.emit(`${channel}:${status}`, this, payload);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get shell socket
|
||||
*/
|
||||
shell(options: ShellOptions = {}): Promise<ClientChannel> {
|
||||
return this.connect().then(() => {
|
||||
return new Promise<ClientChannel>((resolve, reject) => {
|
||||
this.sshConnection!.shell(options, (err, stream) => err ? reject(err) : resolve(stream));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Exec a command
|
||||
*/
|
||||
exec(cmd: string, params?: Array<string>, options: ExecOptions = {}): Promise<{ stdout: string; stderr: string }> {
|
||||
cmd += (Array.isArray(params) ? (' ' + params.join(' ')) : '');
|
||||
return this.connect().then(() => {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.sshConnection!.exec(cmd, options, (err, stream) => {
|
||||
if (err) {
|
||||
return reject(err);
|
||||
}
|
||||
let stdout = '';
|
||||
let stderr = '';
|
||||
stream.on('close', function () {
|
||||
return resolve({ stdout, stderr });
|
||||
}).on('data', function (data: Buffer | string) {
|
||||
stdout += data.toString();
|
||||
}).stderr.on('data', function (data: Buffer | string) {
|
||||
stderr += data.toString();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Exec a command
|
||||
*/
|
||||
execPartial(cmd: string, tester: (stdout: string, stderr: string) => boolean, params?: Array<string>, options: ExecOptions = {}): Promise<{ stdout: string; stderr: string }> {
|
||||
cmd += (Array.isArray(params) ? (' ' + params.join(' ')) : '');
|
||||
return this.connect().then(() => {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.sshConnection!.exec(cmd, options, (err, stream) => {
|
||||
if (err) {
|
||||
return reject(err);
|
||||
}
|
||||
let stdout = '';
|
||||
let stderr = '';
|
||||
let resolved = false;
|
||||
stream.on('close', function () {
|
||||
if (!resolved) {
|
||||
return resolve({ stdout, stderr });
|
||||
}
|
||||
}).on('data', function (data: Buffer | string) {
|
||||
stdout += data.toString();
|
||||
|
||||
if (tester(stdout, stderr)) {
|
||||
resolved = true;
|
||||
|
||||
return resolve({ stdout, stderr });
|
||||
}
|
||||
}).stderr.on('data', function (data: Buffer | string) {
|
||||
stderr += data.toString();
|
||||
|
||||
if (tester(stdout, stderr)) {
|
||||
resolved = true;
|
||||
|
||||
return resolve({ stdout, stderr });
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Forward out
|
||||
*/
|
||||
forwardOut(srcIP: string, srcPort: number, destIP: string, destPort: number): Promise<ClientChannel> {
|
||||
return this.connect().then(() => {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.sshConnection!.forwardOut(srcIP, srcPort, destIP, destPort, (err, stream) => {
|
||||
if (err) {
|
||||
return reject(err);
|
||||
}
|
||||
resolve(stream);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a Socks Port
|
||||
*/
|
||||
getSocksPort(localPort: number): Promise<number> {
|
||||
return this.addTunnel({ name: '__socksServer', socks: true, localPort: localPort }).then((tunnel) => {
|
||||
return tunnel.localPort!;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Close SSH Connection
|
||||
*/
|
||||
close(): Promise<void> {
|
||||
this.emit(SSHConstants.CHANNEL.SSH, SSHConstants.STATUS.BEFOREDISCONNECT);
|
||||
return this.closeTunnel().then(() => {
|
||||
if (this.sshConnection) {
|
||||
this.sshConnection.end();
|
||||
this.emit(SSHConstants.CHANNEL.SSH, SSHConstants.STATUS.DISCONNECT);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Connect the SSH Connection
|
||||
*/
|
||||
connect(c?: SSHConnectConfig): Promise<SSHConnection> {
|
||||
this.config = Object.assign(this.config, c);
|
||||
++this.__retries;
|
||||
|
||||
if (this.__$connectPromise) {
|
||||
return this.__$connectPromise;
|
||||
}
|
||||
|
||||
this.__$connectPromise = new Promise((resolve, reject) => {
|
||||
this.emit(SSHConstants.CHANNEL.SSH, SSHConstants.STATUS.BEFORECONNECT);
|
||||
if (!this.config || typeof this.config === 'function' || !(this.config.host || this.config.sock) || !this.config.username) {
|
||||
reject(`Invalid SSH connection configuration host/username can't be empty`);
|
||||
this.__$connectPromise = null;
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.config.identity) {
|
||||
if (fs.existsSync(this.config.identity)) {
|
||||
this.config.privateKey = fs.readFileSync(this.config.identity);
|
||||
}
|
||||
delete this.config.identity;
|
||||
}
|
||||
|
||||
//Start ssh server connection
|
||||
this.sshConnection = new Client();
|
||||
this.sshConnection.on('ready', (err: Error & ClientErrorExtensions) => {
|
||||
if (err) {
|
||||
this.emit(SSHConstants.CHANNEL.SSH, SSHConstants.STATUS.DISCONNECT, { err: err });
|
||||
this.__$connectPromise = null;
|
||||
return reject(err);
|
||||
}
|
||||
this.emit(SSHConstants.CHANNEL.SSH, SSHConstants.STATUS.CONNECT);
|
||||
this.__retries = 0;
|
||||
this.__err = null;
|
||||
resolve(this);
|
||||
}).on('error', (err) => {
|
||||
this.emit(SSHConstants.CHANNEL.SSH, SSHConstants.STATUS.DISCONNECT, { err: err });
|
||||
this.__err = err;
|
||||
}).on('close', () => {
|
||||
this.emit(SSHConstants.CHANNEL.SSH, SSHConstants.STATUS.DISCONNECT, { err: this.__err });
|
||||
if (this.config.reconnect && this.__retries <= this.config.reconnectTries! && this.__err && this.__err.level !== 'client-authentication' && this.__err.code !== 'ENOTFOUND') {
|
||||
setTimeout(() => {
|
||||
this.__$connectPromise = null;
|
||||
resolve(this.connect());
|
||||
}, this.config.reconnectDelay);
|
||||
} else {
|
||||
reject(this.__err);
|
||||
}
|
||||
}).connect(this.config);
|
||||
});
|
||||
return this.__$connectPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get existing tunnel by name
|
||||
*/
|
||||
getTunnel(name: string) {
|
||||
return this.activeTunnels[name];
|
||||
}
|
||||
|
||||
/**
|
||||
* Add new tunnel if not exist
|
||||
*/
|
||||
addTunnel(SSHTunnelConfig: SSHTunnelConfig): Promise<SSHTunnelConfig & { server: Server }> {
|
||||
SSHTunnelConfig.name = SSHTunnelConfig.name || `${SSHTunnelConfig.remoteAddr}@${SSHTunnelConfig.remotePort || SSHTunnelConfig.remoteSocketPath}`;
|
||||
this.emit(SSHConstants.CHANNEL.TUNNEL, SSHConstants.STATUS.BEFORECONNECT, { SSHTunnelConfig: SSHTunnelConfig });
|
||||
if (this.getTunnel(SSHTunnelConfig.name)) {
|
||||
this.emit(SSHConstants.CHANNEL.TUNNEL, SSHConstants.STATUS.CONNECT, { SSHTunnelConfig: SSHTunnelConfig });
|
||||
return Promise.resolve(this.getTunnel(SSHTunnelConfig.name));
|
||||
} else {
|
||||
return new Promise((resolve, reject) => {
|
||||
let server: net.Server;
|
||||
if (SSHTunnelConfig.socks) {
|
||||
server = createSocksServer({
|
||||
connectionFilter: (destination: SocksConnectionInfo, origin: SocksConnectionInfo, callback: (err?: any, dest?: stream.Duplex) => void) => {
|
||||
this.connect().then(() => {
|
||||
this.sshConnection!.forwardOut(
|
||||
origin.address,
|
||||
origin.port,
|
||||
destination.address,
|
||||
destination.port,
|
||||
(err, stream) => {
|
||||
if (err) {
|
||||
this.emit(SSHConstants.CHANNEL.TUNNEL, SSHConstants.STATUS.DISCONNECT, { SSHTunnelConfig: SSHTunnelConfig, err: err });
|
||||
return callback(err);
|
||||
}
|
||||
return callback(null, stream);
|
||||
});
|
||||
});
|
||||
}
|
||||
}).on('proxyError', (err: any) => {
|
||||
this.emit(SSHConstants.CHANNEL.TUNNEL, SSHConstants.STATUS.DISCONNECT, { SSHTunnelConfig: SSHTunnelConfig, err: err });
|
||||
});
|
||||
} else {
|
||||
server = net.createServer()
|
||||
.on('connection', (socket) => {
|
||||
this.connect().then(() => {
|
||||
if (SSHTunnelConfig.remotePort) {
|
||||
this.sshConnection!.forwardOut('127.0.0.1', 0, SSHTunnelConfig.remoteAddr!, SSHTunnelConfig.remotePort!, (err, stream) => {
|
||||
if (err) {
|
||||
this.emit(SSHConstants.CHANNEL.TUNNEL, SSHConstants.STATUS.DISCONNECT, { SSHTunnelConfig: SSHTunnelConfig, err: err });
|
||||
return;
|
||||
}
|
||||
stream.pipe(socket);
|
||||
socket.pipe(stream);
|
||||
});
|
||||
} else {
|
||||
this.sshConnection!.openssh_forwardOutStreamLocal(SSHTunnelConfig.remoteSocketPath!, (err, stream) => {
|
||||
if (err) {
|
||||
this.emit(SSHConstants.CHANNEL.TUNNEL, SSHConstants.STATUS.DISCONNECT, { SSHTunnelConfig: SSHTunnelConfig, err: err });
|
||||
return;
|
||||
}
|
||||
stream.pipe(socket);
|
||||
socket.pipe(stream);
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
SSHTunnelConfig.localPort = SSHTunnelConfig.localPort || 0;
|
||||
server.on('listening', () => {
|
||||
SSHTunnelConfig.localPort = (server.address() as net.AddressInfo).port;
|
||||
this.activeTunnels[SSHTunnelConfig.name!] = Object.assign({}, { server }, SSHTunnelConfig);
|
||||
this.emit(SSHConstants.CHANNEL.TUNNEL, SSHConstants.STATUS.CONNECT, { SSHTunnelConfig: SSHTunnelConfig });
|
||||
resolve(this.activeTunnels[SSHTunnelConfig.name!]);
|
||||
}).on('error', (err: any) => {
|
||||
this.emit(SSHConstants.CHANNEL.TUNNEL, SSHConstants.STATUS.DISCONNECT, { SSHTunnelConfig: SSHTunnelConfig, err: err });
|
||||
server.close();
|
||||
reject(err);
|
||||
delete this.activeTunnels[SSHTunnelConfig.name!];
|
||||
}).listen(SSHTunnelConfig.localPort);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Close the tunnel
|
||||
*/
|
||||
closeTunnel(name?: string): Promise<void> {
|
||||
if (name && this.activeTunnels[name]) {
|
||||
return new Promise((resolve) => {
|
||||
const tunnel = this.activeTunnels[name];
|
||||
this.emit(
|
||||
SSHConstants.CHANNEL.TUNNEL,
|
||||
SSHConstants.STATUS.BEFOREDISCONNECT,
|
||||
{ SSHTunnelConfig: tunnel }
|
||||
);
|
||||
tunnel.server.close(() => {
|
||||
this.emit(
|
||||
SSHConstants.CHANNEL.TUNNEL,
|
||||
SSHConstants.STATUS.DISCONNECT,
|
||||
{ SSHTunnelConfig: this.activeTunnels[name] }
|
||||
);
|
||||
delete this.activeTunnels[name];
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
} else if (!name) {
|
||||
const tunnels = Object.keys(this.activeTunnels).map((key) => this.closeTunnel(key));
|
||||
return Promise.all(tunnels).then(() => { });
|
||||
}
|
||||
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
export default class SSHDestination {
|
||||
constructor(
|
||||
public readonly hostname: string,
|
||||
public readonly user?: string,
|
||||
public readonly port?: number
|
||||
) {
|
||||
}
|
||||
|
||||
static parse(dest: string): SSHDestination {
|
||||
let user: string | undefined;
|
||||
const atPos = dest.lastIndexOf('@');
|
||||
if (atPos !== -1) {
|
||||
user = dest.substring(0, atPos);
|
||||
}
|
||||
|
||||
let port: number | undefined;
|
||||
const colonPos = dest.lastIndexOf(':');
|
||||
if (colonPos !== -1) {
|
||||
port = parseInt(dest.substring(colonPos + 1), 10);
|
||||
}
|
||||
|
||||
const start = atPos !== -1 ? atPos + 1 : 0;
|
||||
const end = colonPos !== -1 ? colonPos : dest.length;
|
||||
const hostname = dest.substring(start, end);
|
||||
|
||||
return new SSHDestination(hostname, user, port);
|
||||
}
|
||||
|
||||
toString(): string {
|
||||
let result = this.hostname;
|
||||
if (this.user) {
|
||||
result = `${this.user}@` + result;
|
||||
}
|
||||
if (this.port) {
|
||||
result = result + `:${this.port}`;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// vscode.uri implementation lowercases the authority, so when reopen or restore
|
||||
// a remote session from the recently openend list the connection fails
|
||||
static parseEncoded(dest: string): SSHDestination {
|
||||
try {
|
||||
const data = JSON.parse(Buffer.from(dest, 'hex').toString());
|
||||
return new SSHDestination(data.hostName, data.user, data.port);
|
||||
} catch {
|
||||
}
|
||||
return SSHDestination.parse(dest.replace(/\\x([0-9a-f]{2})/g, (_, charCode) => String.fromCharCode(parseInt(charCode, 16))));
|
||||
}
|
||||
|
||||
toEncodedString(): string {
|
||||
return this.toString().replace(/[A-Z]/g, (ch) => `\\x${ch.charCodeAt(0).toString(16).toLowerCase()}`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"extends": "../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./out",
|
||||
},
|
||||
"include": [
|
||||
"src/**/*",
|
||||
"../../src/vscode-dts/vscode.d.ts",
|
||||
"../../src/vscode-dts/vscode.proposed.resolvers.d.ts",
|
||||
"../../src/vscode-dts/vscode.proposed.contribViewsRemote.d.ts",
|
||||
]
|
||||
}
|
||||
@@ -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<string, Set<string>>;
|
||||
private visited: Set<string>;
|
||||
private parsedFiles: Map<string, Parser.Tree>;
|
||||
private imports: Map<string, Map<string, ImportInfo>>;
|
||||
private definitions: Map<string, Definition>;
|
||||
private fileStack: Set<string>;
|
||||
|
||||
constructor() {
|
||||
this.parser = new Parser();
|
||||
this.parser.setLanguage(JavaScript);
|
||||
this.graph = new Map();
|
||||
this.visited = new Set();
|
||||
this.parsedFiles = new Map();
|
||||
this.imports = new Map();
|
||||
this.definitions = new Map();
|
||||
this.fileStack = new Set();
|
||||
}
|
||||
|
||||
async parseFile(filePath: string): Promise<Parser.Tree | null> {
|
||||
if (this.parsedFiles.has(filePath)) {
|
||||
return this.parsedFiles.get(filePath)!;
|
||||
}
|
||||
|
||||
if (this.fileStack.has(filePath)) {
|
||||
return null; // Circular import
|
||||
}
|
||||
|
||||
this.fileStack.add(filePath);
|
||||
|
||||
try {
|
||||
const uri = vscode.Uri.file(filePath);
|
||||
const document = await vscode.workspace.openTextDocument(uri);
|
||||
const code = document.getText();
|
||||
const tree = this.parser.parse(code);
|
||||
|
||||
this.parsedFiles.set(filePath, tree);
|
||||
this.collectImports(filePath, tree);
|
||||
this.collectDefinitions(filePath, tree);
|
||||
|
||||
return tree;
|
||||
} catch (error) {
|
||||
console.error(`Error parsing ${filePath}:`, error);
|
||||
return null;
|
||||
} finally {
|
||||
this.fileStack.delete(filePath);
|
||||
}
|
||||
}
|
||||
|
||||
private collectImports(filePath: string, tree: Parser.Tree): void {
|
||||
const fileImports = new Map<string, ImportInfo>();
|
||||
|
||||
const visit = (node: Parser.SyntaxNode): void => {
|
||||
if (node.type === 'import_declaration') {
|
||||
const source = node.childForFieldName('source')?.text.slice(1, -1) ?? '';
|
||||
const specifiers = node.childForFieldName('specifiers');
|
||||
|
||||
specifiers?.children.forEach(spec => {
|
||||
if (spec.type === 'import_specifier') {
|
||||
const local = spec.childForFieldName('local')?.text ?? '';
|
||||
const imported = spec.childForFieldName('imported')?.text ?? '';
|
||||
fileImports.set(local, { source, imported });
|
||||
}
|
||||
});
|
||||
}
|
||||
node.children.forEach(visit);
|
||||
};
|
||||
|
||||
visit(tree.rootNode);
|
||||
this.imports.set(filePath, fileImports);
|
||||
}
|
||||
|
||||
private collectDefinitions(filePath: string, tree: Parser.Tree): void {
|
||||
const visit = (node: Parser.SyntaxNode): void => {
|
||||
if (node.type === 'function_declaration') {
|
||||
const name = node.childForFieldName('name')?.text ?? '';
|
||||
this.definitions.set(name, { file: filePath, node });
|
||||
}
|
||||
else if (node.type === 'variable_declarator') {
|
||||
const name = node.childForFieldName('name')?.text;
|
||||
const value = node.childForFieldName('value');
|
||||
if (name && (value?.type === 'arrow_function' || value?.type === 'function')) {
|
||||
this.definitions.set(name, { file: filePath, node: value });
|
||||
}
|
||||
}
|
||||
node.children.forEach(visit);
|
||||
};
|
||||
|
||||
visit(tree.rootNode);
|
||||
}
|
||||
|
||||
private async getTypeFromPosition(uri: vscode.Uri, position: vscode.Position): Promise<string | null> {
|
||||
const hover = await vscode.commands.executeCommand<vscode.Hover[]>(
|
||||
'vscode.executeHoverProvider',
|
||||
uri,
|
||||
position
|
||||
);
|
||||
|
||||
if (hover?.[0]?.contents.length) {
|
||||
for (const content of hover[0].contents) {
|
||||
let hoverText = typeof content === 'string' ?
|
||||
content :
|
||||
('value' in content ? content.value : '');
|
||||
|
||||
// Remove typescript backticks if present
|
||||
hoverText = hoverText.replace(/```typescript\s*/, '').replace(/```\s*$/, '');
|
||||
console.log('Processing hover text:', hoverText);
|
||||
|
||||
// Extract the type information - look for the type after the colon
|
||||
const typeMatches = [
|
||||
/:\s*([\w<>]+)(?:\[\])?/, // matches "foo: Type" or "foo: Type[]"
|
||||
/var\s+\w+:\s*([\w<>]+)/, // matches "var foo: Type"
|
||||
/\(type\)\s+[\w<>]+:\s*([\w<>]+)/, // matches "(type) foo: Type"
|
||||
/\(method\)\s*([\w<>]+)\./ // matches "(method) Type.method"
|
||||
];
|
||||
|
||||
for (const pattern of typeMatches) {
|
||||
const match = pattern.exec(hoverText);
|
||||
if (match) {
|
||||
let type = match[1];
|
||||
// Handle array types
|
||||
if (hoverText.includes('[]')) {
|
||||
return 'Array';
|
||||
}
|
||||
// Extract base type from generics
|
||||
if (type.includes('<')) {
|
||||
type = type.split('<')[0];
|
||||
}
|
||||
return type;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private async getCallsInDefn(defnNode: Parser.SyntaxNode, currentFile: string): Promise<Set<string>> {
|
||||
const calls = new Set<string>();
|
||||
const fileImports = this.imports.get(currentFile) ?? new Map();
|
||||
const uri = vscode.Uri.file(currentFile);
|
||||
|
||||
const visit = async (node: Parser.SyntaxNode): Promise<void> => {
|
||||
if (node.type === 'call_expression') {
|
||||
const callee = node.childForFieldName('function');
|
||||
if (callee?.type === 'identifier') {
|
||||
const name = callee.text;
|
||||
const importInfo = fileImports.get(name);
|
||||
if (importInfo) {
|
||||
calls.add(`${importInfo.source}:${importInfo.imported}`);
|
||||
} else {
|
||||
calls.add(name);
|
||||
}
|
||||
}
|
||||
else if (callee?.type === 'member_expression') {
|
||||
const method = callee.childForFieldName('property')?.text;
|
||||
const object = callee.childForFieldName('object');
|
||||
|
||||
if (method && object) {
|
||||
const position = new vscode.Position(
|
||||
object.startPosition.row,
|
||||
object.startPosition.column
|
||||
);
|
||||
|
||||
const type = await this.getTypeFromPosition(uri, position);
|
||||
if (type) {
|
||||
calls.add(`${type}.${method}`);
|
||||
} else {
|
||||
calls.add(`method:${method}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const child of node.children) {
|
||||
await visit(child);
|
||||
}
|
||||
};
|
||||
|
||||
await visit(defnNode);
|
||||
return calls;
|
||||
}
|
||||
|
||||
private gotoDefn(name: string): Definition | null {
|
||||
if (name.includes(':')) {
|
||||
const [file, funcName] = name.split(':');
|
||||
const def = this.definitions.get(funcName);
|
||||
return def ?? null;
|
||||
}
|
||||
|
||||
return this.definitions.get(name) ?? null;
|
||||
}
|
||||
|
||||
private getUses(defnNode: Parser.SyntaxNode, currentFile: string): DefnUse[] {
|
||||
const uses: DefnUse[] = [];
|
||||
|
||||
let fnName: string | undefined;
|
||||
if (defnNode.type === 'function_declaration') {
|
||||
fnName = defnNode.childForFieldName('name')?.text;
|
||||
} else if (defnNode.type === 'arrow_function' || defnNode.type === 'function') {
|
||||
const parent = defnNode.parent;
|
||||
if (parent?.type === 'variable_declarator') {
|
||||
fnName = parent.childForFieldName('name')?.text;
|
||||
}
|
||||
}
|
||||
|
||||
if (!fnName) return uses;
|
||||
|
||||
for (const [file, tree] of this.parsedFiles) {
|
||||
const visit = (node: Parser.SyntaxNode): void => {
|
||||
if (node.type === 'call_expression') {
|
||||
const callee = node.childForFieldName('function');
|
||||
if (callee?.type === 'identifier' && callee.text === fnName) {
|
||||
let current: Parser.SyntaxNode | null = node;
|
||||
while (current) {
|
||||
if (current.type === 'function_declaration' ||
|
||||
current.type === 'arrow_function' ||
|
||||
current.type === 'function') {
|
||||
uses.push({ parent: current, file });
|
||||
break;
|
||||
}
|
||||
current = current.parent;
|
||||
}
|
||||
}
|
||||
}
|
||||
node.children.forEach(visit);
|
||||
};
|
||||
|
||||
visit(tree.rootNode);
|
||||
}
|
||||
|
||||
return uses;
|
||||
}
|
||||
|
||||
private async visitAllNodesInGraphFromDefinition(defn: Parser.SyntaxNode, currentFile: string): Promise<void> {
|
||||
let defnName: string | undefined;
|
||||
if (defn.type === 'function_declaration') {
|
||||
defnName = defn.childForFieldName('name')?.text;
|
||||
} else if (defn.type === 'arrow_function' || defn.type === 'function') {
|
||||
const parent = defn.parent;
|
||||
if (parent?.type === 'variable_declarator') {
|
||||
defnName = parent.childForFieldName('name')?.text;
|
||||
}
|
||||
}
|
||||
|
||||
if (!defnName) return;
|
||||
|
||||
const fullName = `${currentFile}:${defnName}`;
|
||||
if (this.visited.has(fullName)) return;
|
||||
|
||||
const calls = await this.getCallsInDefn(defn, currentFile);
|
||||
this.graph.set(fullName, calls);
|
||||
this.visited.add(fullName);
|
||||
|
||||
const callDefns = Array.from(calls).map(call => this.gotoDefn(call));
|
||||
for (const callDefn of callDefns) {
|
||||
if (callDefn) {
|
||||
await this.visitAllNodesInGraphFromDefinition(callDefn.node, callDefn.file);
|
||||
}
|
||||
}
|
||||
|
||||
const defnUses = this.getUses(defn, currentFile);
|
||||
for (const defnUse of defnUses) {
|
||||
await this.visitAllNodesInGraphFromDefinition(defnUse.parent, defnUse.file);
|
||||
}
|
||||
}
|
||||
|
||||
async analyze(entryFile: string): Promise<Map<string, Set<string>>> {
|
||||
const tree = await this.parseFile(entryFile);
|
||||
if (!tree) return new Map();
|
||||
|
||||
const visit = async (node: Parser.SyntaxNode): Promise<void> => {
|
||||
if (node.type === 'function_declaration') {
|
||||
await this.visitAllNodesInGraphFromDefinition(node, entryFile);
|
||||
}
|
||||
else if (node.type === 'variable_declarator') {
|
||||
const value = node.childForFieldName('value');
|
||||
if (value?.type === 'arrow_function' || value?.type === 'function') {
|
||||
await this.visitAllNodesInGraphFromDefinition(value, entryFile);
|
||||
}
|
||||
}
|
||||
for (const child of node.children) {
|
||||
await visit(child);
|
||||
}
|
||||
};
|
||||
|
||||
await visit(tree.rootNode);
|
||||
return this.graph;
|
||||
}
|
||||
}
|
||||
|
||||
export async function runTreeSitter(filePath?: string): Promise<Map<string, Set<string>> | null> {
|
||||
const editor = vscode.window.activeTextEditor;
|
||||
if (!editor && !filePath) {
|
||||
vscode.window.showWarningMessage('No active editor found');
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const targetPath = filePath ?? editor!.document.uri.fsPath;
|
||||
const analyzer = new ProjectAnalyzer();
|
||||
const graph = await analyzer.analyze(targetPath);
|
||||
|
||||
for (const [defn, calls] of graph) {
|
||||
console.log(`${defn} calls: ${[...calls].join(', ')}`);
|
||||
}
|
||||
|
||||
return graph;
|
||||
} catch (error) {
|
||||
console.error('Error analyzing file:', error);
|
||||
vscode.window.showErrorMessage('Error analyzing file');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -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.SemanticTokens>(
|
||||
'vscode.provideDocumentSemanticTokens',
|
||||
document.uri
|
||||
);
|
||||
|
||||
if (!tokens) {
|
||||
console.error('No tokens found');
|
||||
return [];
|
||||
}
|
||||
|
||||
const allTokens = decodeTokens(tokens, document);
|
||||
|
||||
|
||||
return allTokens;
|
||||
}
|
||||
|
||||
function decodeTokens(tokens: vscode.SemanticTokens, document: vscode.TextDocument) {
|
||||
const data = tokens.data;
|
||||
const decodedTokens = [];
|
||||
let line = 0;
|
||||
let character = 0;
|
||||
|
||||
for (let i = 0; i < data.length; i += 5) {
|
||||
const deltaLine = data[i];
|
||||
const deltaStartChar = data[i + 1];
|
||||
const length = data[i + 2];
|
||||
const tokenTypeIdx = data[i + 3];
|
||||
const tokenModifierIdx = data[i + 4];
|
||||
|
||||
line += deltaLine;
|
||||
character = deltaLine === 0 ? character + deltaStartChar : deltaStartChar;
|
||||
|
||||
const type = legend.tokenTypes[tokenTypeIdx] || `(${tokenTypeIdx})`;
|
||||
const modifier = legend.tokenModifiers[tokenModifierIdx] || `(${tokenModifierIdx})`;
|
||||
|
||||
const tokenRange = new vscode.Range(line, character, line, character + length);
|
||||
const tokenText = document.getText(tokenRange);
|
||||
|
||||
decodedTokens.push({
|
||||
line,
|
||||
startCharacter: character,
|
||||
length,
|
||||
type,
|
||||
modifier,
|
||||
text: tokenText,
|
||||
});
|
||||
|
||||
console.log(`Token: '${tokenText}' | Type: ${type} | Modifier: ${modifier} | Line: ${line}, Character: ${character}`);
|
||||
}
|
||||
|
||||
return decodedTokens;
|
||||
}
|
||||
@@ -3,8 +3,10 @@
|
||||
"productName": "Void",
|
||||
"version": "1.94.0",
|
||||
"distro": "this is a commit number if we want to publish on npm",
|
||||
"homepage": "https://voideditor.com",
|
||||
"author": {
|
||||
"name": "Glass Devtools, Inc."
|
||||
"name": "Glass Devtools, Inc.",
|
||||
"email": "andrew@voideditor.com"
|
||||
},
|
||||
"license": "MIT",
|
||||
"main": "./out/main",
|
||||
@@ -12,6 +14,7 @@
|
||||
"scripts": {
|
||||
"buildreact": "cd ./src/vs/workbench/contrib/void/browser/react/ && node build.js && cd ../../../../../../../",
|
||||
"watchreact": "cd ./src/vs/workbench/contrib/void/browser/react/ && node build.js --watch && cd ../../../../../../../",
|
||||
"watchreactd": "deemon npm run watchreact",
|
||||
"test": "echo Please run any of the test scripts from the scripts folder.",
|
||||
"test-browser": "npx playwright install && node test/unit/browser/index.js",
|
||||
"test-browser-amd": "npx playwright install && node test/unit/browser/index.amd.js",
|
||||
@@ -23,7 +26,7 @@
|
||||
"preinstall": "node build/npm/preinstall.js",
|
||||
"postinstall": "node build/npm/postinstall.js",
|
||||
"compile": "node ./node_modules/gulp/bin/gulp.js compile",
|
||||
"watch": "npm-run-all -lp watch-client watch-extensions",
|
||||
"watch": "npm-run-all -lp watchreact watch-client watch-extensions",
|
||||
"watch-amd": "npm-run-all -lp watch-client-amd watch-extensions",
|
||||
"watchd": "deemon npm run watch",
|
||||
"watch-webd": "deemon npm run watch-web",
|
||||
@@ -75,10 +78,12 @@
|
||||
"update-build-ts-version": "npm install typescript@next && tsc -p ./build/tsconfig.build.json"
|
||||
},
|
||||
"dependencies": {
|
||||
"@anthropic-ai/sdk": "^0.32.1",
|
||||
"@anthropic-ai/sdk": "^0.37.0",
|
||||
"@floating-ui/react": "^0.27.3",
|
||||
"@google/generative-ai": "^0.21.0",
|
||||
"@microsoft/1ds-core-js": "^3.2.13",
|
||||
"@microsoft/1ds-post-js": "^3.2.13",
|
||||
"@mistralai/mistralai": "^1.4.0",
|
||||
"@parcel/watcher": "2.1.0",
|
||||
"@rrweb/record": "^2.0.0-alpha.17",
|
||||
"@rrweb/types": "^2.0.0-alpha.17",
|
||||
@@ -105,6 +110,7 @@
|
||||
"@xterm/headless": "^5.6.0-beta.64",
|
||||
"@xterm/xterm": "^5.6.0-beta.64",
|
||||
"ajv": "^8.17.1",
|
||||
"cross-spawn": "^7.0.6",
|
||||
"diff": "^7.0.0",
|
||||
"groq-sdk": "^0.9.0",
|
||||
"http-proxy-agent": "^7.0.0",
|
||||
@@ -119,7 +125,7 @@
|
||||
"node-pty": "1.1.0-beta21",
|
||||
"ollama": "^0.5.11",
|
||||
"open": "^8.4.2",
|
||||
"openai": "^4.76.1",
|
||||
"openai": "^4.85.4",
|
||||
"posthog-node": "^4.3.1",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
@@ -130,7 +136,8 @@
|
||||
"vscode-regexpp": "^3.1.0",
|
||||
"vscode-textmate": "9.1.0",
|
||||
"yauzl": "^3.0.0",
|
||||
"yazl": "^2.4.3"
|
||||
"yazl": "^2.4.3",
|
||||
"zod": "^3.24.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.46.1",
|
||||
@@ -221,6 +228,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",
|
||||
|
||||
@@ -31,8 +31,8 @@
|
||||
"nodejsRepository": "https://nodejs.org",
|
||||
"urlProtocol": "void-editor",
|
||||
"extensionsGallery": {
|
||||
"serviceUrl": "https://open-vsx.org/vscode/gallery",
|
||||
"itemUrl": "https://open-vsx.org/vscode/item"
|
||||
"serviceUrl": "https://marketplace.visualstudio.com/_apis/public/gallery",
|
||||
"itemUrl": "https://marketplace.visualstudio.com/items"
|
||||
},
|
||||
"builtInExtensions": []
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
"@xterm/headless": "^5.6.0-beta.64",
|
||||
"@xterm/xterm": "^5.6.0-beta.64",
|
||||
"cookie": "^0.4.0",
|
||||
"debounced": "1.0.2",
|
||||
"http-proxy-agent": "^7.0.0",
|
||||
"https-proxy-agent": "^7.0.2",
|
||||
"jschardet": "3.1.3",
|
||||
@@ -37,6 +38,7 @@
|
||||
"native-watchdog": "^1.4.1",
|
||||
"node-pty": "1.1.0-beta21",
|
||||
"tas-client-umd": "0.2.0",
|
||||
"tslib": "^2.8.1",
|
||||
"vscode-oniguruma": "1.7.0",
|
||||
"vscode-regexpp": "^3.1.0",
|
||||
"vscode-textmate": "9.1.0",
|
||||
@@ -396,6 +398,12 @@
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/debounced": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/debounced/-/debounced-1.0.2.tgz",
|
||||
"integrity": "sha512-6GPv+l/OOtdb1DKNY70k5ubuJhVjtBjUnujC5vQAHHrMuvBpDXsTc91xEMTdeA3/v4swYHamtdB9XIN7DcKxpw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "4.3.4",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
|
||||
@@ -1028,6 +1036,11 @@
|
||||
"node": ">=8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/tslib": {
|
||||
"version": "2.8.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="
|
||||
},
|
||||
"node_modules/tunnel-agent": {
|
||||
"version": "0.6.0",
|
||||
"resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
|
||||
|
||||
@@ -26,12 +26,14 @@
|
||||
"cookie": "^0.4.0",
|
||||
"http-proxy-agent": "^7.0.0",
|
||||
"https-proxy-agent": "^7.0.2",
|
||||
"debounced": "1.0.2",
|
||||
"jschardet": "3.1.3",
|
||||
"kerberos": "2.1.1",
|
||||
"minimist": "^1.2.6",
|
||||
"native-watchdog": "^1.4.1",
|
||||
"node-pty": "1.1.0-beta21",
|
||||
"tas-client-umd": "0.2.0",
|
||||
"tslib": "^2.8.1",
|
||||
"vscode-oniguruma": "1.7.0",
|
||||
"vscode-regexpp": "^3.1.0",
|
||||
"vscode-textmate": "9.1.0",
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<Application xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
|
||||
<VisualElements
|
||||
BackgroundColor="#2D2D30"
|
||||
BackgroundColor="#FFFFFF"
|
||||
ShowNameOnSquare150x150Logo="on"
|
||||
Square150x150Logo="resources\app\resources\win32\code_150x150.png"
|
||||
Square70x70Logo="resources\app\resources\win32\code_70x70.png"
|
||||
ForegroundText="light"
|
||||
ShortDisplayName="Code - OSS" />
|
||||
ForegroundText="light"
|
||||
ShortDisplayName="Void" />
|
||||
</Application>
|
||||
|
||||
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 173 KiB |
|
Before Width: | Height: | Size: 5.5 MiB After Width: | Height: | Size: 1.4 MiB |
|
After Width: | Height: | Size: 795 KiB |
@@ -121,10 +121,11 @@ import { normalizeNFC } from '../../base/common/normalization.js';
|
||||
import { ICSSDevelopmentService, CSSDevelopmentService } from '../../platform/cssDev/node/cssDevService.js';
|
||||
import { ExtensionSignatureVerificationService, IExtensionSignatureVerificationService } from '../../platform/extensionManagement/node/extensionSignatureVerificationService.js';
|
||||
|
||||
import { LLMMessageChannel } from '../../platform/void/electron-main/llmMessageChannel.js';
|
||||
import { IMetricsService } from '../../platform/void/common/metricsService.js';
|
||||
import { MetricsMainService } from '../../platform/void/electron-main/metricsMainService.js';
|
||||
|
||||
import { LLMMessageChannel } from '../../workbench/contrib/void/electron-main/llmMessageChannel.js';
|
||||
import { IMetricsService } from '../../workbench/contrib/void/common/metricsService.js';
|
||||
import { MetricsMainService } from '../../workbench/contrib/void/electron-main/metricsMainService.js';
|
||||
import { VoidMainUpdateService } from '../../workbench/contrib/void/electron-main/voidUpdateMainService.js';
|
||||
import { IVoidUpdateService } from '../../workbench/contrib/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);
|
||||
|
||||
|
||||
@@ -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<TextEdit[]> {
|
||||
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;
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { safeStorage as safeStorageElectron, app } from 'electron';
|
||||
import { isMacintosh, isWindows } from '../../../base/common/platform.js';
|
||||
import { isMacintosh, isWindows, isLinux } from '../../../base/common/platform.js';
|
||||
import { KnownStorageProvider, IEncryptionMainService, PasswordStoreCLIOption } from '../common/encryptionService.js';
|
||||
import { ILogService } from '../../log/common/log.js';
|
||||
|
||||
@@ -23,6 +23,11 @@ export class EncryptionMainService implements IEncryptionMainService {
|
||||
constructor(
|
||||
@ILogService private readonly logService: ILogService
|
||||
) {
|
||||
if (isLinux && !app.commandLine.getSwitchValue('password-store')) {
|
||||
this.logService.trace('[EncryptionMainService] No password-store switch, defaulting to basic...');
|
||||
app.commandLine.appendSwitch('password-store', PasswordStoreCLIOption.basic);
|
||||
}
|
||||
|
||||
// if this commandLine switch is set, the user has opted in to using basic text encryption
|
||||
if (app.commandLine.getSwitchValue('password-store') === PasswordStoreCLIOption.basic) {
|
||||
this.logService.trace('[EncryptionMainService] setting usePlainTextEncryption to true...');
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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<E extends ClassifiedEvent<OmitMetadata<T>> = never, T extends IGDPRProperty = never>(eventName: string, data?: StrictPropertyCheck<T, E>) {
|
||||
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<E extends ClassifiedEvent<OmitMetadata<T>> = never, T extends IGDPRProperty = never>(eventName: string, data?: StrictPropertyCheck<T, E>) {
|
||||
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<void> {
|
||||
|
||||
@@ -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<string, any>, this._cleanupPatterns);
|
||||
// // remove all PII from data
|
||||
// data = cleanData(data as Record<string, any>, 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<E extends ClassifiedEvent<OmitMetadata<T>> = never, T extends IGDPRProperty = never>(eventName: string, data?: StrictPropertyCheck<T, E>) {
|
||||
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<E extends ClassifiedEvent<OmitMetadata<T>> = never, T extends IGDPRProperty = never>(eventName: string, data?: StrictPropertyCheck<T, E>) {
|
||||
this.publicLogError(eventName, data as ITelemetryData);
|
||||
// Void commented this out
|
||||
// this.publicLogError(eventName, data as ITelemetryData);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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';
|
||||
@@ -16,7 +17,9 @@ import { AvailableForDownload, DisablementReason, IUpdateService, State, StateTy
|
||||
|
||||
export function createUpdateURL(platform: string, quality: string, productService: IProductService): string {
|
||||
// return `https://voideditor.dev/api/update/${platform}/stable`;
|
||||
return `${productService.updateUrl}/api/update/${platform}/${quality}/${productService.commit}`;
|
||||
// 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 = {
|
||||
@@ -58,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());
|
||||
@@ -77,71 +80,29 @@ export abstract class AbstractUpdateService implements IUpdateService {
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
// Void - for now, always update
|
||||
|
||||
const updateMode = this.configurationService.getValue<'none' | 'manual' | 'start' | 'default'>('update.mode');
|
||||
|
||||
const quality = this.getProductQuality(updateMode);
|
||||
if (!quality) {
|
||||
this.setState(State.Disabled(DisablementReason.ManuallyDisabled));
|
||||
this.logService.info('update#ctor - updates are disabled by user preference');
|
||||
return;
|
||||
}
|
||||
|
||||
// const quality = 'stable'
|
||||
this.url = this.doBuildUpdateFeedUrl(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<boolean>('_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 async scheduleCheckForUpdates(delay = 60 * 60 * 1000): Promise<void> {
|
||||
await timeout(delay);
|
||||
await this.checkForUpdates(false);
|
||||
return await this.scheduleCheckForUpdates(60 * 60 * 1000);
|
||||
}
|
||||
// private async scheduleCheckForUpdates(delay = 60 * 60 * 1000): Promise<void> {
|
||||
// await timeout(delay);
|
||||
// await this.checkForUpdates(false);
|
||||
// return await this.scheduleCheckForUpdates(60 * 60 * 1000);
|
||||
// }
|
||||
|
||||
async checkForUpdates(explicit: boolean): Promise<void> {
|
||||
this.logService.trace('update#checkForUpdates, state = ', this.state.type);
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Glass Devtools, Inc. All rights reserved.
|
||||
* Void Editor additions licensed under the AGPL 3.0 License.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
// ---------- common ----------
|
||||
|
||||
// llmMessage
|
||||
import '../common/llmMessageService.js'
|
||||
|
||||
// voidSettings
|
||||
import '../common/voidSettingsService.js'
|
||||
|
||||
// refreshModel
|
||||
import '../common/refreshModelService.js'
|
||||
|
||||
// metrics
|
||||
import '../common/metricsService.js'
|
||||
@@ -1,174 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Glass Devtools, Inc. All rights reserved.
|
||||
* Void Editor additions licensed under the AGPL 3.0 License.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { EventLLMMessageOnTextParams, EventLLMMessageOnErrorParams, EventLLMMessageOnFinalMessageParams, ServiceSendLLMMessageParams, MainLLMMessageParams, MainLLMMessageAbortParams, ServiceModelListParams, EventModelListOnSuccessParams, EventModelListOnErrorParams, MainModelListParams, OllamaModelResponse, OpenaiCompatibleModelResponse, } from './llmMessageTypes.js';
|
||||
import { IChannel } from '../../../base/parts/ipc/common/ipc.js';
|
||||
import { IMainProcessService } from '../../ipc/common/mainProcessService.js';
|
||||
import { InstantiationType, registerSingleton } from '../../instantiation/common/extensions.js';
|
||||
import { generateUuid } from '../../../base/common/uuid.js';
|
||||
import { createDecorator } from '../../instantiation/common/instantiation.js';
|
||||
import { Event } from '../../../base/common/event.js';
|
||||
import { Disposable } from '../../../base/common/lifecycle.js';
|
||||
import { IVoidSettingsService } from './voidSettingsService.js';
|
||||
// import { INotificationService } from '../../notification/common/notification.js';
|
||||
|
||||
// calls channel to implement features
|
||||
export const ILLMMessageService = createDecorator<ILLMMessageService>('llmMessageService');
|
||||
|
||||
export interface ILLMMessageService {
|
||||
readonly _serviceBrand: undefined;
|
||||
sendLLMMessage: (params: ServiceSendLLMMessageParams) => string | null;
|
||||
abort: (requestId: string) => void;
|
||||
ollamaList: (params: ServiceModelListParams<OllamaModelResponse>) => void;
|
||||
openAICompatibleList: (params: ServiceModelListParams<OpenaiCompatibleModelResponse>) => void;
|
||||
}
|
||||
|
||||
export class LLMMessageService extends Disposable implements ILLMMessageService {
|
||||
|
||||
readonly _serviceBrand: undefined;
|
||||
private readonly channel: IChannel // LLMMessageChannel
|
||||
|
||||
// llmMessage
|
||||
private readonly onTextHooks_llm: { [eventId: string]: ((params: EventLLMMessageOnTextParams) => void) } = {}
|
||||
private readonly onFinalMessageHooks_llm: { [eventId: string]: ((params: EventLLMMessageOnFinalMessageParams) => void) } = {}
|
||||
private readonly onErrorHooks_llm: { [eventId: string]: ((params: EventLLMMessageOnErrorParams) => void) } = {}
|
||||
|
||||
|
||||
// ollamaList
|
||||
private readonly onSuccess_ollama: { [eventId: string]: ((params: EventModelListOnSuccessParams<OllamaModelResponse>) => void) } = {}
|
||||
private readonly onError_ollama: { [eventId: string]: ((params: EventModelListOnErrorParams<OllamaModelResponse>) => void) } = {}
|
||||
|
||||
// openAICompatibleList
|
||||
private readonly onSuccess_openAICompatible: { [eventId: string]: ((params: EventModelListOnSuccessParams<OpenaiCompatibleModelResponse>) => void) } = {}
|
||||
private readonly onError_openAICompatible: { [eventId: string]: ((params: EventModelListOnErrorParams<OpenaiCompatibleModelResponse>) => void) } = {}
|
||||
|
||||
constructor(
|
||||
@IMainProcessService private readonly mainProcessService: IMainProcessService, // used as a renderer (only usable on client side)
|
||||
@IVoidSettingsService private readonly voidSettingsService: IVoidSettingsService,
|
||||
// @INotificationService private readonly notificationService: INotificationService,
|
||||
) {
|
||||
super()
|
||||
|
||||
// const service = ProxyChannel.toService<LLMMessageChannel>(mainProcessService.getChannel('void-channel-sendLLMMessage')); // lets you call it like a service
|
||||
this.channel = this.mainProcessService.getChannel('void-channel-llmMessageService')
|
||||
|
||||
// .listen sets up an IPC channel and takes a few ms, so we set up listeners immediately and add hooks to them instead
|
||||
// llm
|
||||
this._register((this.channel.listen('onText_llm') satisfies Event<EventLLMMessageOnTextParams>)(e => {
|
||||
this.onTextHooks_llm[e.requestId]?.(e)
|
||||
}))
|
||||
this._register((this.channel.listen('onFinalMessage_llm') satisfies Event<EventLLMMessageOnFinalMessageParams>)(e => {
|
||||
this.onFinalMessageHooks_llm[e.requestId]?.(e)
|
||||
this._onRequestIdDone(e.requestId)
|
||||
}))
|
||||
this._register((this.channel.listen('onError_llm') satisfies Event<EventLLMMessageOnErrorParams>)(e => {
|
||||
console.log('Error in LLMMessageService:', JSON.stringify(e))
|
||||
this.onErrorHooks_llm[e.requestId]?.(e)
|
||||
this._onRequestIdDone(e.requestId)
|
||||
}))
|
||||
// ollama
|
||||
this._register((this.channel.listen('onSuccess_ollama') satisfies Event<EventModelListOnSuccessParams<OllamaModelResponse>>)(e => {
|
||||
this.onSuccess_ollama[e.requestId]?.(e)
|
||||
}))
|
||||
this._register((this.channel.listen('onError_ollama') satisfies Event<EventModelListOnErrorParams<OllamaModelResponse>>)(e => {
|
||||
this.onError_ollama[e.requestId]?.(e)
|
||||
}))
|
||||
// openaiCompatible
|
||||
this._register((this.channel.listen('onSuccess_openAICompatible') satisfies Event<EventModelListOnSuccessParams<OpenaiCompatibleModelResponse>>)(e => {
|
||||
this.onSuccess_openAICompatible[e.requestId]?.(e)
|
||||
}))
|
||||
this._register((this.channel.listen('onError_openAICompatible') satisfies Event<EventModelListOnErrorParams<OpenaiCompatibleModelResponse>>)(e => {
|
||||
this.onError_openAICompatible[e.requestId]?.(e)
|
||||
}))
|
||||
|
||||
}
|
||||
|
||||
sendLLMMessage(params: ServiceSendLLMMessageParams) {
|
||||
const { onText, onFinalMessage, onError, ...proxyParams } = params;
|
||||
const { featureName } = proxyParams
|
||||
|
||||
// end early if no provider
|
||||
const modelSelection = this.voidSettingsService.state.modelSelectionOfFeature[featureName]
|
||||
if (modelSelection === null) {
|
||||
onError({ message: 'Please add a Provider in Settings!', fullError: null })
|
||||
return null
|
||||
}
|
||||
const { providerName, modelName } = modelSelection
|
||||
|
||||
// add state for request id
|
||||
const requestId_ = generateUuid();
|
||||
this.onTextHooks_llm[requestId_] = onText
|
||||
this.onFinalMessageHooks_llm[requestId_] = onFinalMessage
|
||||
this.onErrorHooks_llm[requestId_] = onError
|
||||
|
||||
const { settingsOfProvider } = this.voidSettingsService.state
|
||||
|
||||
// params will be stripped of all its functions over the IPC channel
|
||||
this.channel.call('sendLLMMessage', {
|
||||
...proxyParams,
|
||||
requestId: requestId_,
|
||||
providerName,
|
||||
modelName,
|
||||
settingsOfProvider,
|
||||
} satisfies MainLLMMessageParams);
|
||||
|
||||
return requestId_
|
||||
}
|
||||
|
||||
|
||||
abort(requestId: string) {
|
||||
this.channel.call('abort', { requestId } satisfies MainLLMMessageAbortParams);
|
||||
this._onRequestIdDone(requestId)
|
||||
}
|
||||
|
||||
|
||||
ollamaList = (params: ServiceModelListParams<OllamaModelResponse>) => {
|
||||
const { onSuccess, onError, ...proxyParams } = params
|
||||
|
||||
const { settingsOfProvider } = this.voidSettingsService.state
|
||||
|
||||
// add state for request id
|
||||
const requestId_ = generateUuid();
|
||||
this.onSuccess_ollama[requestId_] = onSuccess
|
||||
this.onError_ollama[requestId_] = onError
|
||||
|
||||
this.channel.call('ollamaList', {
|
||||
...proxyParams,
|
||||
settingsOfProvider,
|
||||
requestId: requestId_,
|
||||
} satisfies MainModelListParams<OllamaModelResponse>)
|
||||
}
|
||||
|
||||
openAICompatibleList = (params: ServiceModelListParams<OpenaiCompatibleModelResponse>) => {
|
||||
const { onSuccess, onError, ...proxyParams } = params
|
||||
|
||||
const { settingsOfProvider } = this.voidSettingsService.state
|
||||
|
||||
// add state for request id
|
||||
const requestId_ = generateUuid();
|
||||
this.onSuccess_openAICompatible[requestId_] = onSuccess
|
||||
this.onError_openAICompatible[requestId_] = onError
|
||||
|
||||
this.channel.call('openAICompatibleList', {
|
||||
...proxyParams,
|
||||
settingsOfProvider,
|
||||
requestId: requestId_,
|
||||
} satisfies MainModelListParams<OpenaiCompatibleModelResponse>)
|
||||
}
|
||||
|
||||
|
||||
|
||||
_onRequestIdDone(requestId: string) {
|
||||
delete this.onTextHooks_llm[requestId]
|
||||
delete this.onFinalMessageHooks_llm[requestId]
|
||||
delete this.onErrorHooks_llm[requestId]
|
||||
|
||||
delete this.onSuccess_ollama[requestId]
|
||||
delete this.onError_ollama[requestId]
|
||||
}
|
||||
}
|
||||
|
||||
registerSingleton(ILLMMessageService, LLMMessageService, InstantiationType.Eager);
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Glass Devtools, Inc. All rights reserved.
|
||||
* Void Editor additions licensed under the AGPL 3.0 License.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { createDecorator } from '../../instantiation/common/instantiation.js';
|
||||
import { ProxyChannel } from '../../../base/parts/ipc/common/ipc.js';
|
||||
import { IMainProcessService } from '../../ipc/common/mainProcessService.js';
|
||||
import { InstantiationType, registerSingleton } from '../../instantiation/common/extensions.js';
|
||||
|
||||
export interface IMetricsService {
|
||||
readonly _serviceBrand: undefined;
|
||||
capture(event: string, params: Record<string, any>): void;
|
||||
}
|
||||
|
||||
export const IMetricsService = createDecorator<IMetricsService>('metricsService');
|
||||
|
||||
|
||||
// implemented by calling channel
|
||||
export class MetricsService implements IMetricsService {
|
||||
|
||||
readonly _serviceBrand: undefined;
|
||||
private readonly metricsService: IMetricsService;
|
||||
|
||||
constructor(
|
||||
@IMainProcessService mainProcessService: IMainProcessService // (only usable on client side)
|
||||
) {
|
||||
this.metricsService = ProxyChannel.toService<IMetricsService>(mainProcessService.getChannel('void-channel-metrics'));
|
||||
}
|
||||
|
||||
// call capture on the channel
|
||||
capture(...params: Parameters<IMetricsService['capture']>) {
|
||||
this.metricsService.capture(...params);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
registerSingleton(IMetricsService, MetricsService, InstantiationType.Eager);
|
||||
|
||||
@@ -1,268 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Glass Devtools, Inc. All rights reserved.
|
||||
* Void Editor additions licensed under the AGPL 3.0 License.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Emitter, Event } from '../../../base/common/event.js';
|
||||
import { Disposable } from '../../../base/common/lifecycle.js';
|
||||
import { deepClone } from '../../../base/common/objects.js';
|
||||
import { IEncryptionService } from '../../encryption/common/encryptionService.js';
|
||||
import { registerSingleton, InstantiationType } from '../../instantiation/common/extensions.js';
|
||||
import { createDecorator } from '../../instantiation/common/instantiation.js';
|
||||
import { IStorageService, StorageScope, StorageTarget } from '../../storage/common/storage.js';
|
||||
import { defaultSettingsOfProvider, FeatureName, ProviderName, ModelSelectionOfFeature, SettingsOfProvider, SettingName, providerNames, ModelSelection, modelSelectionsEqual, featureNames, modelInfoOfDefaultNames, VoidModelInfo, FeatureFlagSettings, FeatureFlagName, defaultFeatureFlagSettings } from './voidSettingsTypes.js';
|
||||
|
||||
|
||||
const STORAGE_KEY = 'void.voidSettingsStorage'
|
||||
|
||||
type SetSettingOfProviderFn = <S extends SettingName>(
|
||||
providerName: ProviderName,
|
||||
settingName: S,
|
||||
newVal: SettingsOfProvider[ProviderName][S extends keyof SettingsOfProvider[ProviderName] ? S : never],
|
||||
) => Promise<void>;
|
||||
|
||||
type SetModelSelectionOfFeatureFn = <K extends FeatureName>(
|
||||
featureName: K,
|
||||
newVal: ModelSelectionOfFeature[K],
|
||||
options?: { doNotApplyEffects?: true }
|
||||
) => Promise<void>;
|
||||
|
||||
type SetFeatureFlagFn = (flagName: FeatureFlagName, newVal: boolean) => void;
|
||||
|
||||
export type ModelOption = { text: string, value: ModelSelection }
|
||||
|
||||
|
||||
|
||||
export type VoidSettingsState = {
|
||||
readonly settingsOfProvider: SettingsOfProvider; // optionsOfProvider
|
||||
readonly modelSelectionOfFeature: ModelSelectionOfFeature; // stateOfFeature
|
||||
readonly featureFlagSettings: FeatureFlagSettings;
|
||||
|
||||
readonly _modelOptions: ModelOption[] // computed based on the two above items
|
||||
}
|
||||
|
||||
type EventProp = Exclude<keyof VoidSettingsState, '_modelOptions'> | 'all'
|
||||
|
||||
|
||||
export interface IVoidSettingsService {
|
||||
readonly _serviceBrand: undefined;
|
||||
readonly state: VoidSettingsState; // in order to play nicely with react, you should immutably change state
|
||||
readonly waitForInitState: Promise<void>;
|
||||
|
||||
onDidChangeState: Event<EventProp>;
|
||||
|
||||
setSettingOfProvider: SetSettingOfProviderFn;
|
||||
setModelSelectionOfFeature: SetModelSelectionOfFeatureFn;
|
||||
setFeatureFlag: SetFeatureFlagFn;
|
||||
|
||||
setDefaultModels(providerName: ProviderName, modelNames: string[]): void;
|
||||
toggleModelHidden(providerName: ProviderName, modelName: string): void;
|
||||
addModel(providerName: ProviderName, modelName: string): void;
|
||||
deleteModel(providerName: ProviderName, modelName: string): boolean;
|
||||
}
|
||||
|
||||
|
||||
let _computeModelOptions = (settingsOfProvider: SettingsOfProvider) => {
|
||||
let modelOptions: ModelOption[] = []
|
||||
for (const providerName of providerNames) {
|
||||
const providerConfig = settingsOfProvider[providerName]
|
||||
if (!providerConfig._enabled) continue // if disabled, don't display model options
|
||||
for (const { modelName, isHidden } of providerConfig.models) {
|
||||
if (isHidden) continue
|
||||
modelOptions.push({ text: `${modelName} (${providerName})`, value: { providerName, modelName } })
|
||||
}
|
||||
}
|
||||
return modelOptions
|
||||
}
|
||||
|
||||
|
||||
const defaultState = () => {
|
||||
const d: VoidSettingsState = {
|
||||
settingsOfProvider: deepClone(defaultSettingsOfProvider),
|
||||
modelSelectionOfFeature: { 'Ctrl+L': null, 'Ctrl+K': null, 'Autocomplete': null },
|
||||
featureFlagSettings: deepClone(defaultFeatureFlagSettings),
|
||||
_modelOptions: _computeModelOptions(defaultSettingsOfProvider), // computed
|
||||
}
|
||||
return d
|
||||
}
|
||||
|
||||
|
||||
export const IVoidSettingsService = createDecorator<IVoidSettingsService>('VoidSettingsService');
|
||||
class VoidSettingsService extends Disposable implements IVoidSettingsService {
|
||||
_serviceBrand: undefined;
|
||||
|
||||
private readonly _onDidChangeState = new Emitter<EventProp>();
|
||||
readonly onDidChangeState: Event<EventProp> = this._onDidChangeState.event; // this is primarily for use in react, so react can listen + update on state changes
|
||||
|
||||
state: VoidSettingsState;
|
||||
waitForInitState: Promise<void> // await this if you need a valid state initially
|
||||
|
||||
constructor(
|
||||
@IStorageService private readonly _storageService: IStorageService,
|
||||
@IEncryptionService private readonly _encryptionService: IEncryptionService,
|
||||
// could have used this, but it's clearer the way it is (+ slightly different eg StorageTarget.USER)
|
||||
// @ISecretStorageService private readonly _secretStorageService: ISecretStorageService,
|
||||
) {
|
||||
super()
|
||||
|
||||
// at the start, we haven't read the partial config yet, but we need to set state to something
|
||||
this.state = defaultState()
|
||||
|
||||
let resolver: () => void = () => { }
|
||||
this.waitForInitState = new Promise((res, rej) => resolver = res)
|
||||
|
||||
// read and update the actual state immediately
|
||||
this._readState().then(s => {
|
||||
this.state = s
|
||||
resolver()
|
||||
this._onDidChangeState.fire('all')
|
||||
})
|
||||
}
|
||||
|
||||
private async _readState(): Promise<VoidSettingsState> {
|
||||
const encryptedState = this._storageService.get(STORAGE_KEY, StorageScope.APPLICATION)
|
||||
|
||||
if (!encryptedState)
|
||||
return defaultState()
|
||||
|
||||
const stateStr = await this._encryptionService.decrypt(encryptedState)
|
||||
return JSON.parse(stateStr)
|
||||
}
|
||||
|
||||
|
||||
private async _storeState() {
|
||||
const state = this.state
|
||||
const encryptedState = await this._encryptionService.encrypt(JSON.stringify(state))
|
||||
this._storageService.store(STORAGE_KEY, encryptedState, StorageScope.APPLICATION, StorageTarget.USER);
|
||||
}
|
||||
|
||||
setSettingOfProvider: SetSettingOfProviderFn = async (providerName, settingName, newVal) => {
|
||||
|
||||
const newModelSelectionOfFeature = this.state.modelSelectionOfFeature
|
||||
|
||||
const newSettingsOfProvider = {
|
||||
...this.state.settingsOfProvider,
|
||||
[providerName]: {
|
||||
...this.state.settingsOfProvider[providerName],
|
||||
[settingName]: newVal,
|
||||
}
|
||||
}
|
||||
|
||||
const newFeatureFlags = this.state.featureFlagSettings
|
||||
|
||||
// if changed models or enabled a provider, recompute models list
|
||||
const modelsListChanged = settingName === 'models' || settingName === '_enabled'
|
||||
const newModelsList = modelsListChanged ? _computeModelOptions(newSettingsOfProvider) : this.state._modelOptions
|
||||
|
||||
const newState: VoidSettingsState = {
|
||||
modelSelectionOfFeature: newModelSelectionOfFeature,
|
||||
settingsOfProvider: newSettingsOfProvider,
|
||||
featureFlagSettings: newFeatureFlags,
|
||||
_modelOptions: newModelsList,
|
||||
}
|
||||
|
||||
// this must go above this.setanythingelse()
|
||||
this.state = newState
|
||||
|
||||
// if the user-selected model is no longer in the list, update the selection for each feature that needs it to something relevant (the 0th model available, or null)
|
||||
if (modelsListChanged) {
|
||||
for (const featureName of featureNames) {
|
||||
|
||||
const currentSelection = newModelSelectionOfFeature[featureName]
|
||||
const selnIdx = currentSelection === null ? -1 : newModelsList.findIndex(m => modelSelectionsEqual(m.value, currentSelection))
|
||||
|
||||
if (selnIdx === -1) {
|
||||
if (newModelsList.length !== 0)
|
||||
this.setModelSelectionOfFeature(featureName, newModelsList[0].value, { doNotApplyEffects: true })
|
||||
else
|
||||
this.setModelSelectionOfFeature(featureName, null, { doNotApplyEffects: true })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await this._storeState()
|
||||
this._onDidChangeState.fire('settingsOfProvider')
|
||||
}
|
||||
|
||||
|
||||
setFeatureFlag: SetFeatureFlagFn = async (flagName, newVal) => {
|
||||
const newState = {
|
||||
...this.state,
|
||||
featureFlagSettings: {
|
||||
...this.state.featureFlagSettings,
|
||||
[flagName]: newVal
|
||||
}
|
||||
}
|
||||
this.state = newState
|
||||
await this._storeState()
|
||||
this._onDidChangeState.fire('featureFlagSettings')
|
||||
|
||||
}
|
||||
|
||||
|
||||
setModelSelectionOfFeature: SetModelSelectionOfFeatureFn = async (featureName, newVal, options) => {
|
||||
const newState: VoidSettingsState = {
|
||||
...this.state,
|
||||
modelSelectionOfFeature: {
|
||||
...this.state.modelSelectionOfFeature,
|
||||
[featureName]: newVal
|
||||
}
|
||||
}
|
||||
|
||||
this.state = newState
|
||||
|
||||
if (options?.doNotApplyEffects)
|
||||
return
|
||||
|
||||
await this._storeState()
|
||||
this._onDidChangeState.fire('modelSelectionOfFeature')
|
||||
}
|
||||
|
||||
|
||||
|
||||
setDefaultModels(providerName: ProviderName, newDefaultModelNames: string[]) {
|
||||
const { models } = this.state.settingsOfProvider[providerName]
|
||||
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)
|
||||
}
|
||||
toggleModelHidden(providerName: ProviderName, modelName: string) {
|
||||
const { models } = this.state.settingsOfProvider[providerName]
|
||||
const modelIdx = models.findIndex(m => m.modelName === modelName)
|
||||
if (modelIdx === -1) return
|
||||
const newModels: VoidModelInfo[] = [
|
||||
...models.slice(0, modelIdx),
|
||||
{ ...models[modelIdx], isHidden: !models[modelIdx].isHidden },
|
||||
...models.slice(modelIdx + 1, Infinity)
|
||||
]
|
||||
this.setSettingOfProvider(providerName, 'models', newModels)
|
||||
}
|
||||
addModel(providerName: ProviderName, modelName: string) {
|
||||
const { models } = this.state.settingsOfProvider[providerName]
|
||||
const existingIdx = models.findIndex(m => m.modelName === modelName)
|
||||
if (existingIdx !== -1) return // if exists, do nothing
|
||||
const newModels = [
|
||||
...models,
|
||||
{ modelName, isDefault: false, isHidden: false }
|
||||
]
|
||||
this.setSettingOfProvider(providerName, 'models', newModels)
|
||||
}
|
||||
deleteModel(providerName: ProviderName, modelName: string): boolean {
|
||||
const { models } = this.state.settingsOfProvider[providerName]
|
||||
const delIdx = models.findIndex(m => m.modelName === modelName)
|
||||
if (delIdx === -1) return false
|
||||
const newModels = [
|
||||
...models.slice(0, delIdx), // delete the idx
|
||||
...models.slice(delIdx + 1, Infinity)
|
||||
]
|
||||
this.setSettingOfProvider(providerName, 'models', newModels)
|
||||
return true
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
registerSingleton(IVoidSettingsService, VoidSettingsService, InstantiationType.Eager);
|
||||
@@ -1,415 +0,0 @@
|
||||
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Glass Devtools, Inc. All rights reserved.
|
||||
* Void Editor additions licensed under the AGPL 3.0 License.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
|
||||
|
||||
|
||||
export type VoidModelInfo = {
|
||||
modelName: string,
|
||||
isDefault: boolean, // whether or not it's a default for its provider
|
||||
isHidden: boolean, // whether or not the user is hiding it
|
||||
isAutodetected?: boolean, // whether the model was autodetected by polling
|
||||
}
|
||||
|
||||
type ModelInfoOfDefaultNamesOptions = { isAutodetected: true, existingModels: VoidModelInfo[] } // | { isOtherOption: true, ...otherOptions }
|
||||
export const modelInfoOfDefaultNames = (modelNames: string[], options?: ModelInfoOfDefaultNamesOptions): VoidModelInfo[] => {
|
||||
|
||||
const { isAutodetected, existingModels } = options ?? {}
|
||||
const isDefault = true
|
||||
const isHidden = modelNames.length >= 10 // hide all models if there are a ton of them, and make user enable them individually
|
||||
|
||||
if (!existingModels) {
|
||||
|
||||
return modelNames.map((modelName, i) => ({ modelName, isDefault, isAutodetected, isHidden, }))
|
||||
|
||||
} else {
|
||||
// keep existing `isHidden` property
|
||||
|
||||
const existingModelsMap: Record<string, VoidModelInfo> = {}
|
||||
for (const em of existingModels) {
|
||||
existingModelsMap[em.modelName] = em
|
||||
}
|
||||
|
||||
return modelNames.map((modelName, i) => ({ modelName, isDefault, isAutodetected, isHidden: !!existingModelsMap[modelName]?.isHidden, }))
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// https://docs.anthropic.com/en/docs/about-claude/models
|
||||
export const defaultAnthropicModels = modelInfoOfDefaultNames([
|
||||
'claude-3-5-sonnet-20241022',
|
||||
'claude-3-5-haiku-20241022',
|
||||
'claude-3-opus-20240229',
|
||||
'claude-3-sonnet-20240229',
|
||||
// 'claude-3-haiku-20240307',
|
||||
])
|
||||
|
||||
|
||||
// https://platform.openai.com/docs/models/gp
|
||||
export const defaultOpenAIModels = modelInfoOfDefaultNames([
|
||||
'o1-preview',
|
||||
'o1-mini',
|
||||
'gpt-4o',
|
||||
'gpt-4o-mini',
|
||||
// 'gpt-4o-2024-05-13',
|
||||
// 'gpt-4o-2024-08-06',
|
||||
// 'gpt-4o-mini-2024-07-18',
|
||||
// 'gpt-4-turbo',
|
||||
// 'gpt-4-turbo-2024-04-09',
|
||||
// 'gpt-4-turbo-preview',
|
||||
// 'gpt-4-0125-preview',
|
||||
// 'gpt-4-1106-preview',
|
||||
// 'gpt-4',
|
||||
// 'gpt-4-0613',
|
||||
// 'gpt-3.5-turbo-0125',
|
||||
// 'gpt-3.5-turbo',
|
||||
// 'gpt-3.5-turbo-1106',
|
||||
])
|
||||
|
||||
|
||||
|
||||
// https://console.groq.com/docs/models
|
||||
export const defaultGroqModels = modelInfoOfDefaultNames([
|
||||
"mixtral-8x7b-32768",
|
||||
"llama2-70b-4096",
|
||||
"gemma-7b-it"
|
||||
])
|
||||
|
||||
|
||||
export const defaultGeminiModels = modelInfoOfDefaultNames([
|
||||
'gemini-1.5-flash',
|
||||
'gemini-1.5-pro',
|
||||
'gemini-1.5-flash-8b',
|
||||
'gemini-1.0-pro'
|
||||
])
|
||||
|
||||
|
||||
|
||||
// export const parseMaxTokensStr = (maxTokensStr: string) => {
|
||||
// // parse the string but only if the full string is a valid number, eg parseInt('100abc') should return NaN
|
||||
// const int = isNaN(Number(maxTokensStr)) ? undefined : parseInt(maxTokensStr)
|
||||
// if (Number.isNaN(int))
|
||||
// return undefined
|
||||
// return int
|
||||
// }
|
||||
|
||||
|
||||
|
||||
|
||||
export const anthropicMaxPossibleTokens = (modelName: string) => {
|
||||
if (modelName === 'claude-3-5-sonnet-20241022'
|
||||
|| modelName === 'claude-3-5-haiku-20241022')
|
||||
return 8192
|
||||
if (modelName === 'claude-3-opus-20240229'
|
||||
|| modelName === 'claude-3-sonnet-20240229'
|
||||
|| modelName === 'claude-3-haiku-20240307')
|
||||
return 4096
|
||||
return 1024 // return a reasonably small number if they're using a different model
|
||||
}
|
||||
|
||||
|
||||
type UnionOfKeys<T> = T extends T ? keyof T : never;
|
||||
|
||||
|
||||
|
||||
export const defaultProviderSettings = {
|
||||
anthropic: {
|
||||
apiKey: '',
|
||||
},
|
||||
openAI: {
|
||||
apiKey: '',
|
||||
},
|
||||
ollama: {
|
||||
endpoint: 'http://127.0.0.1:11434',
|
||||
},
|
||||
openRouter: {
|
||||
apiKey: '',
|
||||
},
|
||||
openAICompatible: {
|
||||
endpoint: '',
|
||||
apiKey: '',
|
||||
},
|
||||
gemini: {
|
||||
apiKey: '',
|
||||
},
|
||||
groq: {
|
||||
apiKey: ''
|
||||
}
|
||||
} as const
|
||||
|
||||
export type ProviderName = keyof typeof defaultProviderSettings
|
||||
export const providerNames = Object.keys(defaultProviderSettings) as ProviderName[]
|
||||
|
||||
export const localProviderNames = ['ollama', 'openAICompatible'] satisfies ProviderName[] // all local names
|
||||
export const nonlocalProviderNames = providerNames.filter((name) => !(localProviderNames as string[]).includes(name)) // all non-local names
|
||||
|
||||
type CustomSettingName = UnionOfKeys<typeof defaultProviderSettings[ProviderName]>
|
||||
type CustomProviderSettings<providerName extends ProviderName> = {
|
||||
[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, computed when user types in all fields
|
||||
models: VoidModelInfo[],
|
||||
}
|
||||
|
||||
export type SettingsForProvider<providerName extends ProviderName> = CustomProviderSettings<providerName> & CommonProviderSettings
|
||||
|
||||
// part of state
|
||||
export type SettingsOfProvider = {
|
||||
[providerName in ProviderName]: SettingsForProvider<providerName>
|
||||
}
|
||||
|
||||
|
||||
export type SettingName = keyof SettingsForProvider<ProviderName>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
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',
|
||||
|
||||
}
|
||||
}
|
||||
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}"`)
|
||||
}
|
||||
|
||||
type DisplayInfo = {
|
||||
title: string,
|
||||
placeholder: string,
|
||||
subTextMd?: string,
|
||||
}
|
||||
export const displayInfoOfSettingName = (providerName: ProviderName, settingName: SettingName): DisplayInfo => {
|
||||
if (settingName === 'apiKey') {
|
||||
return {
|
||||
title: 'API Key',
|
||||
placeholder: providerName === 'anthropic' ? 'sk-ant-key...' : // sk-ant-api03-key
|
||||
providerName === 'openAI' ? 'sk-proj-key...' :
|
||||
providerName === 'openRouter' ? 'sk-or-key...' : // sk-or-v1-key
|
||||
providerName === 'gemini' ? 'key...' :
|
||||
providerName === 'groq' ? 'gsk_key...' :
|
||||
providerName === 'openAICompatible' ? 'sk-key...' :
|
||||
'(never)',
|
||||
|
||||
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,
|
||||
}
|
||||
}
|
||||
else if (settingName === 'endpoint') {
|
||||
return {
|
||||
title: providerName === 'ollama' ? 'Endpoint' :
|
||||
providerName === 'openAICompatible' ? 'baseURL' // (do not include /chat/completions)
|
||||
: '(never)',
|
||||
|
||||
placeholder: providerName === 'ollama' ? defaultProviderSettings.ollama.endpoint
|
||||
: providerName === 'openAICompatible' ? 'https://my-website.com/v1'
|
||||
: '(never)',
|
||||
|
||||
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') {
|
||||
return {
|
||||
title: '(never)',
|
||||
placeholder: '(never)',
|
||||
}
|
||||
}
|
||||
else if (settingName === 'models') {
|
||||
return {
|
||||
title: '(never)',
|
||||
placeholder: '(never)',
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(`displayInfo: Unknown setting name: "${settingName}"`)
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
const defaultCustomSettings: Record<CustomSettingName, undefined> = {
|
||||
apiKey: undefined,
|
||||
endpoint: undefined,
|
||||
}
|
||||
|
||||
export const voidInitModelOptions = {
|
||||
anthropic: {
|
||||
models: defaultAnthropicModels,
|
||||
},
|
||||
openAI: {
|
||||
models: defaultOpenAIModels,
|
||||
},
|
||||
ollama: {
|
||||
models: [],
|
||||
},
|
||||
openRouter: {
|
||||
models: [], // any string
|
||||
},
|
||||
openAICompatible: {
|
||||
models: [],
|
||||
},
|
||||
gemini: {
|
||||
models: defaultGeminiModels,
|
||||
},
|
||||
groq: {
|
||||
models: defaultGroqModels,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
// used when waiting and for a type reference
|
||||
export const defaultSettingsOfProvider: SettingsOfProvider = {
|
||||
anthropic: {
|
||||
_enabled: undefined,
|
||||
...defaultCustomSettings,
|
||||
...defaultProviderSettings.anthropic,
|
||||
...voidInitModelOptions.anthropic,
|
||||
},
|
||||
openAI: {
|
||||
_enabled: undefined,
|
||||
...defaultCustomSettings,
|
||||
...defaultProviderSettings.openAI,
|
||||
...voidInitModelOptions.openAI,
|
||||
},
|
||||
gemini: {
|
||||
...defaultCustomSettings,
|
||||
...defaultProviderSettings.gemini,
|
||||
...voidInitModelOptions.gemini,
|
||||
_enabled: undefined,
|
||||
},
|
||||
groq: {
|
||||
...defaultCustomSettings,
|
||||
...defaultProviderSettings.groq,
|
||||
...voidInitModelOptions.groq,
|
||||
_enabled: undefined,
|
||||
},
|
||||
ollama: {
|
||||
...defaultCustomSettings,
|
||||
...defaultProviderSettings.ollama,
|
||||
...voidInitModelOptions.ollama,
|
||||
_enabled: undefined,
|
||||
},
|
||||
openRouter: {
|
||||
...defaultCustomSettings,
|
||||
...defaultProviderSettings.openRouter,
|
||||
...voidInitModelOptions.openRouter,
|
||||
_enabled: undefined,
|
||||
},
|
||||
openAICompatible: {
|
||||
...defaultCustomSettings,
|
||||
...defaultProviderSettings.openAICompatible,
|
||||
...voidInitModelOptions.openAICompatible,
|
||||
_enabled: undefined,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
export type ModelSelection = { providerName: ProviderName, modelName: string }
|
||||
|
||||
export const modelSelectionsEqual = (m1: ModelSelection, m2: ModelSelection) => {
|
||||
return m1.modelName === m2.modelName && m1.providerName === m2.providerName
|
||||
}
|
||||
|
||||
// this is a state
|
||||
export type ModelSelectionOfFeature = {
|
||||
'Ctrl+L': ModelSelection | null,
|
||||
'Ctrl+K': ModelSelection | null,
|
||||
'Autocomplete': ModelSelection | null,
|
||||
}
|
||||
export type FeatureName = keyof ModelSelectionOfFeature
|
||||
export const featureNames = ['Ctrl+L', 'Ctrl+K', 'Autocomplete'] as const
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// 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;
|
||||
}
|
||||
export const defaultFeatureFlagSettings: FeatureFlagSettings = {
|
||||
autoRefreshModels: true,
|
||||
}
|
||||
|
||||
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 detect local providers and models (${refreshableProviderNames.map(providerName => displayInfoOfProviderName(providerName).title).join(', ')}).`,
|
||||
}
|
||||
}
|
||||
throw new Error(`featureFlagInfo: Unknown feature flag: "${featureFlag}"`)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,71 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Glass Devtools, Inc. All rights reserved.
|
||||
* Void Editor additions licensed under the AGPL 3.0 License.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import Anthropic from '@anthropic-ai/sdk';
|
||||
import { _InternalSendLLMMessageFnType } from '../../common/llmMessageTypes.js';
|
||||
import { anthropicMaxPossibleTokens } from '../../common/voidSettingsTypes.js';
|
||||
|
||||
// Anthropic
|
||||
type LLMMessageAnthropic = {
|
||||
role: 'user' | 'assistant';
|
||||
content: string;
|
||||
}
|
||||
export const sendAnthropicMsg: _InternalSendLLMMessageFnType = ({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter }) => {
|
||||
|
||||
const thisConfig = settingsOfProvider.anthropic
|
||||
|
||||
const maxTokens = anthropicMaxPossibleTokens(modelName)
|
||||
if (maxTokens === undefined) {
|
||||
onError({ message: `Please set a value for Max Tokens.`, fullError: null })
|
||||
return
|
||||
}
|
||||
|
||||
const anthropic = new Anthropic({ apiKey: thisConfig.apiKey, dangerouslyAllowBrowser: true });
|
||||
|
||||
// find system messages and concatenate them
|
||||
const systemMessage = messages
|
||||
.filter(msg => msg.role === 'system')
|
||||
.map(msg => msg.content)
|
||||
.join('\n');
|
||||
|
||||
// remove system messages for Anthropic
|
||||
const anthropicMessages = messages.filter(msg => msg.role !== 'system') as LLMMessageAnthropic[]
|
||||
|
||||
|
||||
|
||||
const stream = anthropic.messages.stream({
|
||||
system: systemMessage,
|
||||
messages: anthropicMessages,
|
||||
model: modelName,
|
||||
max_tokens: maxTokens,
|
||||
});
|
||||
|
||||
|
||||
// when receive text
|
||||
stream.on('text', (newText, fullText) => {
|
||||
onText({ newText, fullText })
|
||||
})
|
||||
|
||||
// when we get the final message on this stream (or when error/fail)
|
||||
stream.on('finalMessage', (claude_response) => {
|
||||
// stringify the response's content
|
||||
const content = claude_response.content.map(c => c.type === 'text' ? c.text : c.type).join('\n');
|
||||
onFinalMessage({ fullText: content })
|
||||
})
|
||||
|
||||
stream.on('error', (error) => {
|
||||
// the most common error will be invalid API key (401), so we handle this with a nice message
|
||||
if (error instanceof Anthropic.APIError && error.status === 401) {
|
||||
onError({ message: 'Invalid API key.', fullError: error })
|
||||
}
|
||||
else {
|
||||
onError({ message: error + '', fullError: error }) // anthropic errors can be stringified nicely like this
|
||||
}
|
||||
})
|
||||
|
||||
// TODO need to test this to make sure it works, it might throw an error
|
||||
_setAborter(() => stream.controller.abort())
|
||||
|
||||
};
|
||||
@@ -1,53 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Glass Devtools, Inc. All rights reserved.
|
||||
* Void Editor additions licensed under the AGPL 3.0 License.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Content, GoogleGenerativeAI, GoogleGenerativeAIFetchError } from '@google/generative-ai';
|
||||
import { _InternalSendLLMMessageFnType } from '../../common/llmMessageTypes.js';
|
||||
|
||||
// Gemini
|
||||
export const sendGeminiMsg: _InternalSendLLMMessageFnType = async ({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter }) => {
|
||||
|
||||
let fullText = ''
|
||||
|
||||
const thisConfig = settingsOfProvider.gemini
|
||||
|
||||
const genAI = new GoogleGenerativeAI(thisConfig.apiKey);
|
||||
const model = genAI.getGenerativeModel({ model: modelName });
|
||||
|
||||
// remove system messages that get sent to Gemini
|
||||
// str of all system messages
|
||||
const systemMessage = messages
|
||||
.filter(msg => msg.role === 'system')
|
||||
.map(msg => msg.content)
|
||||
.join('\n');
|
||||
|
||||
// Convert messages to Gemini format
|
||||
const geminiMessages: Content[] = messages
|
||||
.filter(msg => msg.role !== 'system')
|
||||
.map((msg, i) => ({
|
||||
parts: [{ text: msg.content }],
|
||||
role: msg.role === 'assistant' ? 'model' : 'user'
|
||||
}))
|
||||
|
||||
model.generateContentStream({ contents: geminiMessages, systemInstruction: systemMessage, })
|
||||
.then(async response => {
|
||||
_setAborter(() => response.stream.return(fullText))
|
||||
|
||||
for await (const chunk of response.stream) {
|
||||
const newText = chunk.text();
|
||||
fullText += newText;
|
||||
onText({ newText, fullText });
|
||||
}
|
||||
onFinalMessage({ fullText });
|
||||
})
|
||||
.catch((error) => {
|
||||
if (error instanceof GoogleGenerativeAIFetchError && error.status === 400) {
|
||||
onError({ message: 'Invalid API key.', fullError: null });
|
||||
}
|
||||
else {
|
||||
onError({ message: error + '', fullError: error });
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Glass Devtools, Inc. All rights reserved.
|
||||
* Void Editor additions licensed under the AGPL 3.0 License.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
// // Greptile
|
||||
// // https://docs.greptile.com/api-reference/query
|
||||
// // https://docs.greptile.com/quickstart#sample-response-streamed
|
||||
|
||||
// import { SendLLMMessageFnTypeInternal } from '../../common/llmMessageTypes.js';
|
||||
|
||||
// export const sendGreptileMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinalMessage, onError, settingsOfProvider, _setAborter }) => {
|
||||
|
||||
// let fullText = ''
|
||||
|
||||
// const thisConfig = settingsOfProvider.greptile
|
||||
|
||||
// fetch('https://api.greptile.com/v2/query', {
|
||||
// method: 'POST',
|
||||
// headers: {
|
||||
// 'Authorization': `Bearer ${thisConfig.apikey}`,
|
||||
// 'X-Github-Token': `${thisConfig.githubPAT}`,
|
||||
// 'Content-Type': `application/json`,
|
||||
// },
|
||||
// body: JSON.stringify({
|
||||
// messages,
|
||||
// stream: true,
|
||||
// repositories: [thisConfig.repoinfo],
|
||||
// }),
|
||||
// })
|
||||
// // this is {message}\n{message}\n{message}...\n
|
||||
// .then(async response => {
|
||||
// const text = await response.text()
|
||||
// console.log('got greptile', text)
|
||||
// return JSON.parse(`[${text.trim().split('\n').join(',')}]`)
|
||||
// })
|
||||
// // TODO make this actually stream, right now it just sends one message at the end
|
||||
// // TODO add _setAborter() when add streaming
|
||||
// .then(async responseArr => {
|
||||
|
||||
// for (const response of responseArr) {
|
||||
// const type: string = response['type']
|
||||
// const message = response['message']
|
||||
|
||||
// // when receive text
|
||||
// if (type === 'message') {
|
||||
// fullText += message
|
||||
// onText({ newText: message, fullText })
|
||||
// }
|
||||
// else if (type === 'sources') {
|
||||
// const { filepath, linestart: _, lineend: _2 } = message as { filepath: string; linestart: number | null; lineend: number | null }
|
||||
// fullText += filepath
|
||||
// onText({ newText: filepath, fullText })
|
||||
// }
|
||||
// // type: 'status' with an empty 'message' means last message
|
||||
// else if (type === 'status') {
|
||||
// if (!message) {
|
||||
// onFinalMessage({ fullText })
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// })
|
||||
// .catch(error => {
|
||||
// onError({ error })
|
||||
// });
|
||||
|
||||
// }
|
||||
@@ -1,46 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Glass Devtools, Inc. All rights reserved.
|
||||
* Void Editor additions licensed under the AGPL 3.0 License.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import Groq from 'groq-sdk';
|
||||
import { _InternalSendLLMMessageFnType } from '../../common/llmMessageTypes.js';
|
||||
|
||||
// Groq
|
||||
export const sendGroqMsg: _InternalSendLLMMessageFnType = async ({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter }) => {
|
||||
let fullText = '';
|
||||
|
||||
const thisConfig = settingsOfProvider.groq
|
||||
|
||||
const groq = new Groq({
|
||||
apiKey: thisConfig.apiKey,
|
||||
dangerouslyAllowBrowser: true
|
||||
});
|
||||
|
||||
await groq.chat.completions
|
||||
.create({
|
||||
messages: messages,
|
||||
model: modelName,
|
||||
stream: true,
|
||||
temperature: 0.7,
|
||||
// max_tokens: parseMaxTokensStr(thisConfig.maxTokens),
|
||||
})
|
||||
.then(async response => {
|
||||
_setAborter(() => response.controller.abort())
|
||||
// when receive text
|
||||
for await (const chunk of response) {
|
||||
const newText = chunk.choices[0]?.delta?.content || '';
|
||||
if (newText) {
|
||||
fullText += newText;
|
||||
onText({ newText, fullText });
|
||||
}
|
||||
}
|
||||
|
||||
onFinalMessage({ fullText });
|
||||
})
|
||||
.catch(error => {
|
||||
onError({ message: error + '', fullError: error });
|
||||
})
|
||||
|
||||
|
||||
};
|
||||
@@ -1,86 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Glass Devtools, Inc. All rights reserved.
|
||||
* Void Editor additions licensed under the AGPL 3.0 License.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Ollama } from 'ollama';
|
||||
import { _InternalModelListFnType, _InternalSendLLMMessageFnType, OllamaModelResponse } from '../../common/llmMessageTypes.js';
|
||||
import { defaultProviderSettings } from '../../common/voidSettingsTypes.js';
|
||||
|
||||
export const ollamaList: _InternalModelListFnType<OllamaModelResponse> = async ({ onSuccess: onSuccess_, onError: onError_, settingsOfProvider }) => {
|
||||
|
||||
const onSuccess = ({ models }: { models: OllamaModelResponse[] }) => {
|
||||
onSuccess_({ models })
|
||||
}
|
||||
|
||||
const onError = ({ error }: { error: string }) => {
|
||||
onError_({ error })
|
||||
}
|
||||
|
||||
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) => {
|
||||
const { models } = response
|
||||
onSuccess({ models })
|
||||
})
|
||||
.catch((error) => {
|
||||
onError({ error: error + '' })
|
||||
})
|
||||
}
|
||||
catch (error) {
|
||||
onError({ error: error + '' })
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Ollama
|
||||
export const sendOllamaMsg: _InternalSendLLMMessageFnType = ({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter }) => {
|
||||
|
||||
const thisConfig = settingsOfProvider.ollama
|
||||
// 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 = ''
|
||||
|
||||
const ollama = new Ollama({ host: thisConfig.endpoint })
|
||||
|
||||
ollama.chat({
|
||||
model: modelName,
|
||||
messages: messages,
|
||||
stream: true,
|
||||
// options: { num_predict: parseMaxTokensStr(thisConfig.maxTokens) } // this is max_tokens
|
||||
})
|
||||
.then(async stream => {
|
||||
_setAborter(() => stream.abort())
|
||||
// iterate through the stream
|
||||
for await (const chunk of stream) {
|
||||
const newText = chunk.message.content;
|
||||
fullText += newText;
|
||||
onText({ newText, fullText });
|
||||
}
|
||||
onFinalMessage({ fullText });
|
||||
|
||||
})
|
||||
// when error/fail
|
||||
.catch((error) => {
|
||||
// if (typeof error === 'object') {
|
||||
// const e = error.error as ErrorResponse['error']
|
||||
// if (e) {
|
||||
// const name = error.name ?? 'Error'
|
||||
// onError({ error: `${name}: ${e}` })
|
||||
// return;
|
||||
// }
|
||||
// }
|
||||
onError({ message: error + '', fullError: error })
|
||||
})
|
||||
|
||||
};
|
||||
|
||||
|
||||
|
||||
// ['codestral', 'qwen2.5-coder', 'qwen2.5-coder:0.5b', 'qwen2.5-coder:1.5b', 'qwen2.5-coder:3b', 'qwen2.5-coder:7b', 'qwen2.5-coder:14b', 'qwen2.5-coder:32b', 'codegemma', 'codegemma:2b', 'codegemma:7b', 'codellama', 'codellama:7b', 'codellama:13b', 'codellama:34b', 'codellama:70b', 'codellama:code', 'codellama:python', 'command-r', 'command-r:35b', 'command-r-plus', 'command-r-plus:104b', 'deepseek-coder-v2', 'deepseek-coder-v2:16b', 'deepseek-coder-v2:236b', 'falcon2', 'falcon2:11b', 'firefunction-v2', 'firefunction-v2:70b', 'gemma', 'gemma:2b', 'gemma:7b', 'gemma2', 'gemma2:2b', 'gemma2:9b', 'gemma2:27b', 'llama2', 'llama2:7b', 'llama2:13b', 'llama2:70b', 'llama3', 'llama3:8b', 'llama3:70b', 'llama3-chatqa', 'llama3-chatqa:8b', 'llama3-chatqa:70b', 'llama3-gradient', 'llama3-gradient:8b', 'llama3-gradient:70b', 'llama3.1', 'llama3.1:8b', 'llama3.1:70b', 'llama3.1:405b', 'llava', 'llava:7b', 'llava:13b', 'llava:34b', 'llava-llama3', 'llava-llama3:8b', 'llava-phi3', 'llava-phi3:3.8b', 'mistral', 'mistral:7b', 'mistral-large', 'mistral-large:123b', 'mistral-nemo', 'mistral-nemo:12b', 'mixtral', 'mixtral:8x7b', 'mixtral:8x22b', 'moondream', 'moondream:1.8b', 'openhermes', 'openhermes:v2.5', 'phi3', 'phi3:3.8b', 'phi3:14b', 'phi3.5', 'phi3.5:3.8b', 'qwen', 'qwen:7b', 'qwen:14b', 'qwen:32b', 'qwen:72b', 'qwen:110b', 'qwen2', 'qwen2:0.5b', 'qwen2:1.5b', 'qwen2:7b', 'qwen2:72b', 'smollm', 'smollm:135m', 'smollm:360m', 'smollm:1.7b',]
|
||||
@@ -1,105 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Glass Devtools, Inc. All rights reserved.
|
||||
* Void Editor additions licensed under the AGPL 3.0 License.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import OpenAI from 'openai';
|
||||
import { _InternalModelListFnType, _InternalSendLLMMessageFnType } from '../../common/llmMessageTypes.js';
|
||||
import { Model } from 'openai/resources/models.js';
|
||||
// import { parseMaxTokensStr } from './util.js';
|
||||
|
||||
|
||||
|
||||
export const openaiCompatibleList: _InternalModelListFnType<Model> = async ({ onSuccess: onSuccess_, onError: onError_, settingsOfProvider }) => {
|
||||
const onSuccess = ({ models }: { models: Model[] }) => {
|
||||
onSuccess_({ models })
|
||||
}
|
||||
|
||||
const onError = ({ error }: { error: string }) => {
|
||||
onError_({ error })
|
||||
}
|
||||
|
||||
try {
|
||||
const thisConfig = settingsOfProvider.openAICompatible
|
||||
const openai = new OpenAI({ baseURL: thisConfig.endpoint, apiKey: thisConfig.apiKey, dangerouslyAllowBrowser: true })
|
||||
|
||||
openai.models.list()
|
||||
.then(async (response) => {
|
||||
const models: Model[] = []
|
||||
models.push(...response.data)
|
||||
while (response.hasNextPage()) {
|
||||
models.push(...(await response.getNextPage()).data)
|
||||
}
|
||||
onSuccess({ models })
|
||||
})
|
||||
.catch((error) => {
|
||||
onError({ error: error + '' })
|
||||
})
|
||||
}
|
||||
catch (error) {
|
||||
onError({ error: error + '' })
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
// OpenAI, OpenRouter, OpenAICompatible
|
||||
export const sendOpenAIMsg: _InternalSendLLMMessageFnType = ({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName }) => {
|
||||
|
||||
let fullText = ''
|
||||
|
||||
let openai: OpenAI
|
||||
let options: OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming
|
||||
|
||||
|
||||
if (providerName === 'openAI') {
|
||||
const thisConfig = settingsOfProvider.openAI
|
||||
openai = new OpenAI({ apiKey: thisConfig.apiKey, dangerouslyAllowBrowser: true });
|
||||
options = { model: modelName, messages: messages, stream: true, /*max_completion_tokens: parseMaxTokensStr(thisConfig.maxTokens)*/ }
|
||||
}
|
||||
else if (providerName === 'openRouter') {
|
||||
const thisConfig = settingsOfProvider.openRouter
|
||||
openai = new OpenAI({
|
||||
baseURL: 'https://openrouter.ai/api/v1', apiKey: thisConfig.apiKey, dangerouslyAllowBrowser: true,
|
||||
defaultHeaders: {
|
||||
'HTTP-Referer': 'https://voideditor.com', // Optional, for including your app on openrouter.ai rankings.
|
||||
'X-Title': 'Void Editor', // Optional. Shows in rankings on openrouter.ai.
|
||||
},
|
||||
});
|
||||
options = { model: modelName, messages: messages, stream: true, /*max_completion_tokens: parseMaxTokensStr(thisConfig.maxTokens)*/ }
|
||||
}
|
||||
else if (providerName === 'openAICompatible') {
|
||||
const thisConfig = settingsOfProvider.openAICompatible
|
||||
openai = new OpenAI({ baseURL: thisConfig.endpoint, apiKey: thisConfig.apiKey, dangerouslyAllowBrowser: true })
|
||||
options = { model: modelName, messages: messages, stream: true, /*max_completion_tokens: parseMaxTokensStr(thisConfig.maxTokens)*/ }
|
||||
}
|
||||
else {
|
||||
console.error(`sendOpenAIMsg: invalid providerName: ${providerName}`)
|
||||
throw new Error(`providerName was invalid: ${providerName}`)
|
||||
}
|
||||
|
||||
openai.models.list()
|
||||
openai.chat.completions
|
||||
.create(options)
|
||||
.then(async response => {
|
||||
_setAborter(() => response.controller.abort())
|
||||
// when receive text
|
||||
for await (const chunk of response) {
|
||||
const newText = chunk.choices[0]?.delta?.content || '';
|
||||
fullText += newText;
|
||||
onText({ newText, fullText });
|
||||
}
|
||||
onFinalMessage({ fullText });
|
||||
})
|
||||
// when error/fail - this catches errors of both .create() and .then(for await)
|
||||
.catch(error => {
|
||||
if (error instanceof OpenAI.APIError && error.status === 401) {
|
||||
onError({ message: 'Invalid API key.', fullError: error });
|
||||
}
|
||||
else {
|
||||
onError({ message: error, fullError: error });
|
||||
}
|
||||
})
|
||||
|
||||
};
|
||||
@@ -1,115 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Glass Devtools, Inc. All rights reserved.
|
||||
* Void Editor additions licensed under the AGPL 3.0 License.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { LLMMMessageParams, OnText, OnFinalMessage, OnError } from '../../common/llmMessageTypes.js';
|
||||
import { IMetricsService } from '../../common/metricsService.js';
|
||||
|
||||
import { sendAnthropicMsg } from './anthropic.js';
|
||||
import { sendOllamaMsg } from './ollama.js';
|
||||
import { sendOpenAIMsg } from './openai.js';
|
||||
import { sendGeminiMsg } from './gemini.js';
|
||||
import { sendGroqMsg } from './groq.js';
|
||||
|
||||
export const sendLLMMessage = ({
|
||||
messages,
|
||||
onText: onText_,
|
||||
onFinalMessage: onFinalMessage_,
|
||||
onError: onError_,
|
||||
abortRef: abortRef_,
|
||||
logging: { loggingName },
|
||||
settingsOfProvider,
|
||||
providerName,
|
||||
modelName,
|
||||
}: LLMMMessageParams,
|
||||
|
||||
metricsService: IMetricsService
|
||||
) => {
|
||||
|
||||
// trim message content (Anthropic and other providers give an error if there is trailing whitespace)
|
||||
messages = messages.map(m => ({ ...m, content: m.content.trim() }))
|
||||
|
||||
// only captures number of messages and message "shape", no actual code, instructions, prompts, etc
|
||||
const captureChatEvent = (eventId: string, extras?: object) => {
|
||||
metricsService.capture(eventId, {
|
||||
providerName,
|
||||
numMessages: messages?.length,
|
||||
messagesShape: messages?.map(msg => ({ role: msg.role, length: msg.content.length })),
|
||||
version: '2024-11-14',
|
||||
...extras,
|
||||
})
|
||||
}
|
||||
const submit_time = new Date()
|
||||
|
||||
let _fullTextSoFar = ''
|
||||
let _aborter: (() => void) | null = null
|
||||
let _setAborter = (fn: () => void) => { _aborter = fn }
|
||||
let _didAbort = false
|
||||
|
||||
const onText: OnText = ({ newText, fullText }) => {
|
||||
if (_didAbort) return
|
||||
onText_({ newText, fullText })
|
||||
_fullTextSoFar = fullText
|
||||
}
|
||||
|
||||
const onFinalMessage: OnFinalMessage = ({ fullText }) => {
|
||||
if (_didAbort) return
|
||||
captureChatEvent(`${loggingName} - Received Full Message`, { messageLength: fullText.length, duration: new Date().getMilliseconds() - submit_time.getMilliseconds() })
|
||||
onFinalMessage_({ fullText })
|
||||
}
|
||||
|
||||
const onError: OnError = ({ message: error, fullError }) => {
|
||||
if (_didAbort) return
|
||||
console.log("ERROR!!!!!", error)
|
||||
console.error('sendLLMMessage onError:', error)
|
||||
captureChatEvent(`${loggingName} - Error`, { error })
|
||||
onError_({ message: error, fullError })
|
||||
}
|
||||
|
||||
const onAbort = () => {
|
||||
captureChatEvent(`${loggingName} - Abort`, { messageLengthSoFar: _fullTextSoFar.length })
|
||||
try { _aborter?.() } // aborter sometimes automatically throws an error
|
||||
catch (e) { }
|
||||
_didAbort = true
|
||||
}
|
||||
abortRef_.current = onAbort
|
||||
|
||||
captureChatEvent(`${loggingName} - Sending Message`, { messageLength: messages[messages.length - 1]?.content.length })
|
||||
|
||||
try {
|
||||
switch (providerName) {
|
||||
case 'anthropic':
|
||||
sendAnthropicMsg({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName });
|
||||
break;
|
||||
case 'openAI':
|
||||
case 'openRouter':
|
||||
case 'openAICompatible':
|
||||
sendOpenAIMsg({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName });
|
||||
break;
|
||||
case 'gemini':
|
||||
sendGeminiMsg({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName });
|
||||
break;
|
||||
case 'ollama':
|
||||
sendOllamaMsg({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName });
|
||||
break;
|
||||
case 'groq':
|
||||
sendGroqMsg({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName });
|
||||
break;
|
||||
default:
|
||||
onError({ message: `Error: Void provider was "${providerName}", which is not recognized.`, fullError: null })
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
catch (error) {
|
||||
if (error instanceof Error) { onError({ message: error + '', fullError: error }) }
|
||||
else { onError({ message: `Unexpected Error in sendLLMMessage: ${error}`, fullError: error }); }
|
||||
// ; (_aborter as any)?.()
|
||||
// _didAbort = true
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -1,140 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Glass Devtools, Inc. All rights reserved.
|
||||
* Void Editor additions licensed under the AGPL 3.0 License.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
// registered in app.ts
|
||||
// code convention is to make a service responsible for this stuff, and not a channel, but having fewer files is simpler...
|
||||
|
||||
import { IServerChannel } from '../../../base/parts/ipc/common/ipc.js';
|
||||
import { Emitter, Event } from '../../../base/common/event.js';
|
||||
import { EventLLMMessageOnTextParams, EventLLMMessageOnErrorParams, EventLLMMessageOnFinalMessageParams, MainLLMMessageParams, AbortRef, LLMMMessageParams, MainLLMMessageAbortParams, MainModelListParams, ModelListParams, EventModelListOnSuccessParams, EventModelListOnErrorParams, OllamaModelResponse, OpenaiCompatibleModelResponse, } from '../common/llmMessageTypes.js';
|
||||
import { sendLLMMessage } from './llmMessage/sendLLMMessage.js'
|
||||
import { IMetricsService } from '../common/metricsService.js';
|
||||
import { ollamaList } from './llmMessage/ollama.js';
|
||||
import { openaiCompatibleList } from './llmMessage/openai.js';
|
||||
|
||||
// NODE IMPLEMENTATION - calls actual sendLLMMessage() and returns listeners to it
|
||||
|
||||
export class LLMMessageChannel implements IServerChannel {
|
||||
// sendLLMMessage
|
||||
private readonly _onText_llm = new Emitter<EventLLMMessageOnTextParams>();
|
||||
private readonly _onFinalMessage_llm = new Emitter<EventLLMMessageOnFinalMessageParams>();
|
||||
private readonly _onError_llm = new Emitter<EventLLMMessageOnErrorParams>();
|
||||
|
||||
// abort
|
||||
private readonly _abortRefOfRequestId_llm: Record<string, AbortRef> = {}
|
||||
|
||||
// ollamaList
|
||||
private readonly _onSuccess_ollama = new Emitter<EventModelListOnSuccessParams<OllamaModelResponse>>();
|
||||
private readonly _onError_ollama = new Emitter<EventModelListOnErrorParams<OllamaModelResponse>>();
|
||||
|
||||
// openaiCompatibleList
|
||||
private readonly _onSuccess_openAICompatible = new Emitter<EventModelListOnSuccessParams<OpenaiCompatibleModelResponse>>();
|
||||
private readonly _onError_openAICompatible = new Emitter<EventModelListOnErrorParams<OpenaiCompatibleModelResponse>>();
|
||||
|
||||
// stupidly, channels can't take in @IService
|
||||
constructor(
|
||||
private readonly metricsService: IMetricsService,
|
||||
) { }
|
||||
|
||||
// browser uses this to listen for changes
|
||||
listen(_: unknown, event: string): Event<any> {
|
||||
if (event === 'onText_llm') {
|
||||
return this._onText_llm.event;
|
||||
}
|
||||
else if (event === 'onFinalMessage_llm') {
|
||||
return this._onFinalMessage_llm.event;
|
||||
}
|
||||
else if (event === 'onError_llm') {
|
||||
return this._onError_llm.event;
|
||||
}
|
||||
else if (event === 'onSuccess_ollama') {
|
||||
return this._onSuccess_ollama.event;
|
||||
}
|
||||
else if (event === 'onError_ollama') {
|
||||
return this._onError_ollama.event;
|
||||
}
|
||||
else if (event === 'onSuccess_openAICompatible') {
|
||||
return this._onSuccess_openAICompatible.event;
|
||||
}
|
||||
else if (event === 'onError_openAICompatible') {
|
||||
return this._onError_openAICompatible.event;
|
||||
}
|
||||
else {
|
||||
throw new Error(`Event not found: ${event}`);
|
||||
}
|
||||
}
|
||||
|
||||
// browser uses this to call
|
||||
async call(_: unknown, command: string, params: any): Promise<any> {
|
||||
try {
|
||||
if (command === 'sendLLMMessage') {
|
||||
this._callSendLLMMessage(params)
|
||||
}
|
||||
else if (command === 'abort') {
|
||||
this._callAbort(params)
|
||||
}
|
||||
else if (command === 'ollamaList') {
|
||||
this._callOllamaList(params)
|
||||
}
|
||||
else if (command === 'openAICompatibleList') {
|
||||
this._callOpenAICompatibleList(params)
|
||||
}
|
||||
else {
|
||||
throw new Error(`Void sendLLM: command "${command}" not recognized.`)
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
console.log('llmMessageChannel: Call Error:', e)
|
||||
}
|
||||
}
|
||||
|
||||
// the only place sendLLMMessage is actually called
|
||||
private async _callSendLLMMessage(params: MainLLMMessageParams) {
|
||||
const { requestId } = params;
|
||||
|
||||
if (!(requestId in this._abortRefOfRequestId_llm))
|
||||
this._abortRefOfRequestId_llm[requestId] = { current: null }
|
||||
|
||||
const mainThreadParams: LLMMMessageParams = {
|
||||
...params,
|
||||
onText: ({ newText, fullText }) => { this._onText_llm.fire({ requestId, newText, fullText }); },
|
||||
onFinalMessage: ({ fullText }) => { this._onFinalMessage_llm.fire({ requestId, fullText }); },
|
||||
onError: ({ message: error, fullError }) => { console.log('sendLLM: firing err'); this._onError_llm.fire({ requestId, message: error, fullError }); },
|
||||
abortRef: this._abortRefOfRequestId_llm[requestId],
|
||||
}
|
||||
sendLLMMessage(mainThreadParams, this.metricsService);
|
||||
}
|
||||
|
||||
private _callAbort(params: MainLLMMessageAbortParams) {
|
||||
const { requestId } = params;
|
||||
if (!(requestId in this._abortRefOfRequestId_llm)) return
|
||||
this._abortRefOfRequestId_llm[requestId].current?.()
|
||||
delete this._abortRefOfRequestId_llm[requestId]
|
||||
}
|
||||
|
||||
private _callOllamaList(params: MainModelListParams<OllamaModelResponse>) {
|
||||
const { requestId } = params;
|
||||
|
||||
const mainThreadParams: ModelListParams<OllamaModelResponse> = {
|
||||
...params,
|
||||
onSuccess: ({ models }) => { this._onSuccess_ollama.fire({ requestId, models }); },
|
||||
onError: ({ error }) => { this._onError_ollama.fire({ requestId, error }); },
|
||||
}
|
||||
ollamaList(mainThreadParams)
|
||||
}
|
||||
|
||||
private _callOpenAICompatibleList(params: MainModelListParams<OpenaiCompatibleModelResponse>) {
|
||||
const { requestId } = params;
|
||||
|
||||
const mainThreadParams: ModelListParams<OpenaiCompatibleModelResponse> = {
|
||||
...params,
|
||||
onSuccess: ({ models }) => { this._onSuccess_openAICompatible.fire({ requestId, models }); },
|
||||
onError: ({ error }) => { this._onError_openAICompatible.fire({ requestId, error }); },
|
||||
}
|
||||
openaiCompatibleList(mainThreadParams)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Glass Devtools, Inc. All rights reserved.
|
||||
* Void Editor additions licensed under the AGPL 3.0 License.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Disposable } from '../../../base/common/lifecycle.js';
|
||||
import { ITelemetryService } from '../../telemetry/common/telemetry.js';
|
||||
|
||||
import { IMetricsService } from '../common/metricsService.js';
|
||||
import { PostHog } from 'posthog-node'
|
||||
|
||||
|
||||
// posthog-js (old):
|
||||
// posthog.init('phc_UanIdujHiLp55BkUTjB1AuBXcasVkdqRwgnwRlWESH2', { api_host: 'https://us.i.posthog.com', })
|
||||
|
||||
// const buildEnv = 'development';
|
||||
// const buildNumber = '1.0.0';
|
||||
// const isMac = process.platform === 'darwin';
|
||||
|
||||
export class MetricsMainService extends Disposable implements IMetricsService {
|
||||
_serviceBrand: undefined;
|
||||
|
||||
readonly _distinctId: string
|
||||
readonly client: PostHog
|
||||
|
||||
constructor(
|
||||
@ITelemetryService private readonly _telemetryService: ITelemetryService
|
||||
) {
|
||||
super()
|
||||
this.client = new PostHog('phc_UanIdujHiLp55BkUTjB1AuBXcasVkdqRwgnwRlWESH2', { host: 'https://us.i.posthog.com', })
|
||||
|
||||
const { devDeviceId, firstSessionDate, machineId } = this._telemetryService
|
||||
this._distinctId = devDeviceId
|
||||
this.client.identify({ distinctId: devDeviceId, properties: { firstSessionDate, machineId } })
|
||||
|
||||
console.log('Void posthog metrics info:', JSON.stringify({ devDeviceId, firstSessionDate, machineId }))
|
||||
}
|
||||
|
||||
capture: IMetricsService['capture'] = (event, params) => {
|
||||
const capture = { distinctId: this._distinctId, event, properties: params } as const
|
||||
// console.log('full capture:', capture)
|
||||
this.client.capture(capture)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,8 @@ import { WorkspaceEdit } from '../../../editor/common/languages.js';
|
||||
// import { IHistoryService } from '../../services/history/common/history.js';
|
||||
|
||||
|
||||
// VOID: THIS FILE IS OUTDATED!!!!!! No longer used anywhere.
|
||||
|
||||
@extHostNamedCustomer(MainContext.MainThreadInlineDiff)
|
||||
export class MainThreadInlineDiff extends Disposable implements MainThreadInlineDiffShape {
|
||||
|
||||
|
||||
@@ -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<number>('sideBar.size', StorageScope.PROFILE, StorageTarget.MACHINE, 200),
|
||||
AUXILIARYBAR_SIZE: new InitializationStateKey<number>('auxiliaryBar.size', StorageScope.PROFILE, StorageTarget.MACHINE, 200),
|
||||
AUXILIARYBAR_SIZE: new InitializationStateKey<number>('auxiliaryBar.size', StorageScope.PROFILE, StorageTarget.MACHINE, 800), // Void changed this from 200 to 800
|
||||
PANEL_SIZE: new InitializationStateKey<number>('panel.size', StorageScope.PROFILE, StorageTarget.MACHINE, 300),
|
||||
|
||||
PANEL_LAST_NON_MAXIMIZED_HEIGHT: new RuntimeStateKey<number>('panel.lastNonMaximizedHeight', StorageScope.PROFILE, StorageTarget.MACHINE, 300),
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
<svg id="Layer_1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024"><style>.st0{fill:#f6f6f6;fill-opacity:0}.st1{fill:#fff}.st2{fill:#167abf}</style><path class="st0" d="M1024 1024H0V0h1024v1024z"/><path class="st1" d="M1024 85.333v853.333H0V85.333h1024z"/><path class="st2" d="M0 85.333h298.667v853.333H0V85.333zm1024 0v853.333H384V85.333h640zm-554.667 160h341.333v-64H469.333v64zm341.334 533.334H469.333v64h341.333l.001-64zm128-149.334H597.333v64h341.333l.001-64zm0-149.333H597.333v64h341.333l.001-64zm0-149.333H597.333v64h341.333l.001-64z"/></svg>
|
||||
|
Before Width: | Height: | Size: 559 B |
|
After Width: | Height: | Size: 795 KiB |
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -18,14 +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.'));
|
||||
@@ -147,38 +146,23 @@ export class EditorGroupWatermark extends Disposable {
|
||||
|
||||
|
||||
private render(): void {
|
||||
// const enabled = this.configurationService.getValue<boolean>('workbench.tips.enabled');
|
||||
|
||||
// if (enabled === this.enabled) {
|
||||
// return;
|
||||
// }
|
||||
|
||||
// this.enabled = enabled;
|
||||
|
||||
|
||||
// if (!enabled) {
|
||||
// return;
|
||||
// }
|
||||
|
||||
// const hasFolder = this.workbenchState !== WorkbenchState.EMPTY;
|
||||
// const selected = (hasFolder ? folderEntries : noFolderEntries)
|
||||
// .filter(entry => !('when' in entry) || this.contextKeyService.contextMatchesRules(entry.when))
|
||||
// .filter(entry => !('mac' in entry) || entry.mac === (isMacintosh && !isWeb))
|
||||
// .filter(entry => !!CommandsRegistry.getCommand(entry.id))
|
||||
// .filter(entry => !!this.keybindingService.lookupKeybinding(entry.id));
|
||||
|
||||
this.clear();
|
||||
const box = append(this.shortcuts, $('.watermark-box'));
|
||||
const boxBelow = append(this.shortcuts, $(''))
|
||||
boxBelow.style.display = 'flex'
|
||||
boxBelow.style.flex = 'row'
|
||||
boxBelow.style.justifyContent = 'center'
|
||||
const 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);
|
||||
// put async at top so don't need to wait (this prevents a jitter on load)
|
||||
const recentlyOpened = await this.workspacesService.getRecentlyOpened()
|
||||
.catch(() => ({ files: [], workspaces: [] })).then(w => w.workspaces);
|
||||
|
||||
clearNode(voidIconBox);
|
||||
clearNode(recentsBox);
|
||||
|
||||
this.currentDisposables.forEach(label => label.dispose());
|
||||
this.currentDisposables.clear();
|
||||
@@ -188,13 +172,14 @@ export class EditorGroupWatermark extends Disposable {
|
||||
if (this.contextService.getWorkbenchState() === WorkbenchState.EMPTY) {
|
||||
|
||||
// Open a folder
|
||||
const button = h('button')
|
||||
button.root.classList.add('void-watermark-button')
|
||||
button.root.style.display = 'block'
|
||||
button.root.style.marginLeft = 'auto'
|
||||
button.root.style.marginRight = 'auto'
|
||||
button.root.textContent = 'Open a folder'
|
||||
button.root.onclick = () => {
|
||||
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);
|
||||
@@ -202,23 +187,14 @@ 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) {
|
||||
|
||||
const span = $('div')
|
||||
span.textContent = 'Recent'
|
||||
span.style.fontWeight = '500'
|
||||
box.append(span)
|
||||
|
||||
box.append(
|
||||
...recentlyOpened.map(w => {
|
||||
voidIconBox.append(
|
||||
...recentlyOpened.map((w, i) => {
|
||||
|
||||
let fullPath: string;
|
||||
let windowOpenable: IWindowOpenable;
|
||||
@@ -235,14 +211,13 @@ export class EditorGroupWatermark extends Disposable {
|
||||
|
||||
const { name, parentPath } = splitRecentLabel(fullPath);
|
||||
|
||||
const li = $('li');
|
||||
const link = $('span');
|
||||
link.classList.add('void-link')
|
||||
const linkSpan = $('span');
|
||||
linkSpan.classList.add('void-link')
|
||||
linkSpan.style.display = 'flex'
|
||||
linkSpan.style.gap = '4px'
|
||||
linkSpan.style.padding = '8px'
|
||||
|
||||
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 => {
|
||||
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
|
||||
@@ -250,29 +225,30 @@ export class EditorGroupWatermark extends Disposable {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
});
|
||||
li.appendChild(link);
|
||||
|
||||
const span = $('span');
|
||||
span.style.paddingLeft = '4px';
|
||||
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);
|
||||
|
||||
return li
|
||||
const dirSpan = $('span');
|
||||
dirSpan.style.paddingLeft = '4px';
|
||||
dirSpan.innerText = parentPath;
|
||||
dirSpan.title = fullPath;
|
||||
|
||||
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'));
|
||||
@@ -283,7 +259,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'));
|
||||
@@ -293,8 +269,8 @@ export class EditorGroupWatermark extends Disposable {
|
||||
this.currentDisposables.add(label2);
|
||||
|
||||
const keys3 = this.keybindingService.lookupKeybinding('workbench.action.openGlobalKeybindings');
|
||||
const button3 = append(boxBelow, $('button'));
|
||||
button3.textContent = 'Void Settings'
|
||||
const button3 = append(recentsBox, $('button'));
|
||||
button3.textContent = `Void Settings`
|
||||
button3.style.display = 'block'
|
||||
button3.style.marginLeft = 'auto'
|
||||
button3.style.marginRight = 'auto'
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -289,7 +289,7 @@ export interface IFileTemplateData {
|
||||
readonly templateDisposables: DisposableStore;
|
||||
readonly elementDisposables: DisposableStore;
|
||||
readonly label: IResourceLabel;
|
||||
readonly voidLabels: IResourceLabel;
|
||||
// readonly voidLabels: IResourceLabel;
|
||||
readonly container: HTMLElement;
|
||||
readonly contribs: IExplorerFileContribution[];
|
||||
currentContext?: ExplorerItem;
|
||||
@@ -349,18 +349,19 @@ export class FilesRenderer implements ICompressibleTreeRenderer<ExplorerItem, Fu
|
||||
renderTemplate(container: HTMLElement): IFileTemplateData {
|
||||
const templateDisposables = new DisposableStore();
|
||||
|
||||
// Create void buttons container
|
||||
const voidButtonsContainer = DOM.append(container, DOM.$('div'));
|
||||
voidButtonsContainer.style.position = 'absolute'
|
||||
voidButtonsContainer.style.top = '0'
|
||||
voidButtonsContainer.style.right = '0'
|
||||
// const voidButtons = DOM.append(voidButtonsContainer, DOM.$('span'));
|
||||
// voidButtons.textContent = 'voidbuttons'
|
||||
// voidButtons.addEventListener('click', () => {
|
||||
// console.log('ON CLICK', templateData.currentContext?.children)
|
||||
// })
|
||||
const voidLabels = this.labels.create(voidButtonsContainer, { supportHighlights: false, supportIcons: false, });
|
||||
voidLabels.element.textContent = 'hi333'
|
||||
// Void added this
|
||||
// // Create void buttons container
|
||||
// const voidButtonsContainer = DOM.append(container, DOM.$('div'));
|
||||
// voidButtonsContainer.style.position = 'absolute'
|
||||
// voidButtonsContainer.style.top = '0'
|
||||
// voidButtonsContainer.style.right = '0'
|
||||
// // const voidButtons = DOM.append(voidButtonsContainer, DOM.$('span'));
|
||||
// // voidButtons.textContent = 'voidbuttons'
|
||||
// // voidButtons.addEventListener('click', () => {
|
||||
// // console.log('ON CLICK', templateData.currentContext?.children)
|
||||
// // })
|
||||
// const voidLabels = this.labels.create(voidButtonsContainer, { supportHighlights: false, supportIcons: false, });
|
||||
// voidLabels.element.textContent = 'hi333'
|
||||
|
||||
const label = templateDisposables.add(this.labels.create(container, { supportHighlights: true }));
|
||||
templateDisposables.add(label.onDidRender(() => {
|
||||
@@ -375,7 +376,10 @@ export class FilesRenderer implements ICompressibleTreeRenderer<ExplorerItem, Fu
|
||||
contr.setResource(templateData.currentContext?.resource);
|
||||
}));
|
||||
|
||||
const templateData: IFileTemplateData = { templateDisposables, elementDisposables: templateDisposables.add(new DisposableStore()), label, voidLabels, container, contribs };
|
||||
|
||||
// const templateData: IFileTemplateData = { templateDisposables, elementDisposables: templateDisposables.add(new DisposableStore()), label, voidLabels, container, contribs };
|
||||
const templateData: IFileTemplateData = { templateDisposables, elementDisposables: templateDisposables.add(new DisposableStore()), label, container, contribs };
|
||||
|
||||
return templateData;
|
||||
}
|
||||
|
||||
@@ -489,11 +493,11 @@ export class FilesRenderer implements ICompressibleTreeRenderer<ExplorerItem, Fu
|
||||
domId
|
||||
});
|
||||
|
||||
templateData.voidLabels.setResource({ resource: undefined, name: 'hi', }, {
|
||||
hideIcon: true,
|
||||
extraClasses: realignNestedChildren ? [...extraClasses, 'align-nest-icon-with-parent-icon'] : extraClasses,
|
||||
forceLabel: true,
|
||||
});
|
||||
// templateData.voidLabels.setResource({ resource: undefined, name: 'hi', }, {
|
||||
// hideIcon: true,
|
||||
// extraClasses: realignNestedChildren ? [...extraClasses, 'align-nest-icon-with-parent-icon'] : extraClasses,
|
||||
// forceLabel: true,
|
||||
// });
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -96,10 +96,10 @@ suite('Files - ExplorerView', () => {
|
||||
container: label,
|
||||
onDidRender: emitter.event
|
||||
},
|
||||
voidLabels: <any>{
|
||||
container: label,
|
||||
onDidRender: emitter.event
|
||||
},
|
||||
// voidLabels: <any>{
|
||||
// container: label,
|
||||
// onDidRender: emitter.event
|
||||
// },
|
||||
|
||||
}, 1, false);
|
||||
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
/*--------------------------------------------------------------------------------------
|
||||
* 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 { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js';
|
||||
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
|
||||
import { IMarkerService, MarkerSeverity } from '../../../../platform/markers/common/markers.js';
|
||||
import { ILanguageFeaturesService } from '../../../../editor/common/services/languageFeatures.js';
|
||||
import { ITextModelService } from '../../../../editor/common/services/resolverService.js';
|
||||
import { Range } from '../../../../editor/common/core/range.js';
|
||||
import { CancellationToken } from '../../../../base/common/cancellation.js';
|
||||
import { CodeActionContext, CodeActionTriggerType } from '../../../../editor/common/languages.js';
|
||||
import { URI } from '../../../../base/common/uri.js';
|
||||
|
||||
export interface IMarkerCheckService {
|
||||
readonly _serviceBrand: undefined;
|
||||
}
|
||||
|
||||
export const IMarkerCheckService = createDecorator<IMarkerCheckService>('markerCheckService');
|
||||
|
||||
class MarkerCheckService extends Disposable implements IMarkerCheckService {
|
||||
_serviceBrand: undefined;
|
||||
|
||||
constructor(
|
||||
@IMarkerService private readonly _markerService: IMarkerService,
|
||||
@ILanguageFeaturesService private readonly _languageFeaturesService: ILanguageFeaturesService,
|
||||
@ITextModelService private readonly _textModelService: ITextModelService,
|
||||
) {
|
||||
super();
|
||||
|
||||
setInterval(async () => {
|
||||
const allMarkers = this._markerService.read();
|
||||
const errors = allMarkers.filter(marker => marker.severity === MarkerSeverity.Error);
|
||||
|
||||
if (errors.length > 0) {
|
||||
for (const error of errors) {
|
||||
|
||||
console.log(`----------------------------------------------`);
|
||||
|
||||
console.log(`${error.resource.toString()}: ${error.startLineNumber} ${error.message} ${error.severity}`); // ! all errors in the file
|
||||
|
||||
try {
|
||||
// Get the text model for the file
|
||||
const modelReference = await this._textModelService.createModelReference(error.resource);
|
||||
const model = modelReference.object.textEditorModel;
|
||||
|
||||
// Create a range from the marker
|
||||
const range = new Range(
|
||||
error.startLineNumber,
|
||||
error.startColumn,
|
||||
error.endLineNumber,
|
||||
error.endColumn
|
||||
);
|
||||
|
||||
// Get code action providers for this model
|
||||
const codeActionProvider = this._languageFeaturesService.codeActionProvider;
|
||||
const providers = codeActionProvider.ordered(model);
|
||||
|
||||
if (providers.length > 0) {
|
||||
// Request code actions from each provider
|
||||
for (const provider of providers) {
|
||||
const context: CodeActionContext = {
|
||||
trigger: CodeActionTriggerType.Invoke, // keeping 'trigger' since it works
|
||||
only: 'quickfix' // adding this to filter for quick fixes
|
||||
};
|
||||
|
||||
const actions = await provider.provideCodeActions(
|
||||
model,
|
||||
range,
|
||||
context,
|
||||
CancellationToken.None
|
||||
);
|
||||
|
||||
if (actions?.actions?.length) {
|
||||
|
||||
const quickFixes = actions.actions.filter(action => action.isPreferred); // ! all quickFixes for the error
|
||||
const quickFixesForImports = actions.actions.filter(action => action.isPreferred && action.title.includes('import')); // ! all possible imports
|
||||
quickFixesForImports
|
||||
|
||||
if (quickFixes.length > 0) {
|
||||
console.log('Available Quick Fixes:');
|
||||
quickFixes.forEach(action => {
|
||||
console.log(`- ${action.title}`);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Dispose the model reference
|
||||
modelReference.dispose();
|
||||
} catch (e) {
|
||||
console.error('Error getting quick fixes:', e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
fixErrorsInFiles(uris: URI[], contextSoFar: []) {
|
||||
// const allMarkers = this._markerService.read();
|
||||
|
||||
|
||||
// check errors in files
|
||||
|
||||
|
||||
// give LLM errors in files
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
// private _onMarkersChanged = (changedResources: readonly URI[]): void => {
|
||||
// for (const resource of changedResources) {
|
||||
// const markers = this._markerService.read({ resource });
|
||||
|
||||
// if (markers.length === 0) {
|
||||
// console.log(`${resource.toString()}: No diagnostics`);
|
||||
// continue;
|
||||
// }
|
||||
|
||||
// console.log(`Diagnostics for ${resource.toString()}:`);
|
||||
// markers.forEach(marker => this._logMarker(marker));
|
||||
// }
|
||||
// };
|
||||
|
||||
|
||||
}
|
||||
|
||||
registerSingleton(IMarkerCheckService, MarkerCheckService, InstantiationType.Eager);
|
||||
@@ -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'
|
||||
@@ -0,0 +1,108 @@
|
||||
/*--------------------------------------------------------------------------------------
|
||||
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
// 1. search(ai)
|
||||
// - tool use to find all possible changes
|
||||
// - if search only: is this file related to the search?
|
||||
// - if search + replace: should I modify this file?
|
||||
// 2. replace(ai)
|
||||
// - what changes to make?
|
||||
// 3. postprocess errors
|
||||
// -fastapply changes simultaneously
|
||||
// -iterate on syntax errors (all files can be changed from a syntax error, not just the one with the error)
|
||||
|
||||
|
||||
// private async _searchUsingAI({ searchClause }: { searchClause: string }) {
|
||||
|
||||
// // const relevantURIs: URI[] = []
|
||||
// // const gatherPrompt = `\
|
||||
// // asdasdas
|
||||
// // `
|
||||
// // const filterPrompt = `\
|
||||
// // Is this file relevant?
|
||||
// // `
|
||||
|
||||
|
||||
// // // optimizations (DO THESE LATER!!!!!!)
|
||||
// // // if tool includes a uri in uriSet, skip it obviously
|
||||
// // let uriSet = new Set<URI>()
|
||||
// // // gather
|
||||
// // let messages = []
|
||||
// // while (true) {
|
||||
// // const result = await new Promise((res, rej) => {
|
||||
// // sendLLMMessage({
|
||||
// // messages,
|
||||
// // tools: ['search'],
|
||||
// // onFinalMessage: ({ result: r, }) => {
|
||||
// // res(r)
|
||||
// // },
|
||||
// // onError: (error) => {
|
||||
// // rej(error)
|
||||
// // }
|
||||
// // })
|
||||
// // })
|
||||
|
||||
// // messages.push({ role: 'tool', content: turnToString(result) })
|
||||
|
||||
// // sendLLMMessage({
|
||||
// // messages: { 'Output ': result },
|
||||
// // onFinalMessage: (r) => {
|
||||
// // // output is file1\nfile2\nfile3\n...
|
||||
// // }
|
||||
// // })
|
||||
|
||||
// // uriSet.add(...)
|
||||
// // }
|
||||
|
||||
// // // writes
|
||||
// // if (!replaceClause) return
|
||||
|
||||
// // for (const uri of uriSet) {
|
||||
// // // in future, batch these
|
||||
// // applyWorkflow({ uri, applyStr: replaceClause })
|
||||
// // }
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// // while (true) {
|
||||
// // const result = new Promise((res, rej) => {
|
||||
// // sendLLMMessage({
|
||||
// // messages,
|
||||
// // tools: ['search'],
|
||||
// // onResult: (r) => {
|
||||
// // res(r)
|
||||
// // }
|
||||
// // })
|
||||
// // })
|
||||
|
||||
// // messages.push(result)
|
||||
|
||||
// // }
|
||||
|
||||
|
||||
// }
|
||||
|
||||
|
||||
// private async _replaceUsingAI({ searchClause, replaceClause, relevantURIs }: { searchClause: string, replaceClause: string, relevantURIs: URI[] }) {
|
||||
|
||||
// for (const uri of relevantURIs) {
|
||||
|
||||
// uri
|
||||
|
||||
// }
|
||||
|
||||
|
||||
|
||||
// // should I change this file?
|
||||
// // if so what changes to make?
|
||||
|
||||
|
||||
|
||||
// // fast apply the changes
|
||||
// }
|
||||
|
||||
@@ -1,23 +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 { Disposable } from '../../../../base/common/lifecycle.js';
|
||||
import { ILanguageFeaturesService } from '../../../../editor/common/services/languageFeatures.js';
|
||||
import { registerSingleton, InstantiationType } from '../../../../platform/instantiation/common/extensions.js';
|
||||
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
|
||||
import { ITextModel } from '../../../../editor/common/model.js';
|
||||
import { Position } from '../../../../editor/common/core/position.js';
|
||||
import { InlineCompletion, InlineCompletionContext } from '../../../../editor/common/languages.js';
|
||||
import { InlineCompletion, InlineCompletionContext, } from '../../../../editor/common/languages.js';
|
||||
import { CancellationToken } from '../../../../base/common/cancellation.js';
|
||||
import { Range } from '../../../../editor/common/core/range.js';
|
||||
import { ILLMMessageService } from '../../../../platform/void/common/llmMessageService.js';
|
||||
import { IEditorService } from '../../../services/editor/common/editorService.js';
|
||||
import { isCodeEditor } from '../../../../editor/browser/editorBrowser.js';
|
||||
import { EditorResourceAccessor } from '../../../common/editor.js';
|
||||
import { IModelService } from '../../../../editor/common/services/model.js';
|
||||
import { extractCodeFromRegular } from './helpers/extractCodeFromResult.js';
|
||||
import { registerWorkbenchContribution2, WorkbenchPhase } from '../../../common/contributions.js';
|
||||
import { ILLMMessageService } from '../common/llmMessageService.js';
|
||||
import { _ln, allLinebreakSymbols } from '../common/voidFileService.js';
|
||||
// import { IContextGatheringService } from './contextGatheringService.js';
|
||||
|
||||
// The extension this was called from is here - https://github.com/voideditor/void/blob/autocomplete/extensions/void/src/extension/extension.ts
|
||||
|
||||
@@ -134,17 +136,27 @@ class LRUCache<K, V> {
|
||||
}
|
||||
}
|
||||
|
||||
type AutocompletionStatus = 'pending' | 'finished' | 'error';
|
||||
type AutocompletionPredictionType =
|
||||
| 'single-line-fill-middle'
|
||||
| 'single-line-redo-suffix'
|
||||
// | 'multi-line-start-here'
|
||||
| 'multi-line-start-on-next-line'
|
||||
| 'do-not-predict'
|
||||
|
||||
type Autocompletion = {
|
||||
id: number,
|
||||
prefix: string,
|
||||
suffix: string,
|
||||
llmPrefix: string,
|
||||
llmSuffix: string,
|
||||
startTime: number,
|
||||
endTime: number | undefined,
|
||||
status: AutocompletionStatus,
|
||||
status: 'pending' | 'finished' | 'error',
|
||||
type: AutocompletionPredictionType,
|
||||
llmPromise: Promise<string> | undefined,
|
||||
insertText: string,
|
||||
requestId: string | null,
|
||||
_newlineCount: number,
|
||||
}
|
||||
|
||||
const DEBOUNCE_TIME = 500
|
||||
@@ -153,13 +165,16 @@ const MAX_CACHE_SIZE = 20
|
||||
const MAX_PENDING_REQUESTS = 2
|
||||
|
||||
// postprocesses the result
|
||||
const postprocessResult = (result: string) => {
|
||||
const processStartAndEndSpaces = (result: string) => {
|
||||
|
||||
// trim all whitespace except for a single leading/trailing space
|
||||
// return result.trim()
|
||||
|
||||
[result,] = extractCodeFromRegular({ text: result, recentlyAddedTextLen: result.length })
|
||||
|
||||
const hasLeadingSpace = result.startsWith(' ');
|
||||
const hasTrailingSpace = result.endsWith(' ');
|
||||
|
||||
return (hasLeadingSpace ? ' ' : '')
|
||||
+ result.trim()
|
||||
+ (hasTrailingSpace ? ' ' : '');
|
||||
@@ -168,13 +183,13 @@ const postprocessResult = (result: string) => {
|
||||
|
||||
|
||||
// trims the end of the prefix to improve cache hit rate
|
||||
const removeLeftTabsAndTrimEnd = (s: string): string => {
|
||||
const removeLeftTabsAndTrimEnds = (s: string): string => {
|
||||
const trimmedString = s.trimEnd();
|
||||
const trailingEnd = s.slice(trimmedString.length);
|
||||
|
||||
// keep only a single trailing newline
|
||||
if (trailingEnd.includes('\n')) {
|
||||
s = trimmedString + '\n';
|
||||
if (trailingEnd.includes(_ln)) {
|
||||
s = trimmedString + _ln;
|
||||
}
|
||||
|
||||
s = s.replace(/^\s+/gm, ''); // remove left tabs
|
||||
@@ -184,7 +199,32 @@ const removeLeftTabsAndTrimEnd = (s: string): string => {
|
||||
|
||||
|
||||
|
||||
function getStringUpToUnbalancedParenthesis(s: string, prefix: string): string {
|
||||
const removeAllWhitespace = (str: string): string => str.replace(/\s+/g, '');
|
||||
|
||||
|
||||
|
||||
function getIsSubsequence({ of, subsequence }: { of: string, subsequence: string }): [boolean, string] {
|
||||
if (subsequence.length === 0) return [true, ''];
|
||||
if (of.length === 0) return [false, ''];
|
||||
|
||||
let subsequenceIndex = 0;
|
||||
let lastMatchChar = '';
|
||||
|
||||
for (let i = 0; i < of.length; i++) {
|
||||
if (of[i] === subsequence[subsequenceIndex]) {
|
||||
lastMatchChar = of[i];
|
||||
subsequenceIndex++;
|
||||
}
|
||||
if (subsequenceIndex === subsequence.length) {
|
||||
return [true, lastMatchChar];
|
||||
}
|
||||
}
|
||||
|
||||
return [false, lastMatchChar];
|
||||
}
|
||||
|
||||
|
||||
function getStringUpToUnbalancedClosingParenthesis(s: string, prefix: string): string {
|
||||
|
||||
const pairs: Record<string, string> = { ')': '(', '}': '{', ']': '[' };
|
||||
|
||||
@@ -220,19 +260,14 @@ function getStringUpToUnbalancedParenthesis(s: string, prefix: string): string {
|
||||
}
|
||||
|
||||
|
||||
const parenthesisChars = `{}()[]<>\`'"`
|
||||
// further trim the autocompletion
|
||||
const postprocessAutocompletion = ({ autocompletionMatchup, autocompletion, prefixAndSuffix }: { autocompletionMatchup: AutocompletionMatchupBounds, autocompletion: Autocompletion, prefixAndSuffix: PrefixAndSuffixInfo }) => {
|
||||
|
||||
// returns the text in the autocompletion to display, assuming the prefix is already matched
|
||||
const toInlineCompletions = ({ matchInfo, prefix, suffix, autocompletion, position, debug }: { matchInfo: matchInfo, prefix: string, suffix: string, autocompletion: Autocompletion, position: Position, debug?: boolean }): { insertText: string, range: Range }[] => {
|
||||
const { prefix, prefixToTheLeftOfCursor, suffixToTheRightOfCursor } = prefixAndSuffix
|
||||
|
||||
|
||||
const suffixLines = suffix.split('\n')
|
||||
const prefixLines = prefix.split('\n')
|
||||
const suffixToTheRightOfCursor = suffixLines[0]
|
||||
const prefixToTheLeftOfCursor = prefixLines[prefixLines.length - 1]
|
||||
const generatedMiddle = autocompletion.insertText
|
||||
|
||||
let startIdx = matchInfo.startIdx
|
||||
let startIdx = autocompletionMatchup.startIdx
|
||||
let endIdx = generatedMiddle.length // exclusive bounds
|
||||
|
||||
// const naiveReturnValue = generatedMiddle.slice(startIdx)
|
||||
@@ -253,7 +288,7 @@ const toInlineCompletions = ({ matchInfo, prefix, suffix, autocompletion, positi
|
||||
}
|
||||
|
||||
// if user is on a blank line and the generation starts with newline(s), remove them
|
||||
const numStartingNewlines = generatedMiddle.slice(startIdx).match(/^\n+/)?.[0].length || 0;
|
||||
const numStartingNewlines = generatedMiddle.slice(startIdx).match(new RegExp(`^${_ln}+`))?.[0].length || 0;
|
||||
if (
|
||||
!prefixToTheLeftOfCursor.trim()
|
||||
&& !suffixToTheRightOfCursor.trim()
|
||||
@@ -263,21 +298,21 @@ const toInlineCompletions = ({ matchInfo, prefix, suffix, autocompletion, positi
|
||||
startIdx += numStartingNewlines
|
||||
}
|
||||
|
||||
// if the generated text matches with the suffix on the current line, stop
|
||||
if (suffixToTheRightOfCursor.trim()) { // completing in the middle of a line
|
||||
// if the generated FIM text matches with the suffix on the current line, stop
|
||||
if (autocompletion.type === 'single-line-fill-middle' && suffixToTheRightOfCursor.trim()) { // completing in the middle of a line
|
||||
// complete until there is a match
|
||||
const rawMatchIndex = generatedMiddle.slice(startIdx).lastIndexOf(suffixToTheRightOfCursor.trim()[0])
|
||||
if (rawMatchIndex > -1) {
|
||||
// console.log('p2', rawMatchIndex, startIdx, suffixToTheRightOfCursor.trim()[0], 'AAA', generatedMiddle.slice(startIdx))
|
||||
const matchIdx = rawMatchIndex + startIdx;
|
||||
const matchChar = generatedMiddle[matchIdx]
|
||||
if (parenthesisChars.includes(matchChar)) {
|
||||
if (`{}()[]<>\`'"`.includes(matchChar)) {
|
||||
endIdx = Math.min(endIdx, matchIdx)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const restOfLineToGenerate = generatedMiddle.slice(startIdx).split('\n')[0] ?? ''
|
||||
const restOfLineToGenerate = generatedMiddle.slice(startIdx).split(_ln)[0] ?? ''
|
||||
// condition to complete as a single line completion
|
||||
if (
|
||||
prefixToTheLeftOfCursor.trim()
|
||||
@@ -285,7 +320,7 @@ const toInlineCompletions = ({ matchInfo, prefix, suffix, autocompletion, positi
|
||||
&& restOfLineToGenerate.trim()
|
||||
) {
|
||||
|
||||
const rawNewlineIdx = generatedMiddle.slice(startIdx).indexOf('\n')
|
||||
const rawNewlineIdx = generatedMiddle.slice(startIdx).indexOf(_ln)
|
||||
if (rawNewlineIdx > -1) {
|
||||
// console.log('p3', startIdx, rawNewlineIdx)
|
||||
const newlineIdx = rawNewlineIdx + startIdx;
|
||||
@@ -311,14 +346,48 @@ const toInlineCompletions = ({ matchInfo, prefix, suffix, autocompletion, positi
|
||||
let completionStr = generatedMiddle.slice(startIdx, endIdx)
|
||||
|
||||
// filter out unbalanced parentheses
|
||||
completionStr = getStringUpToUnbalancedParenthesis(completionStr, prefix)
|
||||
completionStr = getStringUpToUnbalancedClosingParenthesis(completionStr, prefix)
|
||||
// console.log('originalCompletionStr: ', JSON.stringify(generatedMiddle.slice(startIdx)))
|
||||
// console.log('finalCompletionStr: ', JSON.stringify(completionStr))
|
||||
|
||||
|
||||
return completionStr
|
||||
|
||||
}
|
||||
|
||||
// returns the text in the autocompletion to display, assuming the prefix is already matched
|
||||
const toInlineCompletions = ({ autocompletionMatchup, autocompletion, prefixAndSuffix, position, debug }: { autocompletionMatchup: AutocompletionMatchupBounds, autocompletion: Autocompletion, prefixAndSuffix: PrefixAndSuffixInfo, position: Position, debug?: boolean }): { insertText: string, range: Range }[] => {
|
||||
|
||||
let trimmedInsertText = postprocessAutocompletion({ autocompletionMatchup, autocompletion, prefixAndSuffix, })
|
||||
let rangeToReplace: Range = new Range(position.lineNumber, position.column, position.lineNumber, position.column)
|
||||
|
||||
// handle special cases
|
||||
|
||||
// if we redid the suffix, replace the suffix
|
||||
if (autocompletion.type === 'single-line-redo-suffix') {
|
||||
|
||||
const oldSuffix = prefixAndSuffix.suffixToTheRightOfCursor
|
||||
const newSuffix = autocompletion.insertText
|
||||
|
||||
const [isSubsequence, lastMatchingChar] = getIsSubsequence({ // check that the old text contains the same brackets + symbols as the new text
|
||||
subsequence: removeAllWhitespace(oldSuffix), // old suffix
|
||||
of: removeAllWhitespace(newSuffix), // new suffix
|
||||
})
|
||||
if (isSubsequence) {
|
||||
rangeToReplace = new Range(position.lineNumber, position.column, position.lineNumber, Number.MAX_SAFE_INTEGER)
|
||||
}
|
||||
else {
|
||||
|
||||
const lastMatchupIdx = trimmedInsertText.lastIndexOf(lastMatchingChar)
|
||||
trimmedInsertText = trimmedInsertText.slice(0, lastMatchupIdx + 1)
|
||||
const numCharsToReplace = oldSuffix.lastIndexOf(lastMatchingChar) + 1
|
||||
rangeToReplace = new Range(position.lineNumber, position.column, position.lineNumber, position.column + numCharsToReplace)
|
||||
// console.log('show____', trimmedInsertText, rangeToReplace)
|
||||
}
|
||||
}
|
||||
|
||||
return [{
|
||||
insertText: completionStr,
|
||||
insertText: trimmedInsertText,
|
||||
range: rangeToReplace,
|
||||
}]
|
||||
|
||||
@@ -345,7 +414,9 @@ const toInlineCompletions = ({ matchInfo, prefix, suffix, autocompletion, positi
|
||||
|
||||
// }
|
||||
|
||||
const getPrefixAndSuffix = (model: ITextModel, position: Position) => {
|
||||
|
||||
type PrefixAndSuffixInfo = { prefix: string, suffix: string, prefixLines: string[], suffixLines: string[], prefixToTheLeftOfCursor: string, suffixToTheRightOfCursor: string }
|
||||
const getPrefixAndSuffixInfo = (model: ITextModel, position: Position): PrefixAndSuffixInfo => {
|
||||
|
||||
const fullText = model.getValue();
|
||||
|
||||
@@ -353,30 +424,37 @@ const getPrefixAndSuffix = (model: ITextModel, position: Position) => {
|
||||
const prefix = fullText.substring(0, cursorOffset)
|
||||
const suffix = fullText.substring(cursorOffset)
|
||||
|
||||
return { prefix, suffix }
|
||||
|
||||
const prefixLines = prefix.split(_ln)
|
||||
const suffixLines = suffix.split(_ln)
|
||||
|
||||
const prefixToTheLeftOfCursor = prefixLines.slice(-1)[0] ?? ''
|
||||
const suffixToTheRightOfCursor = suffixLines[0] ?? ''
|
||||
|
||||
return { prefix, suffix, prefixLines, suffixLines, prefixToTheLeftOfCursor, suffixToTheRightOfCursor }
|
||||
|
||||
}
|
||||
|
||||
const getIndex = (str: string, line: number, char: number) => {
|
||||
return str.split('\n').slice(0, line).join('\n').length + (line > 0 ? 1 : 0) + char;
|
||||
return str.split(_ln).slice(0, line).join(_ln).length + (line > 0 ? 1 : 0) + char;
|
||||
}
|
||||
const getLastLine = (s: string): string => {
|
||||
const matches = s.match(/[^\n]*$/)
|
||||
const matches = s.match(new RegExp(`[^${_ln}]*$`))
|
||||
return matches ? matches[0] : ''
|
||||
}
|
||||
|
||||
type matchInfo = {
|
||||
lineStart: number,
|
||||
character: number,
|
||||
type AutocompletionMatchupBounds = {
|
||||
startLine: number,
|
||||
startCharacter: number,
|
||||
startIdx: number,
|
||||
}
|
||||
// returns the startIdx of the match if there is a match, or undefined if there is no match
|
||||
// all results are wrt `autocompletion.result`
|
||||
const getPrefixAutocompletionMatch = ({ prefix, autocompletion }: { prefix: string, autocompletion: Autocompletion }): matchInfo | undefined => {
|
||||
const getAutocompletionMatchup = ({ prefix, autocompletion }: { prefix: string, autocompletion: Autocompletion }): AutocompletionMatchupBounds | undefined => {
|
||||
|
||||
const trimmedCurrentPrefix = removeLeftTabsAndTrimEnd(prefix)
|
||||
const trimmedCompletionPrefix = removeLeftTabsAndTrimEnd(autocompletion.prefix)
|
||||
const trimmedCompletionMiddle = removeLeftTabsAndTrimEnd(autocompletion.insertText)
|
||||
const trimmedCurrentPrefix = removeLeftTabsAndTrimEnds(prefix)
|
||||
const trimmedCompletionPrefix = removeLeftTabsAndTrimEnds(autocompletion.prefix)
|
||||
const trimmedCompletionMiddle = removeLeftTabsAndTrimEnds(autocompletion.insertText)
|
||||
|
||||
// console.log('@result: ', JSON.stringify(autocompletion.insertText))
|
||||
// console.log('@trimmedCurrentPrefix: ', JSON.stringify(trimmedCurrentPrefix))
|
||||
@@ -384,7 +462,7 @@ const getPrefixAutocompletionMatch = ({ prefix, autocompletion }: { prefix: stri
|
||||
// console.log('@trimmedCompletionMiddle: ', JSON.stringify(trimmedCompletionMiddle))
|
||||
|
||||
if (trimmedCurrentPrefix.length < trimmedCompletionPrefix.length) { // user must write text beyond the original prefix at generation time
|
||||
console.log('@undefined1')
|
||||
// console.log('@undefined1')
|
||||
return undefined
|
||||
}
|
||||
|
||||
@@ -392,24 +470,24 @@ const getPrefixAutocompletionMatch = ({ prefix, autocompletion }: { prefix: stri
|
||||
!(trimmedCompletionPrefix + trimmedCompletionMiddle)
|
||||
.startsWith(trimmedCurrentPrefix)
|
||||
) {
|
||||
console.log('@undefined2')
|
||||
// console.log('@undefined2')
|
||||
return undefined
|
||||
}
|
||||
|
||||
// reverse map to find position wrt `autocompletion.result`
|
||||
const lineStart =
|
||||
trimmedCurrentPrefix.split('\n').length -
|
||||
trimmedCompletionPrefix.split('\n').length;
|
||||
trimmedCurrentPrefix.split(_ln).length -
|
||||
trimmedCompletionPrefix.split(_ln).length;
|
||||
|
||||
if (lineStart < 0) {
|
||||
console.log('@undefined3')
|
||||
// console.log('@undefined3')
|
||||
|
||||
console.error('Error: No line found.');
|
||||
return undefined;
|
||||
}
|
||||
const currentPrefixLine = getLastLine(trimmedCurrentPrefix)
|
||||
const completionPrefixLine = lineStart === 0 ? getLastLine(trimmedCompletionPrefix) : ''
|
||||
const completionMiddleLine = autocompletion.insertText.split('\n')[lineStart]
|
||||
const completionMiddleLine = autocompletion.insertText.split(_ln)[lineStart]
|
||||
const fullCompletionLine = completionPrefixLine + completionMiddleLine
|
||||
|
||||
// console.log('currentPrefixLine', currentPrefixLine)
|
||||
@@ -418,7 +496,7 @@ const getPrefixAutocompletionMatch = ({ prefix, autocompletion }: { prefix: stri
|
||||
|
||||
const charMatchIdx = fullCompletionLine.indexOf(currentPrefixLine)
|
||||
if (charMatchIdx < 0) {
|
||||
console.log('@undefined4', charMatchIdx)
|
||||
// console.log('@undefined4', charMatchIdx)
|
||||
|
||||
console.error('Warning: Found character with negative index. This should never happen.')
|
||||
return undefined
|
||||
@@ -432,8 +510,8 @@ const getPrefixAutocompletionMatch = ({ prefix, autocompletion }: { prefix: stri
|
||||
const startIdx = getIndex(autocompletion.insertText, lineStart, character)
|
||||
|
||||
return {
|
||||
lineStart,
|
||||
character,
|
||||
startLine: lineStart,
|
||||
startCharacter: character,
|
||||
startIdx,
|
||||
}
|
||||
|
||||
@@ -441,40 +519,89 @@ const getPrefixAutocompletionMatch = ({ prefix, autocompletion }: { prefix: stri
|
||||
}
|
||||
|
||||
|
||||
type CompletionOptions = {
|
||||
predictionType: AutocompletionPredictionType,
|
||||
shouldGenerate: boolean,
|
||||
llmPrefix: string,
|
||||
llmSuffix: string,
|
||||
stopTokens: string[],
|
||||
}
|
||||
const getCompletionOptions = (prefixAndSuffix: PrefixAndSuffixInfo, relevantContext: string, justAcceptedAutocompletion: boolean): CompletionOptions => {
|
||||
|
||||
let { prefix, suffix, prefixToTheLeftOfCursor, suffixToTheRightOfCursor, suffixLines, prefixLines } = prefixAndSuffix
|
||||
|
||||
const getCompletionOptions = ({ prefix, suffix }: { prefix: string, suffix: string }) => {
|
||||
// trim prefix and suffix to not be very large
|
||||
suffixLines = suffix.split(_ln).slice(0, 25)
|
||||
prefixLines = prefix.split(_ln).slice(-25)
|
||||
prefix = prefixLines.join(_ln)
|
||||
suffix = suffixLines.join(_ln)
|
||||
|
||||
const prefixLines = prefix.split('\n')
|
||||
const suffixLines = suffix.split('\n')
|
||||
let completionOptions: CompletionOptions
|
||||
|
||||
const prefixToLeftOfCursor = prefixLines.slice(-1)[0] ?? ''
|
||||
const suffixToRightOfCursor = suffixLines[0] ?? ''
|
||||
// if line is empty, do multiline completion
|
||||
const isLineEmpty = !prefixToTheLeftOfCursor.trim() && !suffixToTheRightOfCursor.trim()
|
||||
const isLinePrefixEmpty = removeAllWhitespace(prefixToTheLeftOfCursor).length === 0
|
||||
const isLineSuffixEmpty = removeAllWhitespace(suffixToTheRightOfCursor).length === 0
|
||||
|
||||
// default parameters
|
||||
let shouldGenerate = true
|
||||
let stopTokens: string[] = ['\n\n', '\r\n\r\n']
|
||||
// TODO add context to prefix
|
||||
// llmPrefix = '\n\n/* Relevant context:\n' + relevantContext + '\n*/\n' + llmPrefix
|
||||
|
||||
// specific cases
|
||||
if (suffixToRightOfCursor.trim() !== '') { // typing between something
|
||||
stopTokens = ['\n', '\r\n']
|
||||
// if we just accepted an autocompletion, predict a multiline completion starting on the next line
|
||||
if (justAcceptedAutocompletion && isLineSuffixEmpty) {
|
||||
const prefixWithNewline = prefix + _ln
|
||||
completionOptions = {
|
||||
predictionType: 'multi-line-start-on-next-line',
|
||||
shouldGenerate: true,
|
||||
llmPrefix: prefixWithNewline,
|
||||
llmSuffix: suffix,
|
||||
stopTokens: [`${_ln}${_ln}`] // double newlines
|
||||
}
|
||||
}
|
||||
// if the current line is empty, predict a single-line completion
|
||||
else if (isLineEmpty) {
|
||||
completionOptions = {
|
||||
predictionType: 'single-line-fill-middle',
|
||||
shouldGenerate: true,
|
||||
llmPrefix: prefix,
|
||||
llmSuffix: suffix,
|
||||
stopTokens: allLinebreakSymbols
|
||||
}
|
||||
}
|
||||
// if suffix is 3 or fewer characters, attempt to complete the line ignorning it
|
||||
else if (removeAllWhitespace(suffixToTheRightOfCursor).length <= 3) {
|
||||
const suffixLinesIgnoringThisLine = suffixLines.slice(1)
|
||||
const suffixStringIgnoringThisLine = suffixLinesIgnoringThisLine.length === 0 ? '' : _ln + suffixLinesIgnoringThisLine.join(_ln)
|
||||
completionOptions = {
|
||||
predictionType: 'single-line-redo-suffix',
|
||||
shouldGenerate: true,
|
||||
llmPrefix: prefix,
|
||||
llmSuffix: suffixStringIgnoringThisLine,
|
||||
stopTokens: allLinebreakSymbols
|
||||
}
|
||||
}
|
||||
// else attempt to complete the middle of the line if there is a prefix (the completion looks bad if there is no prefix)
|
||||
else if (!isLinePrefixEmpty) {
|
||||
completionOptions = {
|
||||
predictionType: 'single-line-fill-middle',
|
||||
shouldGenerate: true,
|
||||
llmPrefix: prefix,
|
||||
llmSuffix: suffix,
|
||||
stopTokens: allLinebreakSymbols
|
||||
}
|
||||
} else {
|
||||
completionOptions = {
|
||||
predictionType: 'do-not-predict',
|
||||
shouldGenerate: false,
|
||||
llmPrefix: prefix,
|
||||
llmSuffix: suffix,
|
||||
stopTokens: []
|
||||
}
|
||||
}
|
||||
|
||||
// if (prefixToLeftOfCursor.trim() === '' && suffixToRightOfCursor.trim() === '') { // at an empty line
|
||||
// stopTokens = ['\n\n', '\r\n\r\n']
|
||||
// }
|
||||
|
||||
if (prefixToLeftOfCursor === '') { // at beginning or end of line
|
||||
shouldGenerate = false
|
||||
}
|
||||
|
||||
return { shouldGenerate, stopTokens }
|
||||
return completionOptions
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
export interface IAutocompleteService {
|
||||
readonly _serviceBrand: undefined;
|
||||
}
|
||||
@@ -482,13 +609,17 @@ export interface IAutocompleteService {
|
||||
export const IAutocompleteService = createDecorator<IAutocompleteService>('AutocompleteService');
|
||||
|
||||
export class AutocompleteService extends Disposable implements IAutocompleteService {
|
||||
|
||||
static readonly ID = 'void.autocompleteService'
|
||||
|
||||
_serviceBrand: undefined;
|
||||
|
||||
private _autocompletionId: number = 0;
|
||||
private _autocompletionsOfDocument: { [docUriStr: string]: LRUCache<number, Autocompletion> } = {}
|
||||
|
||||
private _lastCompletionTime = 0
|
||||
private _lastPrefix: string = ''
|
||||
private _lastCompletionStart = 0
|
||||
private _lastCompletionAccept = 0
|
||||
// private _lastPrefix: string = ''
|
||||
|
||||
// used internally by vscode
|
||||
// fires after every keystroke and returns the completion to show
|
||||
@@ -499,16 +630,15 @@ export class AutocompleteService extends Disposable implements IAutocompleteServ
|
||||
token: CancellationToken,
|
||||
): Promise<InlineCompletion[]> {
|
||||
|
||||
const disabled = true
|
||||
const testMode = false
|
||||
|
||||
if (disabled) return [];
|
||||
|
||||
const docUriStr = model.uri.toString();
|
||||
|
||||
const { prefix, suffix } = getPrefixAndSuffix(model, position)
|
||||
// initialize cache and other variables
|
||||
// note that whenever an autocompletion is rejected, it is removed from cache
|
||||
const prefixAndSuffix = getPrefixAndSuffixInfo(model, position)
|
||||
const { prefix, suffix } = prefixAndSuffix
|
||||
|
||||
// initialize cache if it doesnt exist
|
||||
// note that whenever an autocompletion is accepted, it is removed from cache
|
||||
if (!this._autocompletionsOfDocument[docUriStr]) {
|
||||
this._autocompletionsOfDocument[docUriStr] = new LRUCache<number, Autocompletion>(
|
||||
MAX_CACHE_SIZE,
|
||||
@@ -518,7 +648,7 @@ export class AutocompleteService extends Disposable implements IAutocompleteServ
|
||||
}
|
||||
)
|
||||
}
|
||||
this._lastPrefix = prefix
|
||||
// this._lastPrefix = prefix
|
||||
|
||||
// print all pending autocompletions
|
||||
// let _numPending = 0
|
||||
@@ -527,33 +657,36 @@ export class AutocompleteService extends Disposable implements IAutocompleteServ
|
||||
|
||||
// get autocompletion from cache
|
||||
let cachedAutocompletion: Autocompletion | undefined = undefined
|
||||
let matchInfo: matchInfo | undefined = undefined
|
||||
let autocompletionMatchup: AutocompletionMatchupBounds | undefined = undefined
|
||||
for (const autocompletion of this._autocompletionsOfDocument[docUriStr].items.values()) {
|
||||
// if the user's change matches up with the generated text
|
||||
matchInfo = getPrefixAutocompletionMatch({ prefix, autocompletion })
|
||||
if (matchInfo !== undefined) {
|
||||
// if the user's change matches with the autocompletion
|
||||
autocompletionMatchup = getAutocompletionMatchup({ prefix, autocompletion })
|
||||
if (autocompletionMatchup !== undefined) {
|
||||
cachedAutocompletion = autocompletion
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// if there is a cached autocompletion, return it
|
||||
if (cachedAutocompletion && matchInfo) {
|
||||
if (cachedAutocompletion && autocompletionMatchup) {
|
||||
|
||||
console.log('AA')
|
||||
|
||||
|
||||
// console.log('id: ' + cachedAutocompletion.id)
|
||||
|
||||
if (cachedAutocompletion.status === 'finished') {
|
||||
// console.log('A1')
|
||||
console.log('A1')
|
||||
|
||||
const inlineCompletions = toInlineCompletions({ matchInfo, autocompletion: cachedAutocompletion, prefix, suffix, position, debug: true })
|
||||
const inlineCompletions = toInlineCompletions({ autocompletionMatchup, autocompletion: cachedAutocompletion, prefixAndSuffix, position, debug: true })
|
||||
return inlineCompletions
|
||||
|
||||
} else if (cachedAutocompletion.status === 'pending') {
|
||||
// console.log('A2')
|
||||
console.log('A2')
|
||||
|
||||
try {
|
||||
await cachedAutocompletion.llmPromise;
|
||||
const inlineCompletions = toInlineCompletions({ matchInfo, autocompletion: cachedAutocompletion, prefix, suffix, position })
|
||||
const inlineCompletions = toInlineCompletions({ autocompletionMatchup, autocompletion: cachedAutocompletion, prefixAndSuffix, position })
|
||||
return inlineCompletions
|
||||
|
||||
} catch (e) {
|
||||
@@ -562,19 +695,25 @@ export class AutocompleteService extends Disposable implements IAutocompleteServ
|
||||
}
|
||||
|
||||
} else if (cachedAutocompletion.status === 'error') {
|
||||
// console.log('A3')
|
||||
console.log('A3')
|
||||
} else {
|
||||
console.log('A4')
|
||||
}
|
||||
|
||||
return []
|
||||
}
|
||||
|
||||
// else if no more typing happens, then go forwards with the request
|
||||
|
||||
// wait DEBOUNCE_TIME for the user to stop typing
|
||||
const thisTime = Date.now()
|
||||
this._lastCompletionTime = thisTime
|
||||
|
||||
const justAcceptedAutocompletion = thisTime - this._lastCompletionAccept < 500
|
||||
|
||||
this._lastCompletionStart = thisTime
|
||||
const didTypingHappenDuringDebounce = await new Promise((resolve, reject) =>
|
||||
setTimeout(() => {
|
||||
if (this._lastCompletionTime === thisTime) {
|
||||
if (this._lastCompletionStart === thisTime) {
|
||||
resolve(false)
|
||||
} else {
|
||||
resolve(true)
|
||||
@@ -605,7 +744,15 @@ export class AutocompleteService extends Disposable implements IAutocompleteServ
|
||||
}
|
||||
}
|
||||
|
||||
const { shouldGenerate, stopTokens: _ } = getCompletionOptions({ prefix, suffix }) // TODO mat
|
||||
|
||||
// gather relevant context from the code around the user's selection and definitions
|
||||
// const relevantSnippetsList = await this._contextGatheringService.readCachedSnippets(model, position, 3);
|
||||
// const relevantSnippetsList = this._contextGatheringService.getCachedSnippets();
|
||||
// const relevantSnippets = relevantSnippetsList.map((text) => `${text}`).join('\n-------------------------------\n')
|
||||
// console.log('@@---------------------\n' + relevantSnippets)
|
||||
const relevantContext = ''
|
||||
|
||||
const { shouldGenerate, predictionType, llmPrefix, llmSuffix, stopTokens } = getCompletionOptions(prefixAndSuffix, relevantContext, justAcceptedAutocompletion)
|
||||
|
||||
if (!shouldGenerate) return []
|
||||
|
||||
@@ -613,46 +760,75 @@ export class AutocompleteService extends Disposable implements IAutocompleteServ
|
||||
return []
|
||||
}
|
||||
|
||||
|
||||
|
||||
// console.log('B')
|
||||
|
||||
// create a new autocompletion and add it to cache
|
||||
const newAutocompletion: Autocompletion = {
|
||||
id: this._autocompletionId++,
|
||||
prefix: prefix,
|
||||
prefix: prefix, // the actual prefix and suffix
|
||||
suffix: suffix,
|
||||
llmPrefix: llmPrefix, // the prefix and suffix the llm sees
|
||||
llmSuffix: llmSuffix,
|
||||
startTime: Date.now(),
|
||||
endTime: undefined,
|
||||
type: predictionType,
|
||||
status: 'pending',
|
||||
llmPromise: undefined,
|
||||
insertText: '',
|
||||
requestId: null,
|
||||
_newlineCount: 0,
|
||||
}
|
||||
|
||||
console.log('starting autocomplete...', predictionType)
|
||||
|
||||
// set parameters of `newAutocompletion` appropriately
|
||||
newAutocompletion.llmPromise = new Promise((resolve, reject) => {
|
||||
|
||||
const requestId = this._llmMessageService.sendLLMMessage({
|
||||
logging: { loggingName: 'Autocomplete' },
|
||||
messages: [],
|
||||
onText: async ({ newText, fullText }) => {
|
||||
|
||||
newAutocompletion.insertText = fullText
|
||||
|
||||
// if generation doesn't match the prefix for the first few tokens generated, reject it
|
||||
if (!getPrefixAutocompletionMatch({ prefix: this._lastPrefix, autocompletion: newAutocompletion })) {
|
||||
reject('LLM response did not match user\'s text.')
|
||||
}
|
||||
messagesType: 'FIMMessage',
|
||||
messages: {
|
||||
prefix: llmPrefix,
|
||||
suffix: llmSuffix,
|
||||
stopTokens: stopTokens,
|
||||
},
|
||||
useProviderFor: 'Autocomplete',
|
||||
logging: { loggingName: 'Autocomplete' },
|
||||
onText: () => { }, // unused in FIMMessage
|
||||
// onText: async ({ fullText, newText }) => {
|
||||
|
||||
// newAutocompletion.insertText = fullText
|
||||
|
||||
// // count newlines in newText
|
||||
// const numNewlines = newText.match(/\n|\r\n/g)?.length || 0
|
||||
// newAutocompletion._newlineCount += numNewlines
|
||||
|
||||
// // if too many newlines, resolve up to last newline
|
||||
// if (newAutocompletion._newlineCount > 10) {
|
||||
// const lastNewlinePos = fullText.lastIndexOf('\n')
|
||||
// newAutocompletion.insertText = fullText.substring(0, lastNewlinePos)
|
||||
// resolve(newAutocompletion.insertText)
|
||||
// return
|
||||
// }
|
||||
|
||||
// // if (!getAutocompletionMatchup({ prefix: this._lastPrefix, autocompletion: newAutocompletion })) {
|
||||
// // reject('LLM response did not match user\'s text.')
|
||||
// // }
|
||||
// },
|
||||
onFinalMessage: ({ fullText }) => {
|
||||
|
||||
// newAutocompletion.prefix = prefix
|
||||
// newAutocompletion.suffix = suffix
|
||||
// newAutocompletion.startTime = Date.now()
|
||||
// console.log('____res: ', JSON.stringify(newAutocompletion.insertText))
|
||||
|
||||
newAutocompletion.endTime = Date.now()
|
||||
// newAutocompletion.abortRef = { current: () => { } }
|
||||
newAutocompletion.status = 'finished'
|
||||
// newAutocompletion.promise = undefined
|
||||
newAutocompletion.insertText = postprocessResult(extractCodeFromRegular(fullText))
|
||||
const [text, _] = extractCodeFromRegular({ text: fullText, recentlyAddedTextLen: 0 })
|
||||
newAutocompletion.insertText = processStartAndEndSpaces(text)
|
||||
|
||||
// handle special case for predicting starting on the next line, add a newline character
|
||||
if (newAutocompletion.type === 'multi-line-start-on-next-line') {
|
||||
newAutocompletion.insertText = _ln + newAutocompletion.insertText
|
||||
}
|
||||
|
||||
resolve(newAutocompletion.insertText)
|
||||
|
||||
@@ -662,8 +838,6 @@ export class AutocompleteService extends Disposable implements IAutocompleteServ
|
||||
newAutocompletion.status = 'error'
|
||||
reject(message)
|
||||
},
|
||||
featureName: 'Autocomplete',
|
||||
range: { startLineNumber: position.lineNumber, startColumn: position.column, endLineNumber: position.lineNumber, endColumn: position.column },
|
||||
})
|
||||
newAutocompletion.requestId = requestId
|
||||
|
||||
@@ -686,8 +860,8 @@ export class AutocompleteService extends Disposable implements IAutocompleteServ
|
||||
await newAutocompletion.llmPromise
|
||||
// console.log('id: ' + newAutocompletion.id)
|
||||
|
||||
const matchInfo: matchInfo = { startIdx: 0, lineStart: 0, character: 0 }
|
||||
const inlineCompletions = toInlineCompletions({ matchInfo, autocompletion: newAutocompletion, prefix, suffix, position })
|
||||
const autocompletionMatchup: AutocompletionMatchupBounds = { startIdx: 0, startLine: 0, startCharacter: 0 }
|
||||
const inlineCompletions = toInlineCompletions({ autocompletionMatchup, autocompletion: newAutocompletion, prefixAndSuffix, position })
|
||||
return inlineCompletions
|
||||
|
||||
} catch (e) {
|
||||
@@ -703,6 +877,7 @@ export class AutocompleteService extends Disposable implements IAutocompleteServ
|
||||
@ILLMMessageService private readonly _llmMessageService: ILLMMessageService,
|
||||
@IEditorService private readonly _editorService: IEditorService,
|
||||
@IModelService private readonly _modelService: IModelService,
|
||||
// @IContextGatheringService private readonly _contextGatheringService: IContextGatheringService,
|
||||
) {
|
||||
super()
|
||||
|
||||
@@ -714,7 +889,6 @@ export class AutocompleteService extends Disposable implements IAutocompleteServ
|
||||
return { items: items, }
|
||||
},
|
||||
freeInlineCompletions: (completions) => {
|
||||
|
||||
// get the `docUriStr` and the `position` of the cursor
|
||||
const activePane = this._editorService.activeEditorPane;
|
||||
if (!activePane) return;
|
||||
@@ -727,33 +901,32 @@ export class AutocompleteService extends Disposable implements IAutocompleteServ
|
||||
const model = this._modelService.getModel(resource)
|
||||
if (!model) return;
|
||||
const docUriStr = resource.toString();
|
||||
|
||||
const { prefix, } = getPrefixAndSuffix(model, position)
|
||||
|
||||
if (!this._autocompletionsOfDocument[docUriStr]) return;
|
||||
|
||||
const { prefix, } = getPrefixAndSuffixInfo(model, position)
|
||||
|
||||
// go through cached items and remove matching ones
|
||||
// autocompletion.prefix + autocompletion.insertedText ~== insertedText
|
||||
completions.items.forEach(item => {
|
||||
this._autocompletionsOfDocument[docUriStr].items.forEach((autocompletion: Autocompletion) => {
|
||||
if (removeLeftTabsAndTrimEnd(prefix)
|
||||
=== removeLeftTabsAndTrimEnd(autocompletion.prefix + autocompletion.insertText)
|
||||
) {
|
||||
this._autocompletionsOfDocument[docUriStr].delete(autocompletion.id);
|
||||
}
|
||||
});
|
||||
this._autocompletionsOfDocument[docUriStr].items.forEach((autocompletion: Autocompletion) => {
|
||||
|
||||
// we can do this more efficiently, I just didn't want to deal with all of the edge cases
|
||||
const matchup = removeAllWhitespace(prefix) === removeAllWhitespace(autocompletion.prefix + autocompletion.insertText)
|
||||
|
||||
if (matchup) {
|
||||
console.log('ACCEPT', autocompletion.id)
|
||||
this._lastCompletionAccept = Date.now()
|
||||
this._autocompletionsOfDocument[docUriStr].delete(autocompletion.id);
|
||||
}
|
||||
});
|
||||
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
registerSingleton(IAutocompleteService, AutocompleteService, InstantiationType.Eager);
|
||||
registerWorkbenchContribution2(AutocompleteService.ID, AutocompleteService, WorkbenchPhase.BlockRestore);
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,354 @@
|
||||
import { CancellationToken } from '../../../../base/common/cancellation.js';
|
||||
import { Position } from '../../../../editor/common/core/position.js';
|
||||
import { DocumentSymbol, SymbolKind } from '../../../../editor/common/languages.js';
|
||||
import { ITextModel } from '../../../../editor/common/model.js';
|
||||
import { ILanguageFeaturesService } from '../../../../editor/common/services/languageFeatures.js';
|
||||
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
|
||||
import { Range, IRange } from '../../../../editor/common/core/range.js';
|
||||
import { Disposable } from '../../../../base/common/lifecycle.js';
|
||||
import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js';
|
||||
import { IModelService } from '../../../../editor/common/services/model.js';
|
||||
import { ICodeEditorService } from '../../../../editor/browser/services/codeEditorService.js';
|
||||
import { URI } from '../../../../base/common/uri.js';
|
||||
|
||||
|
||||
// make sure snippet logic works
|
||||
// change logic for `visited` to intervals
|
||||
// atomically set new snippets at end
|
||||
// throttle cache setting
|
||||
|
||||
interface IVisitedInterval {
|
||||
uri: string;
|
||||
startLine: number;
|
||||
endLine: number;
|
||||
}
|
||||
|
||||
export interface IContextGatheringService {
|
||||
readonly _serviceBrand: undefined;
|
||||
updateCache(model: ITextModel, pos: Position): Promise<void>;
|
||||
getCachedSnippets(): string[];
|
||||
}
|
||||
|
||||
export const IContextGatheringService = createDecorator<IContextGatheringService>('contextGatheringService');
|
||||
|
||||
class ContextGatheringService extends Disposable implements IContextGatheringService {
|
||||
_serviceBrand: undefined;
|
||||
private readonly _NUM_LINES = 3;
|
||||
private readonly _MAX_SNIPPET_LINES = 7; // Reasonable size for context
|
||||
// Cache holds the most recent list of snippets.
|
||||
private _cache: string[] = [];
|
||||
private _snippetIntervals: IVisitedInterval[] = [];
|
||||
|
||||
constructor(
|
||||
@ILanguageFeaturesService private readonly _langFeaturesService: ILanguageFeaturesService,
|
||||
@IModelService private readonly _modelService: IModelService,
|
||||
@ICodeEditorService private readonly _codeEditorService: ICodeEditorService
|
||||
) {
|
||||
super();
|
||||
this._modelService.getModels().forEach(model => this._subscribeToModel(model));
|
||||
this._register(this._modelService.onModelAdded(model => this._subscribeToModel(model)));
|
||||
}
|
||||
|
||||
private _subscribeToModel(model: ITextModel): void {
|
||||
console.log("Subscribing to model:", model.uri.toString());
|
||||
this._register(model.onDidChangeContent(() => {
|
||||
const editor = this._codeEditorService.getFocusedCodeEditor();
|
||||
if (editor && editor.getModel() === model) {
|
||||
const pos = editor.getPosition();
|
||||
console.log("updateCache called at position:", pos);
|
||||
if (pos) {
|
||||
this.updateCache(model, pos);
|
||||
}
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
public async updateCache(model: ITextModel, pos: Position): Promise<void> {
|
||||
const snippets = new Set<string>();
|
||||
this._snippetIntervals = []; // Reset intervals for new cache update
|
||||
|
||||
await this._gatherNearbySnippets(model, pos, this._NUM_LINES, 3, snippets, this._snippetIntervals);
|
||||
await this._gatherParentSnippets(model, pos, this._NUM_LINES, 3, snippets, this._snippetIntervals);
|
||||
|
||||
// Convert to array and filter overlapping snippets
|
||||
this._cache = Array.from(snippets);
|
||||
console.log("Cache updated:", this._cache);
|
||||
}
|
||||
|
||||
public getCachedSnippets(): string[] {
|
||||
return this._cache;
|
||||
}
|
||||
|
||||
// Basic snippet extraction.
|
||||
private _getSnippetForRange(model: ITextModel, range: IRange, numLines: number): string {
|
||||
const startLine = Math.max(range.startLineNumber - numLines, 1);
|
||||
const endLine = Math.min(range.endLineNumber + numLines, model.getLineCount());
|
||||
|
||||
// Enforce maximum snippet size
|
||||
const totalLines = endLine - startLine + 1;
|
||||
const adjustedStartLine = totalLines > this._MAX_SNIPPET_LINES
|
||||
? endLine - this._MAX_SNIPPET_LINES + 1
|
||||
: startLine;
|
||||
|
||||
const snippetRange = new Range(adjustedStartLine, 1, endLine, model.getLineMaxColumn(endLine));
|
||||
return this._cleanSnippet(model.getValueInRange(snippetRange));
|
||||
}
|
||||
|
||||
private _cleanSnippet(snippet: string): string {
|
||||
return snippet
|
||||
.split('\n')
|
||||
// Remove empty lines and lines with only comments
|
||||
.filter(line => {
|
||||
const trimmed = line.trim();
|
||||
return trimmed && !/^\/\/+$/.test(trimmed);
|
||||
})
|
||||
// Rejoin with newlines
|
||||
.join('\n')
|
||||
// Remove excess whitespace
|
||||
.trim();
|
||||
}
|
||||
|
||||
private _normalizeSnippet(snippet: string): string {
|
||||
return snippet
|
||||
// Remove multiple newlines
|
||||
.replace(/\n{2,}/g, '\n')
|
||||
// Remove trailing whitespace
|
||||
.trim();
|
||||
}
|
||||
|
||||
private _addSnippetIfNotOverlapping(
|
||||
model: ITextModel,
|
||||
range: IRange,
|
||||
snippets: Set<string>,
|
||||
visited: IVisitedInterval[]
|
||||
): void {
|
||||
const startLine = range.startLineNumber;
|
||||
const endLine = range.endLineNumber;
|
||||
const uri = model.uri.toString();
|
||||
|
||||
if (!this._isRangeVisited(uri, startLine, endLine, visited)) {
|
||||
visited.push({ uri, startLine, endLine });
|
||||
const snippet = this._normalizeSnippet(this._getSnippetForRange(model, range, this._NUM_LINES));
|
||||
if (snippet.length > 0) {
|
||||
snippets.add(snippet);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async _gatherNearbySnippets(
|
||||
model: ITextModel,
|
||||
pos: Position,
|
||||
numLines: number,
|
||||
depth: number,
|
||||
snippets: Set<string>,
|
||||
visited: IVisitedInterval[]
|
||||
): Promise<void> {
|
||||
if (depth <= 0) return;
|
||||
|
||||
const startLine = Math.max(pos.lineNumber - numLines, 1);
|
||||
const endLine = Math.min(pos.lineNumber + numLines, model.getLineCount());
|
||||
const range = new Range(startLine, 1, endLine, model.getLineMaxColumn(endLine));
|
||||
|
||||
this._addSnippetIfNotOverlapping(model, range, snippets, visited);
|
||||
|
||||
const symbols = await this._getSymbolsNearPosition(model, pos, numLines);
|
||||
for (const sym of symbols) {
|
||||
const defs = await this._getDefinitionSymbols(model, sym);
|
||||
for (const def of defs) {
|
||||
const defModel = this._modelService.getModel(def.uri);
|
||||
if (defModel) {
|
||||
const defPos = new Position(def.range.startLineNumber, def.range.startColumn);
|
||||
this._addSnippetIfNotOverlapping(defModel, def.range, snippets, visited);
|
||||
await this._gatherNearbySnippets(defModel, defPos, numLines, depth - 1, snippets, visited);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async _gatherParentSnippets(
|
||||
model: ITextModel,
|
||||
pos: Position,
|
||||
numLines: number,
|
||||
depth: number,
|
||||
snippets: Set<string>,
|
||||
visited: IVisitedInterval[]
|
||||
): Promise<void> {
|
||||
if (depth <= 0) return;
|
||||
|
||||
const container = await this._findContainerFunction(model, pos);
|
||||
if (!container) return;
|
||||
|
||||
const containerRange = container.kind === SymbolKind.Method ? container.selectionRange : container.range;
|
||||
this._addSnippetIfNotOverlapping(model, containerRange, snippets, visited);
|
||||
|
||||
const symbols = await this._getSymbolsNearRange(model, containerRange, numLines);
|
||||
for (const sym of symbols) {
|
||||
const defs = await this._getDefinitionSymbols(model, sym);
|
||||
for (const def of defs) {
|
||||
const defModel = this._modelService.getModel(def.uri);
|
||||
if (defModel) {
|
||||
const defPos = new Position(def.range.startLineNumber, def.range.startColumn);
|
||||
this._addSnippetIfNotOverlapping(defModel, def.range, snippets, visited);
|
||||
await this._gatherNearbySnippets(defModel, defPos, numLines, depth - 1, snippets, visited);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const containerPos = new Position(containerRange.startLineNumber, containerRange.startColumn);
|
||||
await this._gatherParentSnippets(model, containerPos, numLines, depth - 1, snippets, visited);
|
||||
}
|
||||
|
||||
private _isRangeVisited(uri: string, startLine: number, endLine: number, visited: IVisitedInterval[]): boolean {
|
||||
return visited.some(interval =>
|
||||
interval.uri === uri &&
|
||||
!(endLine < interval.startLine || startLine > interval.endLine)
|
||||
);
|
||||
}
|
||||
|
||||
private async _getSymbolsNearPosition(model: ITextModel, pos: Position, numLines: number): Promise<DocumentSymbol[]> {
|
||||
const startLine = Math.max(pos.lineNumber - numLines, 1);
|
||||
const endLine = Math.min(pos.lineNumber + numLines, model.getLineCount());
|
||||
const range = new Range(startLine, 1, endLine, model.getLineMaxColumn(endLine));
|
||||
return this._getSymbolsInRange(model, range);
|
||||
}
|
||||
|
||||
private async _getSymbolsNearRange(model: ITextModel, range: IRange, numLines: number): Promise<DocumentSymbol[]> {
|
||||
const centerLine = Math.floor((range.startLineNumber + range.endLineNumber) / 2);
|
||||
const startLine = Math.max(centerLine - numLines, 1);
|
||||
const endLine = Math.min(centerLine + numLines, model.getLineCount());
|
||||
const searchRange = new Range(startLine, 1, endLine, model.getLineMaxColumn(endLine));
|
||||
return this._getSymbolsInRange(model, searchRange);
|
||||
}
|
||||
|
||||
private async _getSymbolsInRange(model: ITextModel, range: IRange): Promise<DocumentSymbol[]> {
|
||||
const symbols: DocumentSymbol[] = [];
|
||||
const providers = this._langFeaturesService.documentSymbolProvider.ordered(model);
|
||||
for (const provider of providers) {
|
||||
try {
|
||||
const result = await provider.provideDocumentSymbols(model, CancellationToken.None);
|
||||
if (result) {
|
||||
const flat = this._flattenSymbols(result);
|
||||
const intersecting = flat.filter(sym => this._rangesIntersect(sym.range, range));
|
||||
symbols.push(...intersecting);
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn("Symbol provider error:", e);
|
||||
}
|
||||
}
|
||||
// Also check reference providers.
|
||||
const refProviders = this._langFeaturesService.referenceProvider.ordered(model);
|
||||
for (let line = range.startLineNumber; line <= range.endLineNumber; line++) {
|
||||
const content = model.getLineContent(line);
|
||||
const words = content.match(/[a-zA-Z_]\w*/g) || [];
|
||||
for (const word of words) {
|
||||
const startColumn = content.indexOf(word) + 1;
|
||||
const pos = new Position(line, startColumn);
|
||||
if (!this._positionInRange(pos, range)) continue;
|
||||
for (const provider of refProviders) {
|
||||
try {
|
||||
const refs = await provider.provideReferences(model, pos, { includeDeclaration: true }, CancellationToken.None);
|
||||
if (refs) {
|
||||
const filtered = refs.filter(ref => this._rangesIntersect(ref.range, range));
|
||||
for (const ref of filtered) {
|
||||
symbols.push({
|
||||
name: word,
|
||||
detail: '',
|
||||
kind: SymbolKind.Variable,
|
||||
range: ref.range,
|
||||
selectionRange: ref.range,
|
||||
children: [],
|
||||
tags: []
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn("Reference provider error:", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return symbols;
|
||||
}
|
||||
|
||||
private _flattenSymbols(symbols: DocumentSymbol[]): DocumentSymbol[] {
|
||||
const flat: DocumentSymbol[] = [];
|
||||
for (const sym of symbols) {
|
||||
flat.push(sym);
|
||||
if (sym.children && sym.children.length > 0) {
|
||||
flat.push(...this._flattenSymbols(sym.children));
|
||||
}
|
||||
}
|
||||
return flat;
|
||||
}
|
||||
|
||||
private _rangesIntersect(a: IRange, b: IRange): boolean {
|
||||
return !(
|
||||
a.endLineNumber < b.startLineNumber ||
|
||||
a.startLineNumber > b.endLineNumber ||
|
||||
(a.endLineNumber === b.startLineNumber && a.endColumn < b.startColumn) ||
|
||||
(a.startLineNumber === b.endLineNumber && a.endColumn > b.endColumn)
|
||||
);
|
||||
}
|
||||
|
||||
private _positionInRange(pos: Position, range: IRange): boolean {
|
||||
return pos.lineNumber >= range.startLineNumber &&
|
||||
pos.lineNumber <= range.endLineNumber &&
|
||||
(pos.lineNumber !== range.startLineNumber || pos.column >= range.startColumn) &&
|
||||
(pos.lineNumber !== range.endLineNumber || pos.column <= range.endColumn);
|
||||
}
|
||||
|
||||
// Get definition symbols for a given symbol.
|
||||
private async _getDefinitionSymbols(model: ITextModel, symbol: DocumentSymbol): Promise<(DocumentSymbol & { uri: URI })[]> {
|
||||
const pos = new Position(symbol.range.startLineNumber, symbol.range.startColumn);
|
||||
const providers = this._langFeaturesService.definitionProvider.ordered(model);
|
||||
const defs: (DocumentSymbol & { uri: URI })[] = [];
|
||||
for (const provider of providers) {
|
||||
try {
|
||||
const res = await provider.provideDefinition(model, pos, CancellationToken.None);
|
||||
if (res) {
|
||||
const links = Array.isArray(res) ? res : [res];
|
||||
defs.push(...links.map(link => ({
|
||||
name: symbol.name,
|
||||
detail: symbol.detail,
|
||||
kind: symbol.kind,
|
||||
range: link.range,
|
||||
selectionRange: link.range,
|
||||
children: [],
|
||||
tags: symbol.tags || [],
|
||||
uri: link.uri // Now keeping it as URI instead of converting to string
|
||||
})));
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn("Definition provider error:", e);
|
||||
}
|
||||
}
|
||||
return defs;
|
||||
}
|
||||
|
||||
private async _findContainerFunction(model: ITextModel, pos: Position): Promise<DocumentSymbol | null> {
|
||||
const searchRange = new Range(
|
||||
Math.max(pos.lineNumber - 1, 1), 1,
|
||||
Math.min(pos.lineNumber + 1, model.getLineCount()),
|
||||
model.getLineMaxColumn(pos.lineNumber)
|
||||
);
|
||||
const symbols = await this._getSymbolsInRange(model, searchRange);
|
||||
const funcs = symbols.filter(s =>
|
||||
(s.kind === SymbolKind.Function || s.kind === SymbolKind.Method) &&
|
||||
this._positionInRange(pos, s.range)
|
||||
);
|
||||
if (!funcs.length) return null;
|
||||
return funcs.reduce((innermost, current) => {
|
||||
if (!innermost) return current;
|
||||
const moreInner =
|
||||
(current.range.startLineNumber > innermost.range.startLineNumber ||
|
||||
(current.range.startLineNumber === innermost.range.startLineNumber &&
|
||||
current.range.startColumn > innermost.range.startColumn)) &&
|
||||
(current.range.endLineNumber < innermost.range.endLineNumber ||
|
||||
(current.range.endLineNumber === innermost.range.endLineNumber &&
|
||||
current.range.endColumn < innermost.range.endColumn));
|
||||
return moreInner ? current : innermost;
|
||||
}, null as DocumentSymbol | null);
|
||||
}
|
||||
}
|
||||
|
||||
registerSingleton(IContextGatheringService, ContextGatheringService, InstantiationType.Eager);
|
||||
@@ -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 { URI } from '../../../../../base/common/uri.js';
|
||||
@@ -80,6 +80,7 @@ export class ConsistentItemService extends Disposable {
|
||||
}
|
||||
|
||||
const initializeEditor = (editor: ICodeEditor) => {
|
||||
// if (editor.getModel()?.uri.scheme !== 'file') return // THIS BREAKS THINGS
|
||||
addTabSwitchListeners(editor)
|
||||
addDisposeListener(editor)
|
||||
putItemsOnEditor(editor, editor.getModel()?.uri ?? null)
|
||||
@@ -90,10 +91,7 @@ export class ConsistentItemService extends Disposable {
|
||||
this._register(this._editorService.onCodeEditorAdd(editor => { initializeEditor(editor) }))
|
||||
|
||||
// when an editor is deleted, remove its items
|
||||
this._register(this._editorService.onCodeEditorRemove(editor => {
|
||||
removeItemsFromEditor(editor)
|
||||
}))
|
||||
|
||||
this._register(this._editorService.onCodeEditorRemove(editor => { removeItemsFromEditor(editor) }))
|
||||
}
|
||||
|
||||
|
||||
@@ -157,7 +155,6 @@ export class ConsistentItemService extends Disposable {
|
||||
|
||||
|
||||
removeConsistentItemFromURI(consistentItemId: string) {
|
||||
|
||||
if (!(consistentItemId in this.infoOfConsistentItemId))
|
||||
return
|
||||
|
||||
@@ -173,6 +170,7 @@ export class ConsistentItemService extends Disposable {
|
||||
|
||||
// clear
|
||||
this.consistentItemIdsOfURI[uri.fsPath]?.delete(consistentItemId)
|
||||
|
||||
delete this.infoOfConsistentItemId[consistentItemId]
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,174 @@
|
||||
/*--------------------------------------------------------------------------------------
|
||||
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
// 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];
|
||||
}
|
||||