Compare commits
672 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 22f02d3ed1 | |||
| 74f8303803 | |||
| 41fe5c50e2 | |||
| 36b1b56690 | |||
| 249eee341d | |||
| 198a948f6c | |||
| 7dcb08b09a | |||
| efeb85b48b | |||
| d698e6f3c1 | |||
| 2161d740df | |||
| 96b5b72d30 | |||
| e2e8279fce | |||
| 5ed1f465a2 | |||
| 995357710e | |||
| a98106072c | |||
| c1be466467 | |||
| 9cbe83b57c | |||
| e46b295449 | |||
| 3fd3634257 | |||
| 231990088a | |||
| 20da82762e | |||
| 0d9be68aff | |||
| 165b5a4fd2 | |||
| 812014d6d3 | |||
| d3e73b92e0 | |||
| 00da05f24f | |||
| e4694256eb | |||
| 18b278577d | |||
| a252bb7a30 | |||
| c240360710 | |||
| 893308c38e | |||
| 48defe849d | |||
| 3c03f58488 | |||
| 2470e0de7f | |||
| 387fd64db0 | |||
| 4ffd269db5 | |||
| 5ce25e20c3 | |||
| 63efa219f5 | |||
| 826d148305 | |||
| 323d80ef55 | |||
| 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 | |||
| 4e392bee8e | |||
| 86346f1bf0 | |||
| a0672f845d | |||
| f96d320ce0 | |||
| bb7e69b20b | |||
| f19e579ce9 | |||
| dd133d2cd5 | |||
| 2ba04a78a8 | |||
| fb948b331f | |||
| 5083b8e971 | |||
| 35091eb8f2 | |||
| b9e61bf4e2 | |||
| 55ec3365a2 | |||
| d6811e872a | |||
| c176b8020f | |||
| 85c2c80db0 | |||
| b677d3dfc1 | |||
| 6dcf684429 | |||
| 26ddd6418c | |||
| cc177b9cd4 | |||
| dd204978e5 | |||
| fb48c4e67e | |||
| 74fa5cb505 | |||
| fae03c3060 | |||
| 5453acdb19 | |||
| 7047459e57 | |||
| 68722ee5c3 | |||
| cffe558cf5 | |||
| 9bc551f0ac | |||
| 01d7878671 | |||
| 2519f094cc | |||
| 4a993a716b | |||
| 23d1d4169a | |||
| f688e1ffae | |||
| a443315c14 | |||
| eeacb2d3c9 | |||
| cdbdf3c5c7 | |||
| 825c40bfff | |||
| a72305819a | |||
| a3d3309b9d | |||
| 32bfe9a111 | |||
| a9adcad560 | |||
| 80f7511eee | |||
| 7fe8f5c47d | |||
| b31882565b | |||
| 6aaf425419 | |||
| b0d21dfdb1 | |||
| 2087948789 | |||
| 6feb713682 | |||
| 667dc8bd7e | |||
| bdb0c65346 | |||
| df14b80d8b | |||
| 86decf8ed9 | |||
| bd9b9a9058 | |||
| 83c1f3430a | |||
| 02de8937ac | |||
| 78f3819c33 | |||
| b9cea4906e | |||
| 4a4677b5e2 | |||
| 5dab1a6a77 | |||
| 9b01d5ba7c | |||
| dcb708758d | |||
| dd4548466a | |||
| cda489e3e0 | |||
| 13594ef06c | |||
| c2b8df06eb | |||
| 33809ec48b | |||
| b4820abc9e | |||
| 0e79dec0d7 | |||
| 659f01413f | |||
| 110534df30 | |||
| d3b61867f0 | |||
| 954e611fda | |||
| 64760aae13 | |||
| d0d1124543 | |||
| 90a3fd0a21 | |||
| 11ed2545c7 | |||
| 3d1df92f6a | |||
| 4ca16caa4e | |||
| 06b5cca59a | |||
| e3c292eee7 | |||
| 99c3786d06 | |||
| 6e40a92315 | |||
| a9b2ff631d | |||
| 198adaaf18 | |||
| 2df7fa9ebc | |||
| 312942b004 | |||
| 0a444f6ff8 | |||
| 0c10cb73f6 | |||
| ef8bba00b9 | |||
| 3a3bc24fef | |||
| ff7830e704 | |||
| 01a345a391 | |||
| 33909b3143 | |||
| 9543bf8eb3 | |||
| 9a9d0da91e | |||
| 4065c5b504 | |||
| 4729e4dac3 | |||
| d85a316536 | |||
| 1df044fb02 | |||
| 076e0add44 | |||
| dfbd729fa4 | |||
| 8a17c9246b | |||
| 1afb39b155 | |||
| f08fd04af5 | |||
| ef9d0daef4 | |||
| ffe5a41b01 | |||
| 372beb7a2d | |||
| 406f29bb5a | |||
| ee8b98ff74 | |||
| ffb0549c7c | |||
| 8f9c2f0630 | |||
| 84bfc26dce | |||
| 80a87ff3c6 | |||
| 806ec939e5 | |||
| 98a22d2eb6 | |||
| ffa31bc289 | |||
| a1ab8a5796 | |||
| 70c7d0261c | |||
| e7af8d9448 | |||
| e1feaba74a | |||
| 8456f979ba | |||
| 6630d596f6 | |||
| a435a59a2e | |||
| 9cf4980004 | |||
| 6298be394d | |||
| e647ce0054 | |||
| 5fb30fed87 | |||
| 7cc57bf086 | |||
| 3fb6a4890d | |||
| 97fd332c4e | |||
| 81282e16b3 | |||
| 382275fa9f | |||
| 91f8321278 | |||
| 9b1f21d7c1 | |||
| 739ac2562b | |||
| 298423928e | |||
| 702d11ed3e | |||
| 4513b732d6 | |||
| 78e72f0c80 | |||
| f457f006fb | |||
| d6074e7ab5 | |||
| 3669401819 | |||
| 1e975ecc3b | |||
| 8476a99903 | |||
| 85314f8d47 | |||
| ee7d0ed836 | |||
| ba87b4f0de | |||
| e4bc21ffb1 | |||
| 81b78b7765 | |||
| c3c53b5e81 | |||
| f49a6f8e41 | |||
| 5a26aa8746 | |||
| 8661fe1417 | |||
| 2a519157da | |||
| ac8365def4 | |||
| 610b52587b | |||
| 01efab6a1d | |||
| 7f8e1ff00d | |||
| 5b55eed08a | |||
| d618223289 | |||
| 7752278720 | |||
| 597c04f63b | |||
| a7c4a1b179 | |||
| a7aab9cf86 | |||
| c44497510b | |||
| c485d2c478 | |||
| dfdf29fb36 | |||
| 420fb1a186 | |||
| 78a48bae13 | |||
| fc187e0028 | |||
| f973f8d4e7 | |||
| 8403eed8ca | |||
| 718b8cc61b | |||
| f6924df09b | |||
| 8b25f80e9b | |||
| a42ff2d59a | |||
| 0589a2a5ec | |||
| 7dda4bdc08 | |||
| f21ef61677 | |||
| 56b6d406d6 | |||
| 2ae29a50e5 | |||
| 9673e974ea | |||
| 842d388669 | |||
| f146385e1d | |||
| bb87726b34 | |||
| deb30b49bc | |||
| 56923eec52 | |||
| d2692d2594 | |||
| 5d44fdc17d | |||
| aada60c5be | |||
| b53e5d4886 | |||
| f299998994 | |||
| 967292e615 | |||
| 19e3ebefda | |||
| 7cf6c9d798 | |||
| 14e314e5b8 | |||
| 4e6e0b3bde | |||
| 63b086b786 | |||
| bda6ad06c9 | |||
| 829d82850d | |||
| 3991372f5b | |||
| 9b427c78bf | |||
| 5dad4c8f06 | |||
| e2d39fe4b1 | |||
| 1365400f21 | |||
| 94f4d2caaf | |||
| 696abc39fd | |||
| abc7151cd6 | |||
| c86f47743e | |||
| 9d6eb62c3e | |||
| 2f26b34a96 | |||
| 5807c94a61 | |||
| df23314bd2 | |||
| 6703aa1545 | |||
| aaab289ece | |||
| d809839530 | |||
| 9e583960ae | |||
| 19737d4cdc | |||
| 901579a445 | |||
| 051f8f38e7 | |||
| 07fcb3a72c | |||
| 2d80a29275 | |||
| 62cdaba44b | |||
| 3147a92463 | |||
| d175ac20ef | |||
| 42a2dec781 | |||
| dbef72c01c | |||
| 6ddf977c5d | |||
| 522f136872 | |||
| a28194c601 | |||
| cb2de5505b | |||
| f84c964d10 | |||
| bb4f99f687 | |||
| 49b43820b2 | |||
| b22bbf9639 | |||
| a1ff458007 | |||
| fb263f9d81 | |||
| 72deba43dd | |||
| 24fdb1ca0d | |||
| f20f41c804 | |||
| 66037e2049 | |||
| adb504f2e0 | |||
| 8b90d7827d | |||
| 62e1657274 | |||
| 7f776eebdb | |||
| 0db0513752 | |||
| d8b6b3c306 | |||
| f7c9d732f5 | |||
| 8a95b13711 | |||
| 55884ed1c9 | |||
| 582b25dc44 | |||
| 252af4c863 | |||
| e5ac36facd | |||
| 77fc2d2d4a | |||
| 298adac06e | |||
| 0f920fb867 | |||
| d8e9b50890 | |||
| 02112fbc4f | |||
| 05241649ff | |||
| 4ba00f6216 | |||
| 296cd1b477 | |||
| 2148fe77ec | |||
| 1f692ff01a | |||
| 8e4f943e30 | |||
| a52eba91c9 | |||
| 695d6a1e30 | |||
| a647530d77 | |||
| 18e3f19512 | |||
| 86294c676a | |||
| 7cfd9e5580 | |||
| d29eca457a | |||
| 0855376e02 | |||
| e1692758c6 | |||
| 411167ee44 | |||
| 389efb4701 | |||
| 48f9ac2ba1 | |||
| f9198d4d97 | |||
| eafc3dc0c9 | |||
| f2b176dfda | |||
| 02781927e1 | |||
| 4b3f881e1f | |||
| f3653c1c48 | |||
| 3a1955901e | |||
| e3ce68d2d6 | |||
| ab3d44387e | |||
| 96e24a89e6 | |||
| 25cc6aeb3b | |||
| 4cac92689a | |||
| 7444488278 | |||
| a6cf723dbe | |||
| 7e22b80986 | |||
| ef9ac7a9e3 | |||
| 48426403f3 | |||
| 4ea1bfa9c3 | |||
| a3e337fe2d | |||
| d12a5f3b38 | |||
| 68b5187f7c | |||
| 5140c79dcf | |||
| 91c65b6060 | |||
| aa6b5c6b6c | |||
| cc3a08e1ab | |||
| 5b3a6d240e | |||
| 8897feb290 |
@@ -10,6 +10,7 @@
|
||||
"jsdoc",
|
||||
"header",
|
||||
"local"
|
||||
// "react" // Void
|
||||
],
|
||||
"rules": {
|
||||
"constructor-super": "warn",
|
||||
|
||||
@@ -21,3 +21,6 @@ vscode.db
|
||||
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,22 +1,33 @@
|
||||
# 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:
|
||||
|
||||
- Suggest New Features ([Discord](https://discord.gg/RSNjgaugJs))
|
||||
- Build New Features ([Project](https://github.com/orgs/voideditor/projects/2/views/3))
|
||||
- Submit Issues/Docs/Bugs ([Issues](https://github.com/voideditor/void/issues))
|
||||
- 💫 Complete items on the [Roadmap](https://github.com/orgs/voideditor/projects/2).
|
||||
- 💡 Make suggestions in our [Discord](https://discord.gg/RSNjgaugJs).
|
||||
- 🪴 Start new Issues - see [Issues](https://github.com/voideditor/void/issues).
|
||||
|
||||
|
||||
## Building the full IDE
|
||||
|
||||
Please follow the steps below to build the IDE. If you have any questions, feel free to [submit an issue](https://github.com/voideditor/void/issues/new) with any build errors, or refer to VSCode's full [How to Contribute](https://github.com/microsoft/vscode/wiki/How-to-Contribute) page.
|
||||
### 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 two folders called `void/`.
|
||||
|
||||
|
||||
|
||||
|
||||
## Building Void
|
||||
|
||||
### a. Build Prerequisites - Mac
|
||||
|
||||
If you're using a Mac, make sure you have Python and XCode installed (you probably do by default).
|
||||
If you're using a Mac, you need Python and XCode. You probably have these by default.
|
||||
|
||||
### b. Build Prerequisites - Windows
|
||||
|
||||
@@ -27,38 +38,40 @@ Go to the "Workloads" tab and select:
|
||||
- `Node.js build tools`
|
||||
|
||||
Go to the "Individual Components" tab and select:
|
||||
- `MSVC v143 - VS 2022 C++ x64/x86 Spectre-mitigated libs (Latest)`,
|
||||
- `C++ ATL for latest build tools with Spectre Mitigations`,
|
||||
- `C++ MFC for latest build tools with Spectre Mitigations`.
|
||||
- `MSVC v143 - VS 2022 C++ x64/x86 Spectre-mitigated libs (Latest)`
|
||||
- `C++ ATL for latest build tools with Spectre Mitigations`
|
||||
- `C++ MFC for latest build tools with Spectre Mitigations`
|
||||
|
||||
Finally, click Install.
|
||||
|
||||
### c. Build Prerequisites - Linux
|
||||
|
||||
First, make sure you've installed NodeJS and run `npm install -g node-gyp`. Then:
|
||||
- Debian (Ubuntu, etc) - `sudo apt-get install build-essential g++ libx11-dev libxkbfile-dev libsecret-1-dev libkrb5-dev python-is-python3`.
|
||||
- Red Hat (Fedora, etc) - `sudo dnf install @development-tools gcc gcc-c++ make libsecret-devel krb5-devel libX11-devel libxkbfile-devel`.
|
||||
- Others - see [How to Contribute](https://github.com/microsoft/vscode/wiki/How-to-Contribute).
|
||||
First, run `npm install -g node-gyp`. Then:
|
||||
|
||||
### Build instructions
|
||||
- Debian (Ubuntu, etc): `sudo apt-get install build-essential g++ libx11-dev libxkbfile-dev libsecret-1-dev libkrb5-dev python-is-python3`.
|
||||
- Red Hat (Fedora, etc): `sudo dnf install @development-tools gcc gcc-c++ make libsecret-devel krb5-devel libX11-devel libxkbfile-devel`.
|
||||
- Others: see [How to Contribute](https://github.com/microsoft/vscode/wiki/How-to-Contribute).
|
||||
|
||||
To build Void, first follow the prerequisite steps above for your operating system and open `void/` inside VSCode. Then:
|
||||
### d. Building Void
|
||||
|
||||
1. Install all dependencies.
|
||||
To build Void, open `void/` inside VSCode. Then open your terminal and run:
|
||||
|
||||
```
|
||||
npm install
|
||||
```
|
||||
1. `npm install` to install all dependencies.
|
||||
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.bat` (Windows).
|
||||
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.
|
||||
|
||||
2. Run `cd ./src/vs/workbench/contrib/void/browser/react/` and then `node ./build.js` to build Void's external dependencies (our React components, etc).
|
||||
#### Building Void from Terminal
|
||||
|
||||
3. Press <kbd>Ctrl+Shift+B</kbd>, or if you prefer using the terminal run `npm run watch`.
|
||||
|
||||
This can take ~5 min.
|
||||
|
||||
If you ran <kbd>Ctrl+Shift+B</kbd>, the build is done when you see two check marks.
|
||||
|
||||
If you ran `npm run watch`, the build is done when you see something like this:
|
||||
Alternatively, if you want to build Void from the terminal, instead of pressing <kbd>Cmd+Shift+B</kbd> you can run `npm run watch`. The build is done when you see something like this:
|
||||
|
||||
```
|
||||
[watch-extensions] [00:37:39] Finished compilation extensions with 0 errors after 19303 ms
|
||||
@@ -67,65 +80,67 @@ If you ran `npm run watch`, the build is done when you see something like this:
|
||||
[watch-client ] [00:38:07] Finished compilation with 0 errors after 5 ms
|
||||
```
|
||||
|
||||
<!-- 3. Press <kbd>Ctrl+Shift+B</kbd> to start the build process. -->
|
||||
|
||||
4. In a new terminal, run `./scripts/code.sh` (Mac/Linux) or `./scripts/code.bat` (Windows). This should open up the built IDE!
|
||||
You can always press <kbd>Ctrl+Shift+P</kbd> and run "Reload Window" inside the new window to see changes without re-building.
|
||||
|
||||
Now that you're set up, feel free to check out our [Issues](https://github.com/voideditor/void/issues) page.
|
||||
|
||||
### Common Fixes
|
||||
|
||||
- Make sure you have the same NodeJS version as `.nvmrc`.
|
||||
|
||||
- If you see `[ERROR] Cannot start service: Host version "0.23.1" does not match binary version "0.23.0"`, run `npm i -D esbuild@0.23.0` or do a clean install of your npm dependencies.
|
||||
|
||||
|
||||
## Bundling
|
||||
#### Common Fixes
|
||||
|
||||
To bundle the IDE into an executable, run `npm run gulp vscode-darwin-arm64`.
|
||||
|
||||
Here are the full options: `vscode-{win32-ia32 | win32-x64 | darwin-x64 | darwin-arm64 | linux-ia32 | linux-x64 | linux-arm}(-min)`
|
||||
|
||||
|
||||
## Roadmap
|
||||
|
||||
Here are the most important topics on our Roadmap. More ⭐'s = more important. Please refer to our [Issues](https://github.com/voideditor/void/issues) page for the latest issues.
|
||||
|
||||
## ⭐⭐⭐ Make History work well.
|
||||
|
||||
When the user submits a response or presses the apply/accept/reject button, we should add these events to the history, allowing the user to undo/redo them. Right now there is unexpected behavior if the user tries to undo or redo their changes.
|
||||
|
||||
## ⭐⭐⭐ Build Cursor-style quick edits (Ctrl+K).
|
||||
|
||||
When the user presses Ctrl+K, an input box should appear inline with the code that they were selecting. This is somewhat difficult to do because an extension alone cannot do this, and it requires creating a new component in the IDE. We think you can modify vscode's built-in "codelens" or "zone widget" components, but we are open to alternatives.
|
||||
|
||||
## ⭐⭐⭐ Creative.
|
||||
|
||||
Examples: creating better code search, or supporting AI agents that can edit across files and make multiple LLM calls.
|
||||
|
||||
Eventually, we want to build a convenient API for creating AI tools. The API will provide methods for creating the UI (showing an autocomplete suggestion, or creating a new diff), detecting event changes (like `onKeystroke` or `onFileOpen`), and modifying the user's file-system (storing indexes associated with each file), making it much easier to make your own AI plugin. We plan on building these features further along in timeline, but we wanted to list them for completeness.
|
||||
|
||||
## ⭐ One-stars.
|
||||
|
||||
⭐ Let the user Accept / Reject all Diffs in an entire file via the sidebar.
|
||||
|
||||
# Guidelines
|
||||
|
||||
We're always glad to talk about new ideas, help you get set up, and make sure your changes align with our vision for the project. Feel free to shoot us a message in the #general channel of the [Discord](https://discord.gg/RSNjgaugJs) for any reason. Please check in especially if you want to make a lot of changes or build a large new feature.
|
||||
- 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.
|
||||
|
||||
|
||||
|
||||
## Submitting a Pull Request
|
||||
## Packaging
|
||||
|
||||
Please submit a pull request once you've made a change. You don't need to submit an issue.
|
||||
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.
|
||||
|
||||
Please don't use AI to write your PR 🙂.
|
||||
|
||||
### Mac
|
||||
- `npm run gulp vscode-darwin-arm64` - most common (Apple Silicon)
|
||||
- `npm run gulp vscode-darwin-x64` (Intel)
|
||||
|
||||
### Windows
|
||||
- `npm run gulp vscode-win32-x64` - most common
|
||||
- `npm run gulp vscode-win32-ia32`
|
||||
|
||||
### Linux
|
||||
- `npm run gulp vscode-linux-x64` - most common
|
||||
- `npm run gulp vscode-linux-arm`
|
||||
- `npm run gulp vscode-linux-ia32`
|
||||
|
||||
|
||||
### Output
|
||||
|
||||
This will generate a folder outside of `void/`:
|
||||
```bash
|
||||
workspace/
|
||||
├── void/ # Your Void fork
|
||||
└── VSCode-darwin-arm64/ # Generated output
|
||||
```
|
||||
|
||||
### Distributing
|
||||
Void's maintainers distribute Void on our website and in releases. If you'd like to see the scripts to convert `Mac .app -> .dmg`, `Windows folder -> .exe`, and `Linux folder -> appimage` for distribution, feel free to reach out.
|
||||
|
||||
## Pull Request Guidelines
|
||||
|
||||
|
||||
- Please submit a pull request once you've made a change.
|
||||
- No need to submit an Issue unless you're creating a new feature that might involve multiple PRs.
|
||||
- Please don't use AI to write your PR 🙂
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<!--
|
||||
# Relevant files
|
||||
|
||||
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
|
||||
@@ -146,7 +161,4 @@ We keep track of all the files we've changed with Void so it's easy to rebase:
|
||||
|
||||
- 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 -->
|
||||
@@ -2,18 +2,22 @@
|
||||
|
||||
The Void team put together this list of links to get up and running with VSCode's sourcecode. We hope it's helpful!
|
||||
|
||||
## Contributing
|
||||
|
||||
- [How VSCode's sourcecode is organized](https://github.com/microsoft/vscode/wiki/Source-Code-Organization) - this explains where the entry point files are, what `browser/` and `common/` mean, etc. This is the most important read on this whole list! We recommend reading the whole thing.
|
||||
|
||||
- [Built-in VSCode styles](https://code.visualstudio.com/api/references/theme-color) - CSS variables that are built into VSCode. Use `var(--vscode-{theme but replacing . with -})`. You can also see their [Webview theming guide](https://code.visualstudio.com/api/extension-guides/webview#theming-webview-content).
|
||||
|
||||
## Beginners / Getting started
|
||||
|
||||
- [VSCode UI guide](https://code.visualstudio.com/docs/getstarted/userinterface) - covers auxbar, panels, etc.
|
||||
|
||||
- [UX guide](https://code.visualstudio.com/api/ux-guidelines/overview) - covers Containers, Views, Items, etc.
|
||||
|
||||
## Contributing
|
||||
|
||||
- [How VS Code's sourcecode is organized](https://github.com/microsoft/vscode/wiki/Source-Code-Organization) - this explains where the entry point files are, what `browser/` and `common/` mean, etc. This is the most important read on this whole list! We recommend reading the whole thing.
|
||||
## Misc
|
||||
|
||||
|
||||
- [Every command](https://code.visualstudio.com/api/references/commands) built-in to VSCode - sometimes useful to reference.
|
||||
- [Every command](https://code.visualstudio.com/api/references/commands) built-in to VSCode - not used often, but here for reference.
|
||||
|
||||
|
||||
## VSCode's Extension API
|
||||
@@ -28,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}`
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -341,6 +341,8 @@ function packageTask(platform, arch, sourceFolderName, destinationFolderName, op
|
||||
this.emit('data', file);
|
||||
}));
|
||||
|
||||
|
||||
// Void - this is important, creates the product.json in .app
|
||||
let productJsonContents;
|
||||
const productJsonStream = gulp.src(['product.json'], { base: '.' })
|
||||
.pipe(json({ commit, date: readISODate('out-build'), checksums, version }))
|
||||
@@ -425,15 +427,15 @@ function packageTask(platform, arch, sourceFolderName, destinationFolderName, op
|
||||
'resources/win32/vue.ico',
|
||||
'resources/win32/xml.ico',
|
||||
'resources/win32/yaml.ico',
|
||||
'resources/win32/code_70x70.png',
|
||||
'resources/win32/code_150x150.png'
|
||||
'resources/win32/code_70x70.png', // <-- Void icon
|
||||
'resources/win32/code_150x150.png' // <-- Void icon
|
||||
], { base: '.' }));
|
||||
} else if (platform === 'linux') {
|
||||
all = es.merge(all, gulp.src('resources/linux/code.png', { base: '.' }));
|
||||
all = es.merge(all, gulp.src('resources/linux/code.png', { base: '.' })); // <-- Void icon
|
||||
} else if (platform === 'darwin') {
|
||||
const shortcut = gulp.src('resources/darwin/bin/code.sh')
|
||||
.pipe(replace('@@APPNAME@@', product.applicationName))
|
||||
.pipe(rename('bin/code'));
|
||||
.pipe(rename('bin/code')); // <-- Void icon
|
||||
|
||||
all = es.merge(all, shortcut);
|
||||
}
|
||||
|
||||
@@ -54,7 +54,7 @@ function darwinBundleDocumentType(extensions, icon, nameOrSuffix, utis) {
|
||||
role: 'Editor',
|
||||
ostypes: ['TEXT', 'utxt', 'TUTX', '****'],
|
||||
extensions,
|
||||
iconFile: 'resources/darwin/' + icon.toLowerCase() + '.icns',
|
||||
iconFile: 'resources/darwin/' + icon.toLowerCase() + '.icns', // <-- Void icon code.icns
|
||||
utis
|
||||
};
|
||||
}
|
||||
@@ -179,7 +179,7 @@ exports.config = {
|
||||
darwinForceDarkModeSupport: true,
|
||||
darwinCredits: darwinCreditsTemplate ? Buffer.from(darwinCreditsTemplate({ commit: commit, date: new Date().toISOString() })) : undefined,
|
||||
linuxExecutableName: product.applicationName,
|
||||
winIcon: 'resources/win32/code.ico',
|
||||
winIcon: 'resources/win32/code.ico', // <-- Void icon
|
||||
token: process.env['GITHUB_TOKEN'],
|
||||
repo: product.electronRepository || undefined,
|
||||
validateChecksum: true,
|
||||
|
||||
@@ -68,7 +68,7 @@ function darwinBundleDocumentType(extensions: string[], icon: string, nameOrSuff
|
||||
role: 'Editor',
|
||||
ostypes: ['TEXT', 'utxt', 'TUTX', '****'],
|
||||
extensions,
|
||||
iconFile: 'resources/darwin/' + icon.toLowerCase() + '.icns',
|
||||
iconFile: 'resources/darwin/' + icon.toLowerCase() + '.icns', // <-- Void icon code.icns
|
||||
utis
|
||||
};
|
||||
}
|
||||
@@ -196,7 +196,7 @@ export const config = {
|
||||
darwinForceDarkModeSupport: true,
|
||||
darwinCredits: darwinCreditsTemplate ? Buffer.from(darwinCreditsTemplate({ commit: commit, date: new Date().toISOString() })) : undefined,
|
||||
linuxExecutableName: product.applicationName,
|
||||
winIcon: 'resources/win32/code.ico',
|
||||
winIcon: 'resources/win32/code.ico', // <-- Void icon
|
||||
token: process.env['GITHUB_TOKEN'],
|
||||
repo: product.electronRepository || undefined,
|
||||
validateChecksum: true,
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
? ('; LicenseFile: "' + RepoDir + '\licenses\LICENSE-' + Language + '.rtf"') \
|
||||
: '; LicenseFile: "' + RepoDir + '\' + RootLicenseFileName + '"'
|
||||
|
||||
|
||||
[Setup]
|
||||
AppId={#AppId}
|
||||
AppName={#NameLong}
|
||||
@@ -20,8 +21,10 @@ Compression=lzma
|
||||
SolidCompression=yes
|
||||
AppMutex={code:GetAppMutex}
|
||||
SetupMutex={#AppMutex}setup
|
||||
WizardImageFile="{#RepoDir}\resources\win32\inno-big-100.bmp,{#RepoDir}\resources\win32\inno-big-125.bmp,{#RepoDir}\resources\win32\inno-big-150.bmp,{#RepoDir}\resources\win32\inno-big-175.bmp,{#RepoDir}\resources\win32\inno-big-200.bmp,{#RepoDir}\resources\win32\inno-big-225.bmp,{#RepoDir}\resources\win32\inno-big-250.bmp"
|
||||
WizardSmallImageFile="{#RepoDir}\resources\win32\inno-small-100.bmp,{#RepoDir}\resources\win32\inno-small-125.bmp,{#RepoDir}\resources\win32\inno-small-150.bmp,{#RepoDir}\resources\win32\inno-small-175.bmp,{#RepoDir}\resources\win32\inno-small-200.bmp,{#RepoDir}\resources\win32\inno-small-225.bmp,{#RepoDir}\resources\win32\inno-small-250.bmp"
|
||||
; this is a Void icon comment. Old: WizardImageFile="{#RepoDir}\resources\win32\inno-big-100.bmp,{#RepoDir}\resources\win32\inno-big-125.bmp,{#RepoDir}\resources\win32\inno-big-150.bmp,{#RepoDir}\resources\win32\inno-big-175.bmp,{#RepoDir}\resources\win32\inno-big-200.bmp,{#RepoDir}\resources\win32\inno-big-225.bmp,{#RepoDir}\resources\win32\inno-big-250.bmp"
|
||||
; this is a Void icon comment. Old: WizardSmallImageFile="{#RepoDir}\resources\win32\inno-small-100.bmp,{#RepoDir}\resources\win32\inno-small-125.bmp,{#RepoDir}\resources\win32\inno-small-150.bmp,{#RepoDir}\resources\win32\inno-small-175.bmp,{#RepoDir}\resources\win32\inno-small-200.bmp,{#RepoDir}\resources\win32\inno-small-225.bmp,{#RepoDir}\resources\win32\inno-small-250.bmp"
|
||||
; WizardImageFile="{#RepoDir}\resources\win32\inno-void.bmp"
|
||||
WizardSmallImageFile="{#RepoDir}\resources\win32\inno-void.bmp"
|
||||
SetupIconFile={#RepoDir}\resources\win32\code.ico
|
||||
UninstallDisplayIcon={app}\{#ExeBasename}.exe
|
||||
ChangesEnvironment=true
|
||||
|
||||
@@ -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,3 @@
|
||||
This folder is no longer relevant, since Void is no longer just an extension.
|
||||
|
||||
See the new Void code in `src/vs/workbench/contrib/void` (and a few other locations - you can just search "Void").
|
||||
@@ -1,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;
|
||||
}
|
||||
@@ -1,471 +0,0 @@
|
||||
import * as vscode from 'vscode';
|
||||
import { AbortRef, LLMMessage, sendLLMMessage } from '../common/sendLLMMessage';
|
||||
import { getVoidConfigFromPartial, VoidConfig } from '../webviews/common/contextForConfig';
|
||||
import { LRUCache } from 'lru-cache';
|
||||
|
||||
|
||||
|
||||
// The extension this was called from is here - https://github.com/voideditor/void/blob/autocomplete/extensions/void/src/extension/extension.ts
|
||||
|
||||
|
||||
/*
|
||||
A summary of autotab:
|
||||
|
||||
Postprocessing
|
||||
-one common problem for all models is outputting unbalanced parentheses
|
||||
we solve this by trimming all extra closing parentheses from the generated string
|
||||
in future, should make sure parentheses are always balanced
|
||||
|
||||
-another problem is completing the middle of a string, eg. "const [x, CURSOR] = useState()"
|
||||
we complete up to first matchup character
|
||||
but should instead complete the whole line / block (difficult because of parenthesis accuracy)
|
||||
|
||||
-too much info is bad. usually we want to show the user 1 line, and have a preloaded response afterwards
|
||||
this should happen automatically with caching system
|
||||
should break preloaded responses into \n\n chunks
|
||||
|
||||
Preprocessing
|
||||
- we don't generate if cursor is at end / beginning of a line (no spaces)
|
||||
- we generate 1 line if there is text to the right of cursor
|
||||
- we generate 1 line if variable declaration
|
||||
- (in many cases want to show 1 line but generate multiple)
|
||||
|
||||
State
|
||||
- cache based on prefix (and do some trimming first)
|
||||
- when press tab on one line, should have an immediate followup response
|
||||
to do this, show autocompletes before they're fully finished
|
||||
- [todo] remove each autotab when accepted
|
||||
- [todo] treat windows \r\n separately from \n
|
||||
!- [todo] provide type information
|
||||
|
||||
Details
|
||||
-generated results are trimmed up to 1 leading/trailing space
|
||||
-prefixes are cached up to 1 trailing newline
|
||||
-
|
||||
*/
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
type AutocompletionStatus = 'pending' | 'finished' | 'error';
|
||||
type Autocompletion = {
|
||||
id: number,
|
||||
prefix: string,
|
||||
suffix: string,
|
||||
startTime: number,
|
||||
endTime: number | undefined,
|
||||
abortRef: AbortRef,
|
||||
status: AutocompletionStatus,
|
||||
llmPromise: Promise<string> | undefined,
|
||||
result: string,
|
||||
}
|
||||
|
||||
const DEBOUNCE_TIME = 500
|
||||
const TIMEOUT_TIME = 60000
|
||||
const MAX_CACHE_SIZE = 20
|
||||
const MAX_PENDING_REQUESTS = 2
|
||||
|
||||
// postprocesses the result
|
||||
const postprocessResult = (result: string) => {
|
||||
|
||||
console.log('result: ', JSON.stringify(result))
|
||||
|
||||
// trim all whitespace except for a single leading/trailing space
|
||||
const hasLeadingSpace = result.startsWith(' ');
|
||||
const hasTrailingSpace = result.endsWith(' ');
|
||||
return (hasLeadingSpace ? ' ' : '')
|
||||
+ result.trim()
|
||||
+ (hasTrailingSpace ? ' ' : '');
|
||||
|
||||
}
|
||||
|
||||
const extractCodeFromResult = (result: string) => {
|
||||
|
||||
// extract the code between triple backticks
|
||||
const parts = result.split(/```(?:\s*\w+)?\n?/);
|
||||
|
||||
// if there is no ``` then return the raw result
|
||||
if (parts.length === 1) {
|
||||
return result;
|
||||
}
|
||||
|
||||
// else return the code between the triple backticks
|
||||
return parts[1]
|
||||
|
||||
}
|
||||
|
||||
// trims the end of the prefix to improve cache hit rate
|
||||
const trimPrefix = (prefix: string) => {
|
||||
const trimmedPrefix = prefix.trimEnd()
|
||||
const trailingEnd = prefix.substring(trimmedPrefix.length)
|
||||
|
||||
// keep only a single trailing newline
|
||||
if (trailingEnd.includes('\n')) {
|
||||
return trimmedPrefix + '\n'
|
||||
}
|
||||
|
||||
// else ignore all spaces and return the trimmed prefix
|
||||
return trimmedPrefix
|
||||
}
|
||||
|
||||
function getStringUpToUnbalancedParenthesis(s: string, prefixToTheLeft: string): string {
|
||||
|
||||
const pairs: Record<string, string> = { ')': '(', '}': '{', ']': '[' };
|
||||
|
||||
// todo find first open bracket in prefix and get all brackets beyond it in prefix
|
||||
// get all bracets in prefix
|
||||
let stack: string[] = []
|
||||
const firstOpenIdx = prefixToTheLeft.search(/[[({]/);
|
||||
if (firstOpenIdx !== -1) stack = prefixToTheLeft.slice(firstOpenIdx).split('').filter(c => '()[]{}'.includes(c))
|
||||
|
||||
// Iterate through each character
|
||||
for (let i = 0; i < s.length; i++) {
|
||||
const char = s[i];
|
||||
|
||||
if (char === '(' || char === '{' || char === '[') { stack.push(char); }
|
||||
else if (char === ')' || char === '}' || char === ']') {
|
||||
if (stack.length === 0 || stack.pop() !== pairs[char]) { return s.substring(0, i); }
|
||||
}
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
// finds the text in the autocompletion to display, assuming the prefix is already matched
|
||||
// example:
|
||||
// originalPrefix = abcd
|
||||
// generatedMiddle = efgh
|
||||
// originalSuffix = ijkl
|
||||
// the user has typed "ef" so prefix = abcdef
|
||||
// we want to return the rest of the generatedMiddle, which is "gh"
|
||||
const toInlineCompletion = ({ prefix, suffix, autocompletion, position }: { prefix: string, suffix: string, autocompletion: Autocompletion, position: vscode.Position }): vscode.InlineCompletionItem => {
|
||||
const originalPrefix = autocompletion.prefix
|
||||
const generatedMiddle = autocompletion.result
|
||||
|
||||
const trimmedOriginalPrefix = trimPrefix(originalPrefix)
|
||||
const trimmedCurrentPrefix = trimPrefix(prefix)
|
||||
|
||||
const suffixLines = suffix.split('\n')
|
||||
const prefixLines = trimmedCurrentPrefix.split('\n')
|
||||
const suffixToTheRightOfCursor = suffixLines[0].trim()
|
||||
const prefixToTheLeftOfCursor = prefixLines[prefixLines.length - 1].trim()
|
||||
|
||||
const generatedLines = generatedMiddle.split('\n')
|
||||
|
||||
// compute startIdx
|
||||
let startIdx = trimmedCurrentPrefix.length - trimmedOriginalPrefix.length
|
||||
if (startIdx < 0) {
|
||||
return new vscode.InlineCompletionItem('')
|
||||
}
|
||||
|
||||
// compute endIdx
|
||||
// hacks to get the suffix to render properly with lower quality models
|
||||
// if the generated text matches with the suffix on the current line, stop
|
||||
let endIdx: number | undefined = generatedMiddle.length // exclusive bounds
|
||||
|
||||
if (suffixToTheRightOfCursor !== '') { // completing in the middle of a line
|
||||
console.log('1')
|
||||
// complete until there is a match
|
||||
const matchIndex = generatedMiddle.lastIndexOf(suffixToTheRightOfCursor[0])
|
||||
if (matchIndex > 0) { endIdx = matchIndex }
|
||||
}
|
||||
|
||||
if (prefixToTheLeftOfCursor !== '') { // completing the end of a line
|
||||
console.log('2')
|
||||
// show a single line
|
||||
const newlineIdx = generatedMiddle.indexOf('\n')
|
||||
if (newlineIdx > -1) { endIdx = newlineIdx }
|
||||
}
|
||||
|
||||
// // if a generated line matches with a suffix line, stop
|
||||
// if (suffixLines.length > 1) {
|
||||
// console.log('3')
|
||||
// const lines = []
|
||||
// for (const generatedLine of generatedLines) {
|
||||
// if (suffixLines.slice(0, 10).some(suffixLine =>
|
||||
// generatedLine.trim() !== '' && suffixLine.trim() !== ''
|
||||
// && generatedLine.trim().startsWith(suffixLine.trim())
|
||||
// )) break;
|
||||
// lines.push(generatedLine)
|
||||
// }
|
||||
// endIdx = lines.join('\n').length // this is hacky, remove or refactor in future
|
||||
// }
|
||||
|
||||
let completionStr = generatedMiddle.slice(startIdx, endIdx)
|
||||
|
||||
// filter out unbalanced parentheses
|
||||
console.log('completionStrBeforeParens: ', JSON.stringify(completionStr))
|
||||
completionStr = getStringUpToUnbalancedParenthesis(completionStr, prefixLines.slice(-2).join('\n'))
|
||||
|
||||
console.log('originalCompletionStr: ', JSON.stringify(generatedMiddle.slice(startIdx)))
|
||||
console.log('finalCompletionStr: ', JSON.stringify(completionStr))
|
||||
|
||||
return new vscode.InlineCompletionItem(completionStr, new vscode.Range(position, position))
|
||||
|
||||
}
|
||||
|
||||
// returns whether this autocompletion is in the cache
|
||||
const doesPrefixMatchAutocompletion = ({ prefix, autocompletion }: { prefix: string, autocompletion: Autocompletion }): boolean => {
|
||||
|
||||
const originalPrefix = autocompletion.prefix
|
||||
const generatedMiddle = autocompletion.result
|
||||
const originalPrefixTrimmed = trimPrefix(originalPrefix)
|
||||
const currentPrefixTrimmed = trimPrefix(prefix)
|
||||
|
||||
if (currentPrefixTrimmed.length < originalPrefixTrimmed.length) {
|
||||
return false
|
||||
}
|
||||
|
||||
const isMatch = (originalPrefixTrimmed + generatedMiddle).startsWith(currentPrefixTrimmed)
|
||||
return isMatch
|
||||
|
||||
}
|
||||
|
||||
const getCompletionOptions = ({ prefix, suffix }: { prefix: string, suffix: string }) => {
|
||||
|
||||
const prefixLines = prefix.split('\n')
|
||||
const suffixLines = suffix.split('\n')
|
||||
|
||||
const prefixToLeftOfCursor = prefixLines.slice(-1)[0] ?? ''
|
||||
const suffixToRightOfCursor = suffixLines[0]
|
||||
|
||||
// default parameters
|
||||
let shouldGenerate = true
|
||||
let stopTokens: string[] = ['\n\n', '\r\n\r\n']
|
||||
|
||||
// specific cases
|
||||
if (suffixToRightOfCursor.trim() !== '') { // typing between something
|
||||
stopTokens = ['\n', '\r\n']
|
||||
}
|
||||
|
||||
// if (prefixToLeftOfCursor.trim() === '' && suffixToRightOfCursor.trim() === '') { // at an empty line
|
||||
// stopTokens = ['\n\n', '\r\n\r\n']
|
||||
// }
|
||||
|
||||
if (prefixToLeftOfCursor === '' || suffixToRightOfCursor === '') { // at beginning or end of line
|
||||
shouldGenerate = false
|
||||
}
|
||||
|
||||
console.log('shouldGenerate:', shouldGenerate, stopTokens)
|
||||
|
||||
return { shouldGenerate, stopTokens }
|
||||
|
||||
}
|
||||
|
||||
export class AutocompleteProvider implements vscode.InlineCompletionItemProvider {
|
||||
|
||||
private _extensionContext: vscode.ExtensionContext;
|
||||
|
||||
private _autocompletionId: number = 0;
|
||||
private _autocompletionsOfDocument: { [docUriStr: string]: LRUCache<number, Autocompletion> } = {}
|
||||
|
||||
private _lastCompletionTime = 0
|
||||
private _lastPrefix: string = ''
|
||||
|
||||
constructor(context: vscode.ExtensionContext) {
|
||||
this._extensionContext = context
|
||||
}
|
||||
|
||||
// used internally by vscode
|
||||
// fires after every keystroke and returns the completion to show
|
||||
async provideInlineCompletionItems(
|
||||
document: vscode.TextDocument,
|
||||
position: vscode.Position,
|
||||
context: vscode.InlineCompletionContext,
|
||||
token: vscode.CancellationToken,
|
||||
): Promise<vscode.InlineCompletionItem[]> {
|
||||
|
||||
const disabled = false
|
||||
if (disabled) { return []; }
|
||||
|
||||
const docUriStr = document.uri.toString()
|
||||
|
||||
|
||||
const fullText = document.getText();
|
||||
const cursorOffset = document.offsetAt(position);
|
||||
const prefix = fullText.substring(0, cursorOffset)
|
||||
const suffix = fullText.substring(cursorOffset)
|
||||
const voidConfig = getVoidConfigFromPartial(this._extensionContext.globalState.get('partialVoidConfig') ?? {})
|
||||
|
||||
// initialize cache and other variables
|
||||
// note that whenever an autocompletion is rejected, it is removed from cache
|
||||
if (!this._autocompletionsOfDocument[docUriStr]) {
|
||||
this._autocompletionsOfDocument[docUriStr] = new LRUCache<number, Autocompletion>({
|
||||
max: MAX_CACHE_SIZE,
|
||||
dispose: (autocompletion) => {
|
||||
autocompletion.abortRef.current()
|
||||
}
|
||||
})
|
||||
}
|
||||
this._lastPrefix = prefix
|
||||
|
||||
// get all pending autocompletions
|
||||
let __c = 0
|
||||
this._autocompletionsOfDocument[docUriStr].forEach(a => { if (a.status === 'pending') __c += 1 })
|
||||
console.log('pending: ' + __c)
|
||||
|
||||
// get autocompletion from cache
|
||||
let cachedAutocompletion: Autocompletion | undefined = undefined
|
||||
for (const autocompletion of this._autocompletionsOfDocument[docUriStr].values()) {
|
||||
// if the user's change matches up with the generated text
|
||||
if (doesPrefixMatchAutocompletion({ prefix, autocompletion })) {
|
||||
cachedAutocompletion = autocompletion
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// if there is a cached autocompletion, return it
|
||||
if (cachedAutocompletion) {
|
||||
|
||||
if (cachedAutocompletion.status === 'finished') {
|
||||
console.log('A1')
|
||||
|
||||
const inlineCompletion = toInlineCompletion({ autocompletion: cachedAutocompletion, prefix, suffix, position })
|
||||
return [inlineCompletion]
|
||||
|
||||
} else if (cachedAutocompletion.status === 'pending') {
|
||||
console.log('A2')
|
||||
|
||||
try {
|
||||
await cachedAutocompletion.llmPromise;
|
||||
console.log('id: ' + cachedAutocompletion.id)
|
||||
const inlineCompletion = toInlineCompletion({ autocompletion: cachedAutocompletion, prefix, suffix, position })
|
||||
return [inlineCompletion]
|
||||
|
||||
} catch (e) {
|
||||
this._autocompletionsOfDocument[docUriStr].delete(cachedAutocompletion.id)
|
||||
console.error('Error creating autocompletion (1): ' + e)
|
||||
}
|
||||
|
||||
} else if (cachedAutocompletion.status === 'error') {
|
||||
console.log('A3')
|
||||
}
|
||||
|
||||
return []
|
||||
}
|
||||
|
||||
// else if no more typing happens, then go forwards with the request
|
||||
// wait DEBOUNCE_TIME for the user to stop typing
|
||||
const thisTime = Date.now()
|
||||
this._lastCompletionTime = thisTime
|
||||
const didTypingHappenDuringDebounce = await new Promise((resolve, reject) =>
|
||||
setTimeout(() => {
|
||||
if (this._lastCompletionTime === thisTime) {
|
||||
resolve(false)
|
||||
} else {
|
||||
resolve(true)
|
||||
}
|
||||
}, DEBOUNCE_TIME)
|
||||
)
|
||||
|
||||
// if more typing happened, then do not go forwards with the request
|
||||
if (didTypingHappenDuringDebounce) {
|
||||
return []
|
||||
}
|
||||
|
||||
console.log('B')
|
||||
|
||||
// if there are too many pending requests, cancel the oldest one
|
||||
let numPending = 0
|
||||
let oldestPending: Autocompletion | undefined = undefined
|
||||
for (const autocompletion of this._autocompletionsOfDocument[docUriStr].values()) {
|
||||
if (autocompletion.status === 'pending') {
|
||||
numPending += 1
|
||||
if (oldestPending === undefined) {
|
||||
oldestPending = autocompletion
|
||||
}
|
||||
if (numPending >= MAX_PENDING_REQUESTS) {
|
||||
// cancel the oldest pending request and remove it from cache
|
||||
this._autocompletionsOfDocument[docUriStr].delete(oldestPending.id)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const { shouldGenerate, stopTokens } = getCompletionOptions({ prefix, suffix })
|
||||
|
||||
if (!shouldGenerate) return []
|
||||
|
||||
// create a new autocompletion and add it to cache
|
||||
const newAutocompletion: Autocompletion = {
|
||||
id: this._autocompletionId++,
|
||||
prefix: prefix,
|
||||
suffix: suffix,
|
||||
startTime: Date.now(),
|
||||
endTime: undefined,
|
||||
abortRef: { current: () => { } },
|
||||
status: 'pending',
|
||||
llmPromise: undefined,
|
||||
result: '',
|
||||
}
|
||||
|
||||
// set parameters of `newAutocompletion` appropriately
|
||||
newAutocompletion.llmPromise = new Promise((resolve, reject) => {
|
||||
|
||||
sendLLMMessage({
|
||||
mode: 'fim',
|
||||
fimInfo: { prefix, suffix },
|
||||
options: { stopTokens },
|
||||
onText: async (tokenStr, completionStr) => {
|
||||
|
||||
newAutocompletion.result = completionStr
|
||||
|
||||
// if generation doesn't match the prefix for the first few tokens generated, reject it
|
||||
if (!doesPrefixMatchAutocompletion({ prefix: this._lastPrefix, autocompletion: newAutocompletion })) {
|
||||
reject('LLM response did not match user\'s text.')
|
||||
}
|
||||
},
|
||||
onFinalMessage: (finalMessage) => {
|
||||
|
||||
// newAutocompletion.prefix = prefix
|
||||
// newAutocompletion.suffix = suffix
|
||||
// newAutocompletion.startTime = Date.now()
|
||||
newAutocompletion.endTime = Date.now()
|
||||
// newAutocompletion.abortRef = { current: () => { } }
|
||||
newAutocompletion.status = 'finished'
|
||||
// newAutocompletion.promise = undefined
|
||||
newAutocompletion.result = postprocessResult(extractCodeFromResult(finalMessage))
|
||||
|
||||
resolve(newAutocompletion.result)
|
||||
|
||||
},
|
||||
onError: (e) => {
|
||||
newAutocompletion.endTime = Date.now()
|
||||
newAutocompletion.status = 'error'
|
||||
reject(e)
|
||||
},
|
||||
voidConfig,
|
||||
abortRef: newAutocompletion.abortRef,
|
||||
})
|
||||
|
||||
// if the request hasnt resolved in TIMEOUT_TIME seconds, reject it
|
||||
setTimeout(() => {
|
||||
if (newAutocompletion.status === 'pending') {
|
||||
reject('Timeout receiving message to LLM.')
|
||||
}
|
||||
}, TIMEOUT_TIME)
|
||||
|
||||
|
||||
})
|
||||
|
||||
// add autocompletion to cache
|
||||
this._autocompletionsOfDocument[docUriStr].set(newAutocompletion.id, newAutocompletion)
|
||||
|
||||
// show autocompletion
|
||||
try {
|
||||
await newAutocompletion.llmPromise
|
||||
console.log('id: ' + newAutocompletion.id)
|
||||
|
||||
const inlineCompletion = toInlineCompletion({ autocompletion: newAutocompletion, prefix, suffix, position })
|
||||
return [inlineCompletion]
|
||||
|
||||
} catch (e) {
|
||||
this._autocompletionsOfDocument[docUriStr].delete(newAutocompletion.id)
|
||||
console.error('Error creating autocompletion (2): ' + e)
|
||||
return []
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -1,14 +1,20 @@
|
||||
{
|
||||
"name": "code-oss-dev",
|
||||
"name": "void-dev",
|
||||
"productName": "Void",
|
||||
"version": "1.94.0",
|
||||
"distro": "ffcc24343ac46468a625666e5b9e673971dd1a1f",
|
||||
"distro": "this is a commit number if we want to publish on npm",
|
||||
"homepage": "https://voideditor.com",
|
||||
"author": {
|
||||
"name": "Microsoft Corporation"
|
||||
"name": "Glass Devtools, Inc.",
|
||||
"email": "andrew@voideditor.com"
|
||||
},
|
||||
"license": "MIT",
|
||||
"main": "./out/main",
|
||||
"private": true,
|
||||
"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",
|
||||
@@ -20,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",
|
||||
@@ -72,9 +78,15 @@
|
||||
"update-build-ts-version": "npm install typescript@next && tsc -p ./build/tsconfig.build.json"
|
||||
},
|
||||
"dependencies": {
|
||||
"@anthropic-ai/sdk": "^0.32.1",
|
||||
"@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",
|
||||
"@vscode/deviceid": "^0.1.1",
|
||||
"@vscode/iconv-lite-umd": "0.7.0",
|
||||
"@vscode/policy-watcher": "^1.1.4",
|
||||
@@ -97,6 +109,9 @@
|
||||
"@xterm/addon-webgl": "^0.19.0-beta.64",
|
||||
"@xterm/headless": "^5.6.0-beta.64",
|
||||
"@xterm/xterm": "^5.6.0-beta.64",
|
||||
"ajv": "^8.17.1",
|
||||
"diff": "^7.0.0",
|
||||
"groq-sdk": "^0.9.0",
|
||||
"http-proxy-agent": "^7.0.0",
|
||||
"https-proxy-agent": "^7.0.2",
|
||||
"jschardet": "3.1.3",
|
||||
@@ -107,23 +122,29 @@
|
||||
"native-keymap": "^3.3.5",
|
||||
"native-watchdog": "^1.4.1",
|
||||
"node-pty": "1.1.0-beta21",
|
||||
"ollama": "^0.5.11",
|
||||
"open": "^8.4.2",
|
||||
"openai": "^4.76.1",
|
||||
"posthog-node": "^4.3.1",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-syntax-highlighter": "^15.6.1",
|
||||
"tas-client-umd": "0.2.0",
|
||||
"v8-inspect-profiler": "^0.1.1",
|
||||
"vscode-oniguruma": "1.7.0",
|
||||
"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": {
|
||||
"@anthropic-ai/sdk": "^0.32.1",
|
||||
"@google/generative-ai": "^0.21.0",
|
||||
"@playwright/test": "^1.46.1",
|
||||
"@swc/core": "1.3.62",
|
||||
"@types/cookie": "^0.3.3",
|
||||
"@types/debug": "^4.1.5",
|
||||
"@types/diff": "^6.0.0",
|
||||
"@types/eslint": "^9.6.1",
|
||||
"@types/gulp-svgmin": "^1.2.1",
|
||||
"@types/http-proxy-agent": "^2.0.1",
|
||||
"@types/kerberos": "^1.1.2",
|
||||
@@ -164,7 +185,6 @@
|
||||
"cssnano": "^6.0.3",
|
||||
"debounce": "^1.0.0",
|
||||
"deemon": "^1.8.0",
|
||||
"diff": "^7.0.0",
|
||||
"electron": "30.5.1",
|
||||
"eslint": "8.36.0",
|
||||
"eslint-plugin-header": "3.1.1",
|
||||
@@ -207,9 +227,9 @@
|
||||
"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",
|
||||
"ollama": "^0.5.9",
|
||||
"openai": "^4.71.1",
|
||||
"opn": "^6.0.0",
|
||||
"original-fs": "^1.2.0",
|
||||
"os-browserify": "^0.3.0",
|
||||
@@ -217,14 +237,10 @@
|
||||
"path-browserify": "^1.0.1",
|
||||
"postcss": "^8.4.33",
|
||||
"postcss-nesting": "^12.0.2",
|
||||
"posthog-js": "^1.184.2",
|
||||
"pump": "^1.0.1",
|
||||
"rcedit": "^1.1.0",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-syntax-highlighter": "^15.6.1",
|
||||
"rimraf": "^2.7.1",
|
||||
"scope-tailwind": "^1.0.1",
|
||||
"scope-tailwind": "^1.0.5",
|
||||
"sinon": "^12.0.1",
|
||||
"sinon-test": "^3.1.3",
|
||||
"source-map": "0.6.1",
|
||||
|
||||
@@ -1,84 +1,38 @@
|
||||
{
|
||||
"nameShort": "Void",
|
||||
"nameLong": "Void",
|
||||
"applicationName": "code-oss",
|
||||
"dataFolderName": ".vscode-oss",
|
||||
"win32MutexName": "vscodeoss",
|
||||
"applicationName": "void",
|
||||
"dataFolderName": ".void-editor",
|
||||
"win32MutexName": "voideditor",
|
||||
"licenseName": "MIT",
|
||||
"licenseUrl": "https://github.com/microsoft/vscode/blob/main/LICENSE.txt",
|
||||
"serverLicenseUrl": "https://github.com/microsoft/vscode/blob/main/LICENSE.txt",
|
||||
"licenseUrl": "https://github.com/voideditor/void/blob/main/LICENSE.txt",
|
||||
"serverLicenseUrl": "https://github.com/voideditor/void/blob/main/LICENSE.txt",
|
||||
"serverGreeting": [],
|
||||
"serverLicense": [],
|
||||
"serverLicensePrompt": "",
|
||||
"serverApplicationName": "code-server-oss",
|
||||
"serverDataFolderName": ".vscode-server-oss",
|
||||
"tunnelApplicationName": "code-tunnel-oss",
|
||||
"win32DirName": "Microsoft Code OSS",
|
||||
"win32NameVersion": "Microsoft Code OSS",
|
||||
"win32RegValueName": "CodeOSS",
|
||||
"win32x64AppId": "{{D77B7E06-80BA-4137-BCF4-654B95CCEBC5}",
|
||||
"win32arm64AppId": "{{D1ACE434-89C5-48D1-88D3-E2991DF85475}",
|
||||
"win32x64UserAppId": "{{CC6B787D-37A0-49E8-AE24-8559A032BE0C}",
|
||||
"win32arm64UserAppId": "{{3AEBF0C8-F733-4AD4-BADE-FDB816D53D7B}",
|
||||
"win32AppUserModelId": "Microsoft.CodeOSS",
|
||||
"win32ShellNameShort": "C&ode - OSS",
|
||||
"win32TunnelServiceMutex": "vscodeoss-tunnelservice",
|
||||
"win32TunnelMutex": "vscodeoss-tunnel",
|
||||
"darwinBundleIdentifier": "com.visualstudio.code.oss",
|
||||
"linuxIconName": "code-oss",
|
||||
"serverApplicationName": "void-server",
|
||||
"serverDataFolderName": ".void-server",
|
||||
"tunnelApplicationName": "void-tunnel",
|
||||
"win32DirName": "Void",
|
||||
"win32NameVersion": "Void",
|
||||
"win32RegValueName": "VoidEditor",
|
||||
"win32x64AppId": "{{9D394D01-1728-45A7-B997-A6C82C5452C3}",
|
||||
"win32arm64AppId": "{{0668DD58-2BDE-4101-8CDA-40252DF8875D}",
|
||||
"win32x64UserAppId": "{{8BED5DC1-6C55-46E6-9FE6-18F7E6F7C7F1}",
|
||||
"win32arm64UserAppId": "{{F6C87466-BC82-4A8F-B0FF-18CA366BA4D8}",
|
||||
"win32AppUserModelId": "Void.Editor",
|
||||
"win32ShellNameShort": "V&oid",
|
||||
"win32TunnelServiceMutex": "void-tunnelservice",
|
||||
"win32TunnelMutex": "void-tunnel",
|
||||
"darwinBundleIdentifier": "com.voideditor.code",
|
||||
"linuxIconName": "void-editor",
|
||||
"licenseFileName": "LICENSE.txt",
|
||||
"reportIssueUrl": "https://github.com/microsoft/vscode/issues/new",
|
||||
"reportIssueUrl": "https://github.com/voideditor/void/issues/new",
|
||||
"nodejsRepository": "https://nodejs.org",
|
||||
"urlProtocol": "code-oss",
|
||||
"webviewContentExternalBaseUrlTemplate": "https://{{uuid}}.vscode-cdn.net/insider/ef65ac1ba57f57f2a3961bfe94aa20481caca4c6/out/vs/workbench/contrib/webview/browser/pre/",
|
||||
"builtInExtensions": [
|
||||
{
|
||||
"name": "ms-vscode.js-debug-companion",
|
||||
"version": "1.1.3",
|
||||
"sha256": "7380a890787452f14b2db7835dfa94de538caf358ebc263f9d46dd68ac52de93",
|
||||
"repo": "https://github.com/microsoft/vscode-js-debug-companion",
|
||||
"metadata": {
|
||||
"id": "99cb0b7f-7354-4278-b8da-6cc79972169d",
|
||||
"publisherId": {
|
||||
"publisherId": "5f5636e7-69ed-4afe-b5d6-8d231fb3d3ee",
|
||||
"publisherName": "ms-vscode",
|
||||
"displayName": "Microsoft",
|
||||
"flags": "verified"
|
||||
},
|
||||
"publisherDisplayName": "Microsoft"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "ms-vscode.js-debug",
|
||||
"version": "1.93.0",
|
||||
"sha256": "9339cb8e6b77f554df54d79e71f533279cb76b0f9b04c207f633bfd507442b6a",
|
||||
"repo": "https://github.com/microsoft/vscode-js-debug",
|
||||
"metadata": {
|
||||
"id": "25629058-ddac-4e17-abba-74678e126c5d",
|
||||
"publisherId": {
|
||||
"publisherId": "5f5636e7-69ed-4afe-b5d6-8d231fb3d3ee",
|
||||
"publisherName": "ms-vscode",
|
||||
"displayName": "Microsoft",
|
||||
"flags": "verified"
|
||||
},
|
||||
"publisherDisplayName": "Microsoft"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "ms-vscode.vscode-js-profile-table",
|
||||
"version": "1.0.9",
|
||||
"sha256": "3b62ee4276a2bbea3fe230f94b1d5edd915b05966090ea56f882e1e0ab53e1a6",
|
||||
"repo": "https://github.com/microsoft/vscode-js-profile-visualizer",
|
||||
"metadata": {
|
||||
"id": "7e52b41b-71ad-457b-ab7e-0620f1fc4feb",
|
||||
"publisherId": {
|
||||
"publisherId": "5f5636e7-69ed-4afe-b5d6-8d231fb3d3ee",
|
||||
"publisherName": "ms-vscode",
|
||||
"displayName": "Microsoft",
|
||||
"flags": "verified"
|
||||
},
|
||||
"publisherDisplayName": "Microsoft"
|
||||
}
|
||||
}
|
||||
]
|
||||
"urlProtocol": "void-editor",
|
||||
"extensionsGallery": {
|
||||
"serviceUrl": "https://open-vsx.org/vscode/gallery",
|
||||
"itemUrl": "https://open-vsx.org/vscode/item"
|
||||
},
|
||||
"builtInExtensions": []
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
@@ -396,6 +397,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",
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
"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",
|
||||
|
||||
|
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 813 KiB |
|
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 254 KiB |
|
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 254 KiB |
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 29 KiB |
@@ -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: 89 KiB After Width: | Height: | Size: 173 KiB |
|
Before Width: | Height: | Size: 395 B After Width: | Height: | Size: 5.9 KiB |
|
Before Width: | Height: | Size: 338 B After Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 1.4 MiB |
|
After Width: | Height: | Size: 795 KiB |
@@ -123,7 +123,7 @@ protocol.registerSchemesAsPrivileged([
|
||||
},
|
||||
{
|
||||
scheme: 'vscode-file',
|
||||
privileges: { secure: true, standard: true, supportFetchAPI: true, corsEnabled: true, codeCache: true }
|
||||
privileges: { secure: true, standard: true, supportFetchAPI: true, corsEnabled: true, codeCache: true, }
|
||||
}
|
||||
]);
|
||||
|
||||
|
||||
@@ -52,10 +52,10 @@
|
||||
"./typings",
|
||||
"./vs/**/*.ts",
|
||||
"vscode-dts/vscode.proposed.*.d.ts",
|
||||
"vscode-dts/vscode.d.ts"
|
||||
"vscode-dts/vscode.d.ts",
|
||||
|
||||
// Void added these:
|
||||
// "./vs/**/*.tsx",
|
||||
// "./vs/workbench/contrib/void/browser/**.tsx",
|
||||
// "./vs/**/*.d.mts",
|
||||
]
|
||||
}
|
||||
|
||||
@@ -159,7 +159,7 @@ export class InputBox extends Widget {
|
||||
this.scrollableElement = new ScrollableElement(this.element, { vertical: ScrollbarVisibility.Auto });
|
||||
|
||||
if (this.options.flexibleWidth) {
|
||||
this.input.setAttribute('wrap', 'off');
|
||||
this.input.setAttribute('wrap', 'on');
|
||||
this.mirror.style.whiteSpace = 'pre';
|
||||
this.mirror.style.wordWrap = 'initial';
|
||||
}
|
||||
|
||||
@@ -121,8 +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 { 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).
|
||||
@@ -1103,6 +1106,10 @@ export class CodeApplication extends Disposable {
|
||||
services.set(ITelemetryService, NullTelemetryService);
|
||||
}
|
||||
|
||||
// Void main process services (required for services with a channel for comm between browser and electron-main (node))
|
||||
services.set(IMetricsService, new SyncDescriptor(MetricsMainService, undefined, false));
|
||||
services.set(IVoidUpdateService, new SyncDescriptor(VoidMainUpdateService, undefined, false));
|
||||
|
||||
// Default Extensions Profile Init
|
||||
services.set(IExtensionsProfileScannerService, new SyncDescriptor(ExtensionsProfileScannerService, undefined, true));
|
||||
services.set(IExtensionsScannerService, new SyncDescriptor(ExtensionsScannerService, undefined, true));
|
||||
@@ -1237,10 +1244,15 @@ export class CodeApplication extends Disposable {
|
||||
mainProcessElectronServer.registerChannel('logger', loggerChannel);
|
||||
sharedProcessClient.then(client => client.registerChannel('logger', loggerChannel));
|
||||
|
||||
// Void
|
||||
// const sendLLMMessageChannel = ProxyChannel.fromService(accessor.get(ISendLLMMessageService), disposables);
|
||||
const sendLLMMessageChannel = new LLMMessageChannel();
|
||||
mainProcessElectronServer.registerChannel('void-channel-sendLLMMessage', sendLLMMessageChannel);
|
||||
// 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);
|
||||
|
||||
// Extension Host Debug Broadcasting
|
||||
const electronExtensionHostDebugBroadcastChannel = new ElectronExtensionHostDebugBroadcastChannel(accessor.get(IWindowsMainService));
|
||||
|
||||
@@ -6,6 +6,11 @@ import { ICodeEditor, IViewZone } from '../../editorBrowser.js';
|
||||
import { IRange } from '../../../common/core/range.js';
|
||||
import { EditorOption } from '../../../common/config/editorOptions.js';
|
||||
|
||||
|
||||
// THIS FILE IS OLD + UNUSED!!!
|
||||
|
||||
// SEE inlineDiffsService.ts INSTEAD.
|
||||
|
||||
export interface IInlineDiffService {
|
||||
readonly _serviceBrand: undefined;
|
||||
addDiff(editor: ICodeEditor, originalText: string, modifiedRange: IRange): void;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -22,7 +22,7 @@ export class ExpandLineSelectionAction extends EditorAction {
|
||||
kbOpts: {
|
||||
weight: KeybindingWeight.EditorCore,
|
||||
kbExpr: EditorContextKeys.textInputFocus,
|
||||
primary: KeyMod.CtrlCmd | KeyCode.KeyL
|
||||
primary: KeyMod.CtrlCmd | KeyCode.KeyM // Void changed this to Cmd+M
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
@@ -15,7 +16,10 @@ import { IRequestService } from '../../request/common/request.js';
|
||||
import { AvailableForDownload, DisablementReason, IUpdateService, State, StateType, UpdateType } from '../common/update.js';
|
||||
|
||||
export function createUpdateURL(platform: string, quality: string, productService: IProductService): string {
|
||||
return `${productService.updateUrl}/api/update/${platform}/${quality}/${productService.commit}`;
|
||||
// return `https://voideditor.dev/api/update/${platform}/stable`;
|
||||
// return `${productService.updateUrl}/api/update/${platform}/${quality}/${productService.commit}`;
|
||||
// https://github.com/VSCodium/update-api
|
||||
return `https://updates.voideditor.dev/api/update/${platform}/${quality}/${productService.commit}`;
|
||||
}
|
||||
|
||||
export type UpdateNotAvailableClassification = {
|
||||
@@ -57,7 +61,7 @@ export abstract class AbstractUpdateService implements IUpdateService {
|
||||
@IEnvironmentMainService private readonly environmentMainService: IEnvironmentMainService,
|
||||
@IRequestService protected requestService: IRequestService,
|
||||
@ILogService protected logService: ILogService,
|
||||
@IProductService protected readonly productService: IProductService
|
||||
@IProductService protected readonly productService: IProductService,
|
||||
) {
|
||||
lifecycleMainService.when(LifecycleMainPhase.AfterWindowOpen)
|
||||
.finally(() => this.initialize());
|
||||
@@ -70,75 +74,35 @@ export abstract class AbstractUpdateService implements IUpdateService {
|
||||
*/
|
||||
protected async initialize(): Promise<void> {
|
||||
if (!this.environmentMainService.isBuilt) {
|
||||
console.log('is NOT built, canceling update service')
|
||||
this.setState(State.Disabled(DisablementReason.NotBuilt));
|
||||
return; // updates are never enabled when running out of sources
|
||||
}
|
||||
console.log('is built, continuing with update service')
|
||||
|
||||
if (this.environmentMainService.disableUpdates) {
|
||||
this.setState(State.Disabled(DisablementReason.DisabledByEnvironment));
|
||||
this.logService.info('update#ctor - updates are disabled by the environment');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.productService.updateUrl || !this.productService.commit) {
|
||||
this.setState(State.Disabled(DisablementReason.MissingConfiguration));
|
||||
this.logService.info('update#ctor - updates are disabled as there is no update URL');
|
||||
return;
|
||||
}
|
||||
|
||||
const updateMode = this.configurationService.getValue<'none' | 'manual' | 'start' | 'default'>('update.mode');
|
||||
const quality = this.getProductQuality(updateMode);
|
||||
|
||||
if (!quality) {
|
||||
this.setState(State.Disabled(DisablementReason.ManuallyDisabled));
|
||||
this.logService.info('update#ctor - updates are disabled by user preference');
|
||||
return;
|
||||
}
|
||||
|
||||
this.url = this.buildUpdateFeedUrl(quality);
|
||||
this.url = this.doBuildUpdateFeedUrl('stable');
|
||||
if (!this.url) {
|
||||
this.setState(State.Disabled(DisablementReason.InvalidConfiguration));
|
||||
this.logService.info('update#ctor - updates are disabled as the update URL is badly formed');
|
||||
return;
|
||||
}
|
||||
|
||||
// hidden setting
|
||||
if (this.configurationService.getValue<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 scheduleCheckForUpdates(delay = 60 * 60 * 1000): Promise<void> {
|
||||
return timeout(delay)
|
||||
.then(() => this.checkForUpdates(false))
|
||||
.then(() => {
|
||||
// Check again after 1 hour
|
||||
return this.scheduleCheckForUpdates(60 * 60 * 1000);
|
||||
});
|
||||
}
|
||||
// private async scheduleCheckForUpdates(delay = 60 * 60 * 1000): Promise<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);
|
||||
@@ -160,6 +124,7 @@ export abstract class AbstractUpdateService implements IUpdateService {
|
||||
await this.doDownloadUpdate(this.state);
|
||||
}
|
||||
|
||||
// override implemented by windows and linux
|
||||
protected async doDownloadUpdate(state: AvailableForDownload): Promise<void> {
|
||||
// noop
|
||||
}
|
||||
@@ -174,6 +139,7 @@ export abstract class AbstractUpdateService implements IUpdateService {
|
||||
await this.doApplyUpdate();
|
||||
}
|
||||
|
||||
// windows overrides this
|
||||
protected async doApplyUpdate(): Promise<void> {
|
||||
// noop
|
||||
}
|
||||
@@ -236,6 +202,6 @@ export abstract class AbstractUpdateService implements IUpdateService {
|
||||
// noop
|
||||
}
|
||||
|
||||
protected abstract buildUpdateFeedUrl(quality: string): string | undefined;
|
||||
protected abstract doBuildUpdateFeedUrl(quality: string): string | undefined;
|
||||
protected abstract doCheckForUpdates(context: any): void;
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ export class DarwinUpdateService extends AbstractUpdateService implements IRelau
|
||||
@IEnvironmentMainService environmentMainService: IEnvironmentMainService,
|
||||
@IRequestService requestService: IRequestService,
|
||||
@ILogService logService: ILogService,
|
||||
@IProductService productService: IProductService
|
||||
@IProductService productService: IProductService,
|
||||
) {
|
||||
super(lifecycleMainService, configurationService, environmentMainService, requestService, logService, productService);
|
||||
|
||||
@@ -73,7 +73,7 @@ export class DarwinUpdateService extends AbstractUpdateService implements IRelau
|
||||
this.setState(State.Idle(UpdateType.Archive, message));
|
||||
}
|
||||
|
||||
protected buildUpdateFeedUrl(quality: string): string | undefined {
|
||||
protected doBuildUpdateFeedUrl(quality: string): string | undefined {
|
||||
let assetID: string;
|
||||
if (!this.productService.darwinUniversalAssetId) {
|
||||
assetID = process.arch === 'x64' ? 'darwin' : 'darwin-arm64';
|
||||
|
||||
@@ -30,7 +30,7 @@ export class LinuxUpdateService extends AbstractUpdateService {
|
||||
super(lifecycleMainService, configurationService, environmentMainService, requestService, logService, productService);
|
||||
}
|
||||
|
||||
protected buildUpdateFeedUrl(quality: string): string {
|
||||
protected doBuildUpdateFeedUrl(quality: string): string {
|
||||
return createUpdateURL(`linux-${process.arch}`, quality, this.productService);
|
||||
}
|
||||
|
||||
|
||||
@@ -67,7 +67,7 @@ export class Win32UpdateService extends AbstractUpdateService implements IRelaun
|
||||
@ILogService logService: ILogService,
|
||||
@IFileService private readonly fileService: IFileService,
|
||||
@INativeHostMainService private readonly nativeHostMainService: INativeHostMainService,
|
||||
@IProductService productService: IProductService
|
||||
@IProductService productService: IProductService,
|
||||
) {
|
||||
super(lifecycleMainService, configurationService, environmentMainService, requestService, logService, productService);
|
||||
|
||||
@@ -99,7 +99,7 @@ export class Win32UpdateService extends AbstractUpdateService implements IRelaun
|
||||
await super.initialize();
|
||||
}
|
||||
|
||||
protected buildUpdateFeedUrl(quality: string): string | undefined {
|
||||
protected doBuildUpdateFeedUrl(quality: string): string | undefined {
|
||||
let platform = `win32-${process.arch}`;
|
||||
|
||||
if (getUpdateType() === UpdateType.Archive) {
|
||||
|
||||
@@ -1,105 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Glass Devtools, Inc. All rights reserved.
|
||||
* Void Editor additions licensed under the AGPLv3 License.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { ProxyOnTextPayload, ProxyOnErrorPayload, ProxyOnFinalMessagePayload, LLMMessageServiceParams, ProxyLLMMessageParams, ProxyLLMMessageAbortParams } from '../common/llmMessageTypes.js';
|
||||
import { IChannel } from '../../../base/parts/ipc/common/ipc.js';
|
||||
import { IMainProcessService } from '../../ipc/common/mainProcessService.js';
|
||||
import { InstantiationType, registerSingleton } from '../../instantiation/common/extensions.js';
|
||||
import { generateUuid } from '../../../base/common/uuid.js';
|
||||
import { createDecorator } from '../../instantiation/common/instantiation.js';
|
||||
import { Event } from '../../../base/common/event.js';
|
||||
import { IDisposable } from '../../../base/common/lifecycle.js';
|
||||
|
||||
|
||||
// BROWSER IMPLEMENTATION OF SENDLLMMESSAGE
|
||||
export const ISendLLMMessageService = createDecorator<ISendLLMMessageService>('sendLLMMessageService');
|
||||
|
||||
// defines an interface that node/ creates and browser/ uses
|
||||
export interface ISendLLMMessageService {
|
||||
readonly _serviceBrand: undefined;
|
||||
sendLLMMessage: (params: LLMMessageServiceParams) => string;
|
||||
abort: (requestId: string) => void;
|
||||
}
|
||||
|
||||
|
||||
export class SendLLMMessageService implements ISendLLMMessageService {
|
||||
|
||||
readonly _serviceBrand: undefined;
|
||||
private readonly channel: IChannel;
|
||||
|
||||
private readonly _disposablesOfRequestId: Record<string, IDisposable[]> = {}
|
||||
|
||||
constructor(
|
||||
@IMainProcessService mainProcessService: IMainProcessService // used as a renderer (only usable on client side)
|
||||
) {
|
||||
|
||||
this.channel = mainProcessService.getChannel('void-channel-sendLLMMessage')
|
||||
// const service = ProxyChannel.toService<LLMMessageChannel>(mainProcessService.getChannel('void-channel-sendLLMMessage')); // lets you call it like a service, not needed here
|
||||
}
|
||||
|
||||
_addDisposable(requestId: string, disposable: IDisposable) {
|
||||
if (!this._disposablesOfRequestId[requestId]) {
|
||||
this._disposablesOfRequestId[requestId] = []
|
||||
}
|
||||
this._disposablesOfRequestId[requestId].push(disposable)
|
||||
}
|
||||
|
||||
|
||||
|
||||
sendLLMMessage(params: LLMMessageServiceParams) {
|
||||
const requestId_ = generateUuid();
|
||||
const { onText, onFinalMessage, onError, ...proxyParams } = params;
|
||||
|
||||
// listen for listenerName='onText' | 'onFinalMessage' | 'onError', and call the original function on it
|
||||
|
||||
const onTextEvent: Event<ProxyOnTextPayload> = this.channel.listen('onText')
|
||||
this._addDisposable(requestId_,
|
||||
onTextEvent(e => {
|
||||
if (requestId_ !== e.requestId) return;
|
||||
onText(e)
|
||||
})
|
||||
)
|
||||
|
||||
const onFinalMessageEvent: Event<ProxyOnFinalMessagePayload> = this.channel.listen('onFinalMessage')
|
||||
this._addDisposable(requestId_,
|
||||
onFinalMessageEvent(e => {
|
||||
if (requestId_ !== e.requestId) return;
|
||||
onFinalMessage(e)
|
||||
this._dispose(requestId_)
|
||||
})
|
||||
)
|
||||
|
||||
const onErrorEvent: Event<ProxyOnErrorPayload> = this.channel.listen('onError')
|
||||
this._addDisposable(requestId_,
|
||||
onErrorEvent(e => {
|
||||
if (requestId_ !== e.requestId) return;
|
||||
console.log('event onError', JSON.stringify(e))
|
||||
onError(e)
|
||||
this._dispose(requestId_)
|
||||
})
|
||||
)
|
||||
|
||||
// params will be stripped of all its functions
|
||||
this.channel.call('sendLLMMessage', { ...proxyParams, requestId: requestId_ } satisfies ProxyLLMMessageParams);
|
||||
|
||||
return requestId_
|
||||
}
|
||||
|
||||
private _dispose(requestId: string) {
|
||||
if (!(requestId in this._disposablesOfRequestId)) return
|
||||
for (const disposable of this._disposablesOfRequestId[requestId]) {
|
||||
disposable.dispose()
|
||||
}
|
||||
delete this._disposablesOfRequestId[requestId]
|
||||
}
|
||||
|
||||
abort(requestId: string) {
|
||||
this.channel.call('abort', { requestId } satisfies ProxyLLMMessageAbortParams);
|
||||
this._dispose(requestId)
|
||||
}
|
||||
}
|
||||
|
||||
registerSingleton(ISendLLMMessageService, SendLLMMessageService, InstantiationType.Delayed);
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Glass Devtools, Inc. All rights reserved.
|
||||
* Void Editor additions licensed under the AGPLv3 License.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { VoidConfig } from '../../../workbench/contrib/void/browser/registerConfig.js';
|
||||
|
||||
// ---------- type definitions ----------
|
||||
|
||||
export type OnText = (p: { newText: string, fullText: string }) => void
|
||||
|
||||
export type OnFinalMessage = (p: { fullText: string }) => void
|
||||
|
||||
export type OnError = (p: { error: Error | string }) => void
|
||||
|
||||
export type AbortRef = { current: (() => void) | null }
|
||||
|
||||
export type LLMMessage = {
|
||||
role: 'system' | 'user' | 'assistant';
|
||||
content: string;
|
||||
}
|
||||
|
||||
export type LLMMessageServiceParams = {
|
||||
onText: OnText;
|
||||
onFinalMessage: OnFinalMessage;
|
||||
onError: OnError;
|
||||
|
||||
messages: LLMMessage[];
|
||||
voidConfig: VoidConfig | null;
|
||||
|
||||
logging: {
|
||||
loggingName: string,
|
||||
};
|
||||
}
|
||||
|
||||
export type SendLLMMMessageParams = {
|
||||
onText: OnText;
|
||||
onFinalMessage: OnFinalMessage;
|
||||
onError: OnError;
|
||||
|
||||
messages: LLMMessage[];
|
||||
voidConfig: VoidConfig | null;
|
||||
|
||||
logging: {
|
||||
loggingName: string,
|
||||
};
|
||||
abortRef: AbortRef;
|
||||
}
|
||||
|
||||
// can't send functions across a proxy, use listeners instead
|
||||
export const listenerNames = ['onText', 'onFinalMessage', 'onError'] as const
|
||||
export type ProxyLLMMessageParams = Omit<LLMMessageServiceParams, typeof listenerNames[number]> & { requestId: string }
|
||||
|
||||
export type ProxyOnTextPayload = Parameters<OnText>[0] & { requestId: string }
|
||||
export type ProxyOnFinalMessagePayload = Parameters<OnFinalMessage>[0] & { requestId: string }
|
||||
export type ProxyOnErrorPayload = Parameters<OnError>[0] & { requestId: string }
|
||||
|
||||
export type ProxyLLMMessageAbortParams = { requestId: string }
|
||||
@@ -1,94 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Glass Devtools, Inc. All rights reserved.
|
||||
* Void Editor additions licensed under the AGPLv3 License.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
// this channel is registered in `app.ts`
|
||||
// code convention is to make a service responsible for this stuff, and not a channel, but this is simpler.
|
||||
// you could create one instance in electron-main/my-service.ts and one in browser/my-service.ts (and define the interface IMyService in common/my-service.ts), but we just use a channel here
|
||||
// registerSingleton(ISendLLMMessageService, SendLLMMessageService, InstantiationType.Delayed);
|
||||
|
||||
import { IServerChannel } from '../../../base/parts/ipc/common/ipc.js';
|
||||
import { Emitter, Event } from '../../../base/common/event.js';
|
||||
import { sendLLMMessage } from '../../../workbench/contrib/void/browser/react/out/util/sendLLMMessage.js';
|
||||
import { listenerNames, ProxyOnTextPayload, ProxyOnErrorPayload, ProxyOnFinalMessagePayload, ProxyLLMMessageParams, AbortRef, SendLLMMMessageParams, ProxyLLMMessageAbortParams } from '../common/llmMessageTypes.js';
|
||||
|
||||
// NODE IMPLEMENTATION OF SENDLLMMESSAGE - calls sendLLMMessage() and returns listeners
|
||||
|
||||
export class LLMMessageChannel implements IServerChannel {
|
||||
private readonly _onText = new Emitter<ProxyOnTextPayload>();
|
||||
readonly onText = this._onText.event;
|
||||
|
||||
private readonly _onFinalMessage = new Emitter<ProxyOnFinalMessagePayload>();
|
||||
readonly onFinalMessage = this._onFinalMessage.event;
|
||||
|
||||
private readonly _onError = new Emitter<ProxyOnErrorPayload>();
|
||||
readonly onError = this._onError.event;
|
||||
|
||||
|
||||
private readonly _abortRefOfRequestId: Record<string, AbortRef> = {}
|
||||
|
||||
|
||||
constructor() { }
|
||||
|
||||
// browser uses this to listen for changes
|
||||
listen(_: unknown, event: typeof listenerNames[number]): Event<any> {
|
||||
if (event === 'onText') {
|
||||
return this.onText;
|
||||
}
|
||||
else if (event === 'onFinalMessage') {
|
||||
return this.onFinalMessage;
|
||||
}
|
||||
else if (event === 'onError') {
|
||||
return this.onError;
|
||||
}
|
||||
else {
|
||||
throw new Error(`Event not found: ${event}`);
|
||||
}
|
||||
}
|
||||
|
||||
// browser uses this to call
|
||||
async call(_: unknown, command: string, params: any): Promise<any> {
|
||||
|
||||
try {
|
||||
if (command === 'sendLLMMessage') {
|
||||
this._callSendLLMMessage(params)
|
||||
}
|
||||
else if (command === 'abort') {
|
||||
this._callAbort(params)
|
||||
}
|
||||
else {
|
||||
throw new Error(`Void sendLLM: command "${command}" not recognized.`)
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
console.log('llmMessageChannel: Call Error:', e)
|
||||
}
|
||||
}
|
||||
|
||||
// the only place sendLLMMessage is actually called
|
||||
private _callSendLLMMessage(params: ProxyLLMMessageParams) {
|
||||
const { requestId } = params;
|
||||
|
||||
if (!(requestId in this._abortRefOfRequestId))
|
||||
this._abortRefOfRequestId[requestId] = { current: null }
|
||||
|
||||
const mainThreadParams: SendLLMMMessageParams = {
|
||||
...params,
|
||||
onText: ({ newText, fullText }) => { this._onText.fire({ requestId, newText, fullText }); },
|
||||
onFinalMessage: ({ fullText }) => { this._onFinalMessage.fire({ requestId, fullText }); },
|
||||
onError: ({ error }) => { this._onError.fire({ requestId, error }); },
|
||||
abortRef: this._abortRefOfRequestId[requestId],
|
||||
}
|
||||
sendLLMMessage(mainThreadParams);
|
||||
}
|
||||
|
||||
private _callAbort(params: ProxyLLMMessageAbortParams) {
|
||||
const { requestId } = params;
|
||||
if (!(requestId in this._abortRefOfRequestId)) return
|
||||
this._abortRefOfRequestId[requestId].current?.()
|
||||
delete this._abortRefOfRequestId[requestId]
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -742,18 +742,6 @@ export class CodeWindow extends BaseWindow implements ICodeWindow {
|
||||
cb({ cancel: false, requestHeaders: Object.assign(details.requestHeaders, headers) });
|
||||
});
|
||||
|
||||
|
||||
// // Void: send from https://
|
||||
// this._win.webContents.session.webRequest.onBeforeSendHeaders({ urls }, async (details, cb) => {
|
||||
// // const voidConfig = this.voidConfigStateService.state.voidConfig
|
||||
// // const whichApi = voidConfig.default['whichApi']
|
||||
// const endpoint = 'http://127.' //string | undefined = voidConfig[whichApi as VoidConfigField].endpoint
|
||||
|
||||
// if (endpoint && details.url.startsWith(endpoint)) {
|
||||
// details.requestHeaders['Origin'] = 'https://app.voideditor.com'
|
||||
// }
|
||||
// cb({ cancel: false, requestHeaders: details.requestHeaders });
|
||||
// });
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
|
||||
@@ -239,7 +239,7 @@ MenuRegistry.appendMenuItems([{
|
||||
group: '3_workbench_layout_move',
|
||||
command: {
|
||||
id: ToggleSidebarPositionAction.ID,
|
||||
title: localize('move second sidebar left', "Move Secondary Side Bar Left")
|
||||
title: localize('move second sidebar left', "Move Void Side Bar Left")
|
||||
},
|
||||
when: ContextKeyExpr.and(ContextKeyExpr.notEquals('config.workbench.sideBar.location', 'right'), ContextKeyExpr.equals('viewLocation', ViewContainerLocationToString(ViewContainerLocation.AuxiliaryBar))),
|
||||
order: 1
|
||||
@@ -250,7 +250,7 @@ MenuRegistry.appendMenuItems([{
|
||||
group: '3_workbench_layout_move',
|
||||
command: {
|
||||
id: ToggleSidebarPositionAction.ID,
|
||||
title: localize('move second sidebar right', "Move Secondary Side Bar Right")
|
||||
title: localize('move second sidebar right', "Move Void Side Bar Right")
|
||||
},
|
||||
when: ContextKeyExpr.and(ContextKeyExpr.equals('config.workbench.sideBar.location', 'right'), ContextKeyExpr.equals('viewLocation', ViewContainerLocationToString(ViewContainerLocation.AuxiliaryBar))),
|
||||
order: 1
|
||||
@@ -949,7 +949,7 @@ registerAction2(class extends Action2 {
|
||||
if (!hasAddedView) {
|
||||
results.push({
|
||||
type: 'separator',
|
||||
label: localize('secondarySideBarContainer', "Secondary Side Bar / {0}", containerModel.title)
|
||||
label: localize('secondarySideBarContainer', "Void Side Bar / {0}", containerModel.title)
|
||||
});
|
||||
hasAddedView = true;
|
||||
}
|
||||
@@ -1056,7 +1056,7 @@ class MoveFocusedViewAction extends Action2 {
|
||||
if (!(isViewSolo && currentLocation === ViewContainerLocation.AuxiliaryBar)) {
|
||||
items.push({
|
||||
id: '_.auxiliarybar.newcontainer',
|
||||
label: localize('moveFocusedView.newContainerInSidePanel', "New Secondary Side Bar Entry")
|
||||
label: localize('moveFocusedView.newContainerInSidePanel', "New Void Side Bar Entry")
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1104,7 +1104,7 @@ class MoveFocusedViewAction extends Action2 {
|
||||
|
||||
items.push({
|
||||
type: 'separator',
|
||||
label: localize('secondarySideBar', "Secondary Side Bar")
|
||||
label: localize('secondarySideBar', "Void Side Bar")
|
||||
});
|
||||
|
||||
const pinnedAuxPanels = paneCompositePartService.getPinnedPaneCompositeIds(ViewContainerLocation.AuxiliaryBar);
|
||||
@@ -1386,7 +1386,7 @@ if (!isMacintosh || !isNative) {
|
||||
ToggleVisibilityActions.push(...[
|
||||
CreateToggleLayoutItem(ToggleActivityBarVisibilityActionId, ContextKeyExpr.notEquals('config.workbench.activityBar.location', 'hidden'), localize('activityBar', "Activity Bar"), { whenA: ContextKeyExpr.equals('config.workbench.sideBar.location', 'left'), iconA: activityBarLeftIcon, iconB: activityBarRightIcon }),
|
||||
CreateToggleLayoutItem(ToggleSidebarVisibilityAction.ID, SideBarVisibleContext, localize('sideBar', "Primary Side Bar"), { whenA: ContextKeyExpr.equals('config.workbench.sideBar.location', 'left'), iconA: panelLeftIcon, iconB: panelRightIcon }),
|
||||
CreateToggleLayoutItem(ToggleAuxiliaryBarAction.ID, AuxiliaryBarVisibleContext, localize('secondarySideBar', "Secondary Side Bar"), { whenA: ContextKeyExpr.equals('config.workbench.sideBar.location', 'left'), iconA: panelRightIcon, iconB: panelLeftIcon }),
|
||||
CreateToggleLayoutItem(ToggleAuxiliaryBarAction.ID, AuxiliaryBarVisibleContext, localize('secondarySideBar', "Void Side Bar"), { whenA: ContextKeyExpr.equals('config.workbench.sideBar.location', 'left'), iconA: panelRightIcon, iconB: panelLeftIcon }),
|
||||
CreateToggleLayoutItem(TogglePanelAction.ID, PanelVisibleContext, localize('panel', "Panel"), panelIcon),
|
||||
CreateToggleLayoutItem(ToggleStatusbarVisibilityAction.ID, ContextKeyExpr.equals('config.workbench.statusBar.visible', true), localize('statusBar', "Status Bar"), statusBarIcon),
|
||||
]);
|
||||
|
||||
@@ -2434,7 +2434,7 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi
|
||||
comment: 'Information about the layout of the workbench during statup';
|
||||
activityBarVisible: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether or the not the activity bar is visible' };
|
||||
sideBarVisible: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether or the not the primary side bar is visible' };
|
||||
auxiliaryBarVisible: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether or the not the secondary side bar is visible' };
|
||||
auxiliaryBarVisible: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether or the not the Void side bar is visible' };
|
||||
panelVisible: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether or the not the panel is visible' };
|
||||
statusbarVisible: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether or the not the status bar is visible' };
|
||||
sideBarPosition: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether the primary side bar is on the left or right' };
|
||||
@@ -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 |
@@ -26,7 +26,7 @@ const auxiliaryBarLeftOffIcon = registerIcon('auxiliarybar-left-off-layout-icon'
|
||||
export class ToggleAuxiliaryBarAction extends Action2 {
|
||||
|
||||
static readonly ID = 'workbench.action.toggleAuxiliaryBar';
|
||||
static readonly LABEL = localize2('toggleAuxiliaryBar', "Toggle Secondary Side Bar Visibility");
|
||||
static readonly LABEL = localize2('toggleAuxiliaryBar', "Toggle Void Side Bar Visibility");
|
||||
|
||||
constructor() {
|
||||
super({
|
||||
@@ -34,7 +34,7 @@ export class ToggleAuxiliaryBarAction extends Action2 {
|
||||
title: ToggleAuxiliaryBarAction.LABEL,
|
||||
toggled: {
|
||||
condition: AuxiliaryBarVisibleContext,
|
||||
title: localize('secondary sidebar', "Secondary Side Bar"),
|
||||
title: localize('secondary sidebar', "Void Side Bar"),
|
||||
mnemonicTitle: localize({ key: 'secondary sidebar mnemonic', comment: ['&& denotes a mnemonic'] }, "Secondary Si&&de Bar"),
|
||||
},
|
||||
|
||||
@@ -70,7 +70,7 @@ registerAction2(ToggleAuxiliaryBarAction);
|
||||
registerAction2(class FocusAuxiliaryBarAction extends Action2 {
|
||||
|
||||
static readonly ID = 'workbench.action.focusAuxiliaryBar';
|
||||
static readonly LABEL = localize2('focusAuxiliaryBar', "Focus into Secondary Side Bar");
|
||||
static readonly LABEL = localize2('focusAuxiliaryBar', "Focus into Void Side Bar");
|
||||
|
||||
constructor() {
|
||||
super({
|
||||
@@ -103,7 +103,7 @@ MenuRegistry.appendMenuItems([
|
||||
group: '0_workbench_toggles',
|
||||
command: {
|
||||
id: ToggleAuxiliaryBarAction.ID,
|
||||
title: localize('toggleSecondarySideBar', "Toggle Secondary Side Bar"),
|
||||
title: localize('toggleSecondarySideBar', "Toggle Void Side Bar"),
|
||||
toggled: { condition: AuxiliaryBarVisibleContext, icon: auxiliaryBarLeftIcon },
|
||||
icon: auxiliaryBarLeftOffIcon,
|
||||
},
|
||||
@@ -116,7 +116,7 @@ MenuRegistry.appendMenuItems([
|
||||
group: '0_workbench_toggles',
|
||||
command: {
|
||||
id: ToggleAuxiliaryBarAction.ID,
|
||||
title: localize('toggleSecondarySideBar', "Toggle Secondary Side Bar"),
|
||||
title: localize('toggleSecondarySideBar', "Toggle Void Side Bar"),
|
||||
toggled: { condition: AuxiliaryBarVisibleContext, icon: auxiliaryBarRightIcon },
|
||||
icon: auxiliaryBarRightOffIcon,
|
||||
},
|
||||
@@ -129,7 +129,7 @@ MenuRegistry.appendMenuItems([
|
||||
group: '3_workbench_layout_move',
|
||||
command: {
|
||||
id: ToggleAuxiliaryBarAction.ID,
|
||||
title: localize2('hideAuxiliaryBar', 'Hide Secondary Side Bar'),
|
||||
title: localize2('hideAuxiliaryBar', 'Hide Void Side Bar'),
|
||||
},
|
||||
when: ContextKeyExpr.and(AuxiliaryBarVisibleContext, ContextKeyExpr.equals('viewLocation', ViewContainerLocationToString(ViewContainerLocation.AuxiliaryBar))),
|
||||
order: 2
|
||||
|
||||
@@ -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;
|
||||
@@ -195,11 +195,13 @@ export class AuxiliaryBarPart extends AbstractPaneCompositePart {
|
||||
const positionActions: IAction[] = [];
|
||||
createAndFillInContextMenuActions(activityBarPositionMenu, { primary: [], secondary: positionActions });
|
||||
|
||||
|
||||
// appears when right click
|
||||
actions.push(...[
|
||||
new Separator(),
|
||||
new SubmenuAction('workbench.action.panel.position', localize('activity bar position', "Activity Bar Position"), positionActions),
|
||||
toAction({ id: ToggleSidebarPositionAction.ID, label: currentPositionRight ? localize('move second side bar left', "Move Secondary Side Bar Left") : localize('move second side bar right', "Move Secondary Side Bar Right"), run: () => this.commandService.executeCommand(ToggleSidebarPositionAction.ID) }),
|
||||
toAction({ id: ToggleAuxiliaryBarAction.ID, label: localize('hide second side bar', "Hide Secondary Side Bar"), run: () => this.commandService.executeCommand(ToggleAuxiliaryBarAction.ID) })
|
||||
toAction({ id: ToggleSidebarPositionAction.ID, label: currentPositionRight ? localize('move second side bar left', "Move Void Side Bar Left") : localize('move second side bar right', "Move Void Side Bar Right"), run: () => this.commandService.executeCommand(ToggleSidebarPositionAction.ID) }),
|
||||
toAction({ id: ToggleAuxiliaryBarAction.ID, label: localize('hide second side bar', "Hide Void Side Bar"), run: () => this.commandService.executeCommand(ToggleAuxiliaryBarAction.ID) })
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -432,7 +432,7 @@ export class UnpinEditorAction extends Action {
|
||||
label: string,
|
||||
@ICommandService private readonly commandService: ICommandService
|
||||
) {
|
||||
super(id, label, ThemeIcon.asClassName(Codicon.pinned));
|
||||
super(id, label, ThemeIcon.asClassName(Codicon.starFull));
|
||||
}
|
||||
|
||||
override run(context?: IEditorCommandsContext): Promise<void> {
|
||||
@@ -440,6 +440,24 @@ export class UnpinEditorAction extends Action {
|
||||
}
|
||||
}
|
||||
|
||||
export class PinEditorAction extends Action {
|
||||
|
||||
static readonly ID = 'workbench.action.pinEditor';
|
||||
static readonly LABEL = localize('pinEditor', "Pin Editor");
|
||||
|
||||
constructor(
|
||||
id: string,
|
||||
label: string,
|
||||
@ICommandService private readonly commandService: ICommandService
|
||||
) {
|
||||
super(id, label, ThemeIcon.asClassName(Codicon.star));
|
||||
}
|
||||
|
||||
override async run(context?: IEditorCommandsContext): Promise<void> {
|
||||
return this.commandService.executeCommand('workbench.action.pinEditor', undefined, context);
|
||||
}
|
||||
}
|
||||
|
||||
export class CloseEditorTabAction extends Action {
|
||||
|
||||
static readonly ID = 'workbench.action.closeActiveEditor';
|
||||
|
||||
@@ -4,82 +4,112 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { localize } from '../../../../nls.js';
|
||||
import { Disposable, DisposableStore } from '../../../../base/common/lifecycle.js';
|
||||
import { isMacintosh, isWeb, OS } from '../../../../base/common/platform.js';
|
||||
import { Disposable, DisposableStore, IDisposable } from '../../../../base/common/lifecycle.js';
|
||||
import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js';
|
||||
import { IWorkspaceContextService, WorkbenchState } from '../../../../platform/workspace/common/workspace.js';
|
||||
import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';
|
||||
import { append, clearNode, $, h } from '../../../../base/browser/dom.js';
|
||||
import { KeybindingLabel } from '../../../../base/browser/ui/keybindingLabel/keybindingLabel.js';
|
||||
import { CommandsRegistry } from '../../../../platform/commands/common/commands.js';
|
||||
import { ContextKeyExpr, ContextKeyExpression, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';
|
||||
import { defaultKeybindingLabelStyles } from '../../../../platform/theme/browser/defaultStyles.js';
|
||||
import { editorForeground, registerColor, transparent } from '../../../../platform/theme/common/colorRegistry.js';
|
||||
import { IThemeService } from '../../../../platform/theme/common/themeService.js';
|
||||
import { ColorScheme } from '../../../../platform/theme/common/theme.js';
|
||||
import { isRecentFolder, IWorkspacesService } from '../../../../platform/workspaces/common/workspaces.js';
|
||||
// import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';
|
||||
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 { 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.'));
|
||||
|
||||
interface WatermarkEntry {
|
||||
readonly text: string;
|
||||
readonly id: string;
|
||||
readonly mac?: boolean;
|
||||
readonly when?: ContextKeyExpression;
|
||||
}
|
||||
// interface WatermarkEntry {
|
||||
// readonly text: string;
|
||||
// readonly id: string;
|
||||
// readonly mac?: boolean;
|
||||
// readonly when?: ContextKeyExpression;
|
||||
// }
|
||||
|
||||
const showCommands: WatermarkEntry = { text: localize('watermark.showCommands', "Show All Commands"), id: 'workbench.action.showCommands' };
|
||||
const quickAccess: WatermarkEntry = { text: localize('watermark.quickAccess', "Go to File"), id: 'workbench.action.quickOpen' };
|
||||
const openFileNonMacOnly: WatermarkEntry = { text: localize('watermark.openFile', "Open File"), id: 'workbench.action.files.openFile', mac: false };
|
||||
const openFolderNonMacOnly: WatermarkEntry = { text: localize('watermark.openFolder', "Open Folder"), id: 'workbench.action.files.openFolder', mac: false };
|
||||
const openFileOrFolderMacOnly: WatermarkEntry = { text: localize('watermark.openFileFolder', "Open File or Folder"), id: 'workbench.action.files.openFileFolder', mac: true };
|
||||
const openRecent: WatermarkEntry = { text: localize('watermark.openRecent', "Open Recent"), id: 'workbench.action.openRecent' };
|
||||
const newUntitledFileMacOnly: WatermarkEntry = { text: localize('watermark.newUntitledFile', "New Untitled Text File"), id: 'workbench.action.files.newUntitledFile', mac: true };
|
||||
const findInFiles: WatermarkEntry = { text: localize('watermark.findInFiles', "Find in Files"), id: 'workbench.action.findInFiles' };
|
||||
const toggleTerminal: WatermarkEntry = { text: localize({ key: 'watermark.toggleTerminal', comment: ['toggle is a verb here'] }, "Toggle Terminal"), id: 'workbench.action.terminal.toggleTerminal', when: ContextKeyExpr.equals('terminalProcessSupported', true) };
|
||||
const startDebugging: WatermarkEntry = { text: localize('watermark.startDebugging', "Start Debugging"), id: 'workbench.action.debug.start', when: ContextKeyExpr.equals('terminalProcessSupported', true) };
|
||||
const toggleFullscreen: WatermarkEntry = { text: localize({ key: 'watermark.toggleFullscreen', comment: ['toggle is a verb here'] }, "Toggle Full Screen"), id: 'workbench.action.toggleFullScreen' };
|
||||
const showSettings: WatermarkEntry = { text: localize('watermark.showSettings', "Show Settings"), id: 'workbench.action.openSettings' };
|
||||
// const showCommands: WatermarkEntry = { text: localize('watermark.showCommands', "Show All Commands"), id: 'workbench.action.showCommands' };
|
||||
// const quickAccess: WatermarkEntry = { text: localize('watermark.quickAccess', "Go to File"), id: 'workbench.action.quickOpen' };
|
||||
// const openFileNonMacOnly: WatermarkEntry = { text: localize('watermark.openFile', "Open File"), id: 'workbench.action.files.openFile', mac: false };
|
||||
// const openFolderNonMacOnly: WatermarkEntry = { text: localize('watermark.openFolder', "Open Folder"), id: 'workbench.action.files.openFolder', mac: false };
|
||||
// const openFileOrFolderMacOnly: WatermarkEntry = { text: localize('watermark.openFileFolder', "Open File or Folder"), id: 'workbench.action.files.openFileFolder', mac: true };
|
||||
// const openRecent: WatermarkEntry = { text: localize('watermark.openRecent', "Open Recent"), id: 'workbench.action.openRecent' };
|
||||
// const newUntitledFileMacOnly: WatermarkEntry = { text: localize('watermark.newUntitledFile', "New Untitled Text File"), id: 'workbench.action.files.newUntitledFile', mac: true };
|
||||
// const findInFiles: WatermarkEntry = { text: localize('watermark.findInFiles', "Find in Files"), id: 'workbench.action.findInFiles' };
|
||||
// const toggleTerminal: WatermarkEntry = { text: localize({ key: 'watermark.toggleTerminal', comment: ['toggle is a verb here'] }, "Toggle Terminal"), id: 'workbench.action.terminal.toggleTerminal', when: ContextKeyExpr.equals('terminalProcessSupported', true) };
|
||||
// const startDebugging: WatermarkEntry = { text: localize('watermark.startDebugging', "Start Debugging"), id: 'workbench.action.debug.start', when: ContextKeyExpr.equals('terminalProcessSupported', true) };
|
||||
// const toggleFullscreen: WatermarkEntry = { text: localize({ key: 'watermark.toggleFullscreen', comment: ['toggle is a verb here'] }, "Toggle Full Screen"), id: 'workbench.action.toggleFullScreen' };
|
||||
// const showSettings: WatermarkEntry = { text: localize('watermark.showSettings', "Show Settings"), id: 'workbench.action.openSettings' };
|
||||
|
||||
const noFolderEntries = [
|
||||
showCommands,
|
||||
openFileNonMacOnly,
|
||||
openFolderNonMacOnly,
|
||||
openFileOrFolderMacOnly,
|
||||
openRecent,
|
||||
newUntitledFileMacOnly
|
||||
];
|
||||
// // shown when Void is emtpty
|
||||
// const noFolderEntries = [
|
||||
// // showCommands,
|
||||
// openFileNonMacOnly,
|
||||
// openFolderNonMacOnly,
|
||||
// openFileOrFolderMacOnly,
|
||||
// openRecent,
|
||||
// // newUntitledFileMacOnly
|
||||
// ];
|
||||
|
||||
const folderEntries = [
|
||||
showCommands,
|
||||
quickAccess,
|
||||
findInFiles,
|
||||
startDebugging,
|
||||
toggleTerminal,
|
||||
toggleFullscreen,
|
||||
showSettings
|
||||
];
|
||||
// const folderEntries = [
|
||||
// showCommands,
|
||||
// // quickAccess,
|
||||
// // findInFiles,
|
||||
// // startDebugging,
|
||||
// // toggleTerminal,
|
||||
// // toggleFullscreen,
|
||||
// // showSettings
|
||||
// ];
|
||||
|
||||
export class EditorGroupWatermark extends Disposable {
|
||||
private readonly shortcuts: HTMLElement;
|
||||
private readonly transientDisposables = this._register(new DisposableStore());
|
||||
private enabled: boolean = false;
|
||||
// private enabled: boolean = false;
|
||||
private workbenchState: WorkbenchState;
|
||||
private keybindingLabels = new Set<KeybindingLabel>();
|
||||
private currentDisposables = new Set<IDisposable>();
|
||||
|
||||
constructor(
|
||||
container: HTMLElement,
|
||||
@IKeybindingService private readonly keybindingService: IKeybindingService,
|
||||
@IWorkspaceContextService private readonly contextService: IWorkspaceContextService,
|
||||
@IContextKeyService private readonly contextKeyService: IContextKeyService,
|
||||
@IConfigurationService private readonly configurationService: IConfigurationService
|
||||
// @IContextKeyService private readonly contextKeyService: IContextKeyService,
|
||||
@IConfigurationService private readonly configurationService: IConfigurationService,
|
||||
@IThemeService private readonly themeService: IThemeService,
|
||||
@IWorkspacesService private readonly workspacesService: IWorkspacesService,
|
||||
@ICommandService private readonly commandService: ICommandService,
|
||||
@IHostService private readonly hostService: IHostService,
|
||||
@ILabelService private readonly labelService: ILabelService,
|
||||
) {
|
||||
super();
|
||||
|
||||
const elements = h('.editor-group-watermark', [
|
||||
h('.letterpress'),
|
||||
h('.letterpress@icon'),
|
||||
h('.shortcuts@shortcuts'),
|
||||
]);
|
||||
|
||||
append(container, elements.root);
|
||||
this.shortcuts = elements.shortcuts;
|
||||
this.shortcuts = elements.shortcuts; // shortcuts div is modified on render()
|
||||
|
||||
// void icon style
|
||||
const updateTheme = () => {
|
||||
const theme = this.themeService.getColorTheme().type
|
||||
const isDark = theme === ColorScheme.DARK || theme === ColorScheme.HIGH_CONTRAST_DARK
|
||||
elements.icon.style.maxWidth = '220px'
|
||||
elements.icon.style.opacity = '50%'
|
||||
elements.icon.style.filter = isDark ? '' : 'invert(1)' //brightness(.5)
|
||||
}
|
||||
updateTheme()
|
||||
this._register(
|
||||
this.themeService.onDidColorThemeChange(updateTheme)
|
||||
)
|
||||
|
||||
this.registerListeners();
|
||||
|
||||
@@ -103,56 +133,159 @@ export class EditorGroupWatermark extends Disposable {
|
||||
this.render();
|
||||
}));
|
||||
|
||||
const allEntriesWhenClauses = [...noFolderEntries, ...folderEntries].filter(entry => entry.when !== undefined).map(entry => entry.when!);
|
||||
const allKeys = new Set<string>();
|
||||
allEntriesWhenClauses.forEach(when => when.keys().forEach(key => allKeys.add(key)));
|
||||
this._register(this.contextKeyService.onDidChangeContext(e => {
|
||||
if (e.affectsSome(allKeys)) {
|
||||
this.render();
|
||||
}
|
||||
}));
|
||||
// const allEntriesWhenClauses = [...noFolderEntries, ...folderEntries].filter(entry => entry.when !== undefined).map(entry => entry.when!);
|
||||
// const allKeys = new Set<string>();
|
||||
// allEntriesWhenClauses.forEach(when => when.keys().forEach(key => allKeys.add(key)));
|
||||
// this._register(this.contextKeyService.onDidChangeContext(e => {
|
||||
// if (e.affectsSome(allKeys)) {
|
||||
// this.render();
|
||||
// }
|
||||
// }));
|
||||
}
|
||||
|
||||
|
||||
|
||||
private render(): void {
|
||||
const enabled = this.configurationService.getValue<boolean>('workbench.tips.enabled');
|
||||
|
||||
if (enabled === this.enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.enabled = enabled;
|
||||
this.clear();
|
||||
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'
|
||||
|
||||
if (!enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const box = append(this.shortcuts, $('.watermark-box'));
|
||||
const folder = this.workbenchState !== WorkbenchState.EMPTY;
|
||||
const selected = (folder ? 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));
|
||||
const update = async () => {
|
||||
|
||||
const update = () => {
|
||||
clearNode(box);
|
||||
this.keybindingLabels.forEach(label => label.dispose());
|
||||
this.keybindingLabels.clear();
|
||||
// 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);
|
||||
|
||||
for (const entry of selected) {
|
||||
const keys = this.keybindingService.lookupKeybinding(entry.id);
|
||||
if (!keys) {
|
||||
continue;
|
||||
clearNode(voidIconBox);
|
||||
clearNode(recentsBox);
|
||||
|
||||
this.currentDisposables.forEach(label => label.dispose());
|
||||
this.currentDisposables.clear();
|
||||
|
||||
|
||||
// Void - if the workbench is empty, show open
|
||||
if (this.contextService.getWorkbenchState() === WorkbenchState.EMPTY) {
|
||||
|
||||
// Open a folder
|
||||
const openFolderButton = h('button')
|
||||
openFolderButton.root.classList.add('void-watermark-button')
|
||||
openFolderButton.root.style.display = 'block'
|
||||
openFolderButton.root.style.marginLeft = 'auto'
|
||||
openFolderButton.root.style.marginRight = 'auto'
|
||||
openFolderButton.root.style.marginBottom = '16px'
|
||||
openFolderButton.root.textContent = 'Open a folder'
|
||||
openFolderButton.root.onclick = () => {
|
||||
this.commandService.executeCommand(isMacintosh && isNative ? OpenFileFolderAction.ID : OpenFolderAction.ID)
|
||||
// if (this.contextKeyService.contextMatchesRules(ContextKeyExpr.and(WorkbenchStateContext.isEqualTo('workspace')))) {
|
||||
// this.commandService.executeCommand(OpenFolderViaWorkspaceAction.ID);
|
||||
// } else {
|
||||
// this.commandService.executeCommand(isMacintosh ? 'workbench.action.files.openFileFolder' : 'workbench.action.files.openFolder');
|
||||
// }
|
||||
}
|
||||
const dl = append(box, $('dl'));
|
||||
voidIconBox.appendChild(openFolderButton.root);
|
||||
|
||||
|
||||
// Recents
|
||||
if (recentlyOpened.length !== 0) {
|
||||
|
||||
voidIconBox.append(
|
||||
...recentlyOpened.map((w, i) => {
|
||||
|
||||
let fullPath: string;
|
||||
let windowOpenable: IWindowOpenable;
|
||||
if (isRecentFolder(w)) {
|
||||
windowOpenable = { folderUri: w.folderUri };
|
||||
fullPath = w.label || this.labelService.getWorkspaceLabel(w.folderUri, { verbose: Verbosity.LONG });
|
||||
}
|
||||
else {
|
||||
return null
|
||||
// fullPath = w.label || this.labelService.getWorkspaceLabel(w.workspace, { verbose: Verbosity.LONG });
|
||||
// windowOpenable = { workspaceUri: w.workspace.configPath };
|
||||
}
|
||||
|
||||
|
||||
const { name, parentPath } = splitRecentLabel(fullPath);
|
||||
|
||||
const linkSpan = $('span');
|
||||
linkSpan.classList.add('void-link')
|
||||
linkSpan.style.display = 'flex'
|
||||
linkSpan.style.gap = '4px'
|
||||
linkSpan.style.padding = '8px'
|
||||
|
||||
linkSpan.addEventListener('click', e => {
|
||||
this.hostService.openWindow([windowOpenable], {
|
||||
forceNewWindow: e.ctrlKey || e.metaKey,
|
||||
remoteAuthority: w.remoteAuthority || null // local window if remoteAuthority is not set or can not be deducted from the openable
|
||||
});
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
});
|
||||
|
||||
const nameSpan = $('span');
|
||||
nameSpan.innerText = name;
|
||||
nameSpan.title = fullPath;
|
||||
linkSpan.appendChild(nameSpan);
|
||||
|
||||
const dirSpan = $('span');
|
||||
dirSpan.style.paddingLeft = '4px';
|
||||
dirSpan.innerText = parentPath;
|
||||
dirSpan.title = fullPath;
|
||||
|
||||
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(voidIconBox, $('dl'));
|
||||
const dt = append(dl, $('dt'));
|
||||
dt.textContent = entry.text;
|
||||
dt.textContent = 'Chat'
|
||||
const dd = append(dl, $('dd'));
|
||||
const label = new KeybindingLabel(dd, OS, { renderUnboundKeybindings: true, ...defaultKeybindingLabelStyles });
|
||||
label.set(keys);
|
||||
this.keybindingLabels.add(label);
|
||||
if (keys)
|
||||
label.set(keys);
|
||||
this.currentDisposables.add(label);
|
||||
|
||||
|
||||
const keys2 = this.keybindingService.lookupKeybinding(VOID_CTRL_K_ACTION_ID);
|
||||
const dl2 = append(voidIconBox, $('dl'));
|
||||
const dt2 = append(dl2, $('dt'));
|
||||
dt2.textContent = 'Quick Edit'
|
||||
const dd2 = append(dl2, $('dd'));
|
||||
const label2 = new KeybindingLabel(dd2, OS, { renderUnboundKeybindings: true, ...defaultKeybindingLabelStyles });
|
||||
if (keys2)
|
||||
label2.set(keys2);
|
||||
this.currentDisposables.add(label2);
|
||||
|
||||
const keys3 = this.keybindingService.lookupKeybinding('workbench.action.openGlobalKeybindings');
|
||||
const button3 = append(recentsBox, $('button'));
|
||||
button3.textContent = 'Void Settings'
|
||||
button3.style.display = 'block'
|
||||
button3.style.marginLeft = 'auto'
|
||||
button3.style.marginRight = 'auto'
|
||||
button3.classList.add('void-settings-watermark-button')
|
||||
|
||||
const label3 = new KeybindingLabel(button3, OS, { renderUnboundKeybindings: true, ...defaultKeybindingLabelStyles });
|
||||
if (keys3)
|
||||
label3.set(keys3);
|
||||
button3.onclick = () => {
|
||||
this.commandService.executeCommand(VOID_OPEN_SETTINGS_ACTION_ID)
|
||||
}
|
||||
this.currentDisposables.add(label3);
|
||||
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
update();
|
||||
@@ -167,6 +300,6 @@ export class EditorGroupWatermark extends Disposable {
|
||||
override dispose(): void {
|
||||
super.dispose();
|
||||
this.clear();
|
||||
this.keybindingLabels.forEach(label => label.dispose());
|
||||
this.currentDisposables.forEach(label => label.dispose());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,13 +9,15 @@
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.monaco-workbench .part.editor > .content .editor-group-container.empty {
|
||||
opacity: 0.5; /* dimmed to indicate inactive state */
|
||||
.monaco-workbench .part.editor > .content .editor-group-container.empty {
|
||||
opacity: 0.5;
|
||||
/* dimmed to indicate inactive state */
|
||||
}
|
||||
|
||||
.monaco-workbench .part.editor > .content .editor-group-container.empty.active,
|
||||
.monaco-workbench .part.editor > .content .editor-group-container.empty.dragged-over {
|
||||
opacity: 1; /* indicate active/dragged-over group through undimmed state */
|
||||
opacity: 1;
|
||||
/* indicate active/dragged-over group through undimmed state */
|
||||
}
|
||||
|
||||
.monaco-workbench .part.editor > .content:not(.empty) .editor-group-container.empty.active:focus {
|
||||
@@ -24,12 +26,13 @@
|
||||
}
|
||||
|
||||
.monaco-workbench .part.editor > .content.empty .editor-group-container.empty.active:focus {
|
||||
outline: none; /* never show outline for empty group if it is the last */
|
||||
outline: none;
|
||||
/* never show outline for empty group if it is the last */
|
||||
}
|
||||
|
||||
/* Watermark & shortcuts */
|
||||
|
||||
.monaco-workbench .part.editor > .content .editor-group-container > .editor-group-watermark {
|
||||
.monaco-workbench .part.editor > .content .editor-group-container > .editor-group-watermark {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
max-width: 290px;
|
||||
@@ -49,26 +52,27 @@
|
||||
height: calc(100% - 70px);
|
||||
}
|
||||
|
||||
/* light */
|
||||
.monaco-workbench .part.editor > .content .editor-group-container > .editor-group-watermark > .letterpress {
|
||||
width: 100%;
|
||||
max-height: 100%;
|
||||
aspect-ratio: 1/1;
|
||||
background-image: url('./letterpress-light.svg');
|
||||
background-image: url('./void_cube_noshadow.png');
|
||||
background-size: contain;
|
||||
background-position-x: center;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
.monaco-workbench.vs-dark .part.editor > .content .editor-group-container .editor-group-watermark > .letterpress {
|
||||
background-image: url('./letterpress-dark.svg');
|
||||
background-image: url('./void_cube_noshadow.png');
|
||||
}
|
||||
|
||||
.monaco-workbench.hc-light .part.editor > .content .editor-group-container .editor-group-watermark > .letterpress {
|
||||
background-image: url('./letterpress-hcLight.svg');
|
||||
background-image: url('./void_cube_noshadow.png');
|
||||
}
|
||||
|
||||
.monaco-workbench.hc-black .part.editor > .content .editor-group-container .editor-group-watermark > .letterpress {
|
||||
background-image: url('./letterpress-hcDark.svg');
|
||||
background-image: url('./void_cube_noshadow.png');
|
||||
}
|
||||
|
||||
.monaco-workbench .part.editor > .content:not(.empty) .editor-group-container > .editor-group-watermark > .shortcuts,
|
||||
@@ -109,12 +113,13 @@
|
||||
|
||||
.monaco-workbench .part.editor > .content .editor-group-container > .title {
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.monaco-workbench .part.editor > .content .editor-group-container > .title:not(.tabs) {
|
||||
display: flex; /* when tabs are not shown, use flex layout */
|
||||
display: flex;
|
||||
/* when tabs are not shown, use flex layout */
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
|
||||
@@ -144,7 +149,8 @@
|
||||
.monaco-workbench .part.editor > .content .editor-group-container.empty.locked > .editor-group-container-toolbar,
|
||||
.monaco-workbench .part.editor > .content:not(.empty) .editor-group-container.empty > .editor-group-container-toolbar,
|
||||
.monaco-workbench .part.editor > .content.auxiliary .editor-group-container.empty > .editor-group-container-toolbar {
|
||||
display: block; /* show toolbar when more than one editor group or always when auxiliary or locked */
|
||||
display: block;
|
||||
/* show toolbar when more than one editor group or always when auxiliary or locked */
|
||||
}
|
||||
|
||||
.monaco-workbench .part.editor > .content .editor-group-container > .editor-group-container-toolbar .actions-container {
|
||||
@@ -157,7 +163,7 @@
|
||||
|
||||
/* Editor */
|
||||
|
||||
.monaco-workbench .part.editor > .content .editor-group-container.empty > .editor-container {
|
||||
.monaco-workbench .part.editor > .content .editor-group-container.empty > .editor-container {
|
||||
display: none;
|
||||
}
|
||||
|
||||
|
||||
@@ -385,7 +385,7 @@
|
||||
.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.tab-actions-right.sizing-shrink > .tab-actions,
|
||||
.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.tab-actions-right.sizing-fixed > .tab-actions {
|
||||
flex: 0;
|
||||
overflow: hidden; /* let the tab actions be pushed out of view when sizing is set to shrink/fixed to make more room */
|
||||
overflow: visible; /* ensure tab actions are always visible */
|
||||
}
|
||||
|
||||
.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.dirty.tab-actions-right.sizing-shrink > .tab-actions,
|
||||
@@ -399,18 +399,8 @@
|
||||
overflow: visible; /* ...but still show the tab actions on hover, focus and when dirty or sticky */
|
||||
}
|
||||
|
||||
.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.close-action-off:not(.dirty) > .tab-actions,
|
||||
.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.sticky-compact > .tab-actions {
|
||||
display: none; /* hide the tab actions when we are configured to hide it (unless dirty, but always when sticky-compact) */
|
||||
}
|
||||
|
||||
.monaco-workbench .part.editor > .content .editor-group-container.active > .title .tabs-container > .tab.active > .tab-actions .action-label, /* always show tab actions for active tab */
|
||||
.monaco-workbench .part.editor > .content .editor-group-container.active > .title .tabs-container > .tab > .tab-actions .action-label:focus, /* always show tab actions on focus */
|
||||
.monaco-workbench .part.editor > .content .editor-group-container.active > .title .tabs-container > .tab:hover > .tab-actions .action-label, /* always show tab actions on hover */
|
||||
.monaco-workbench .part.editor > .content .editor-group-container.active > .title .tabs-container > .tab.active:hover > .tab-actions .action-label, /* always show tab actions on hover */
|
||||
.monaco-workbench .part.editor > .content .editor-group-container.active > .title .tabs-container > .tab.sticky:not(.pinned-action-off) > .tab-actions .action-label, /* always show tab actions for sticky tabs */
|
||||
.monaco-workbench .part.editor > .content .editor-group-container.active > .title .tabs-container > .tab.dirty > .tab-actions .action-label { /* always show tab actions for dirty tabs */
|
||||
opacity: 1;
|
||||
display: none; /* only hide tab actions when sticky-compact */
|
||||
}
|
||||
|
||||
.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab > .tab-actions .actions-container {
|
||||
@@ -444,11 +434,11 @@
|
||||
.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.dirty > .tab-actions .action-label,
|
||||
.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.sticky:not(.pinned-action-off) > .tab-actions .action-label,
|
||||
.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab:hover > .tab-actions .action-label {
|
||||
opacity: 0.5; /* show tab actions dimmed for inactive group */
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab > .tab-actions .action-label {
|
||||
opacity: 0;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Tab Actions: Off */
|
||||
|
||||
|
After Width: | Height: | Size: 850 KiB |
|
After Width: | Height: | Size: 795 KiB |
@@ -35,7 +35,7 @@ import { MergeGroupMode, IMergeGroupOptions } from '../../../services/editor/com
|
||||
import { addDisposableListener, EventType, EventHelper, Dimension, scheduleAtNextAnimationFrame, findParentWithClass, clearNode, DragAndDropObserver, isMouseEvent, getWindow } from '../../../../base/browser/dom.js';
|
||||
import { localize } from '../../../../nls.js';
|
||||
import { IEditorGroupsView, EditorServiceImpl, IEditorGroupView, IInternalEditorOpenOptions, IEditorPartsView } from './editor.js';
|
||||
import { CloseEditorTabAction, UnpinEditorAction } from './editorActions.js';
|
||||
import { CloseEditorTabAction, PinEditorAction, UnpinEditorAction } from './editorActions.js';
|
||||
import { assertAllDefined, assertIsDefined } from '../../../../base/common/types.js';
|
||||
import { IEditorService } from '../../../services/editor/common/editorService.js';
|
||||
import { basenameOrAuthority } from '../../../../base/common/resources.js';
|
||||
@@ -113,6 +113,7 @@ export class MultiEditorTabsControl extends EditorTabsControl {
|
||||
|
||||
private readonly closeEditorAction = this._register(this.instantiationService.createInstance(CloseEditorTabAction, CloseEditorTabAction.ID, CloseEditorTabAction.LABEL));
|
||||
private readonly unpinEditorAction = this._register(this.instantiationService.createInstance(UnpinEditorAction, UnpinEditorAction.ID, UnpinEditorAction.LABEL));
|
||||
private readonly pinEditorAction = this._register(this.instantiationService.createInstance(PinEditorAction, PinEditorAction.ID, PinEditorAction.LABEL));
|
||||
|
||||
private readonly tabResourceLabels = this._register(this.instantiationService.createInstance(ResourceLabels, DEFAULT_LABELS_CONTAINER));
|
||||
private tabLabels: IEditorInputLabel[] = [];
|
||||
@@ -1510,6 +1511,8 @@ export class MultiEditorTabsControl extends EditorTabsControl {
|
||||
this.layout(this.dimensions, options);
|
||||
}
|
||||
|
||||
// In MultiEditorTabsControl.ts, modify the redrawTab method:
|
||||
|
||||
private redrawTab(editor: EditorInput, tabIndex: number, tabContainer: HTMLElement, tabLabelWidget: IResourceLabel, tabLabel: IEditorInputLabel, tabActionBar: ActionBar): void {
|
||||
const isTabSticky = this.tabsModel.isSticky(tabIndex);
|
||||
const options = this.groupsView.partOptions;
|
||||
@@ -1518,47 +1521,46 @@ export class MultiEditorTabsControl extends EditorTabsControl {
|
||||
this.redrawTabLabel(editor, tabIndex, tabContainer, tabLabelWidget, tabLabel);
|
||||
|
||||
// Action
|
||||
const hasUnpinAction = isTabSticky && options.tabActionUnpinVisibility;
|
||||
const hasCloseAction = !hasUnpinAction && options.tabActionCloseVisibility;
|
||||
const hasAction = hasUnpinAction || hasCloseAction;
|
||||
const hasCloseAction = options.tabActionCloseVisibility;
|
||||
const hasAction = true; // Always show actions
|
||||
|
||||
let tabAction;
|
||||
if (hasAction) {
|
||||
tabAction = hasUnpinAction ? this.unpinEditorAction : this.closeEditorAction;
|
||||
// Clear existing actions
|
||||
if (!tabActionBar.isEmpty()) {
|
||||
tabActionBar.clear();
|
||||
}
|
||||
|
||||
// Add pin/unpin action based on sticky state
|
||||
if (isTabSticky) {
|
||||
tabActionBar.push(this.unpinEditorAction, { icon: true, label: false, keybinding: this.getKeybindingLabel(this.unpinEditorAction) });
|
||||
} else {
|
||||
// Even if the action is not visible, add it as it contains the dirty indicator
|
||||
tabAction = isTabSticky ? this.unpinEditorAction : this.closeEditorAction;
|
||||
tabActionBar.push(this.pinEditorAction, { icon: true, label: false, keybinding: this.getKeybindingLabel(this.pinEditorAction) });
|
||||
}
|
||||
|
||||
if (!tabActionBar.hasAction(tabAction)) {
|
||||
if (!tabActionBar.isEmpty()) {
|
||||
tabActionBar.clear();
|
||||
}
|
||||
|
||||
tabActionBar.push(tabAction, { icon: true, label: false, keybinding: this.getKeybindingLabel(tabAction) });
|
||||
// Add close action
|
||||
if (hasCloseAction) {
|
||||
tabActionBar.push(this.closeEditorAction, { icon: true, label: false, keybinding: this.getKeybindingLabel(this.closeEditorAction) });
|
||||
}
|
||||
|
||||
tabContainer.classList.toggle(`pinned-action-off`, isTabSticky && !hasUnpinAction);
|
||||
tabContainer.classList.toggle(`close-action-off`, !hasUnpinAction && !hasCloseAction);
|
||||
// Update tab classes and styles
|
||||
tabContainer.classList.toggle('sticky', isTabSticky);
|
||||
tabContainer.classList.toggle('close-action-off', !hasCloseAction);
|
||||
tabContainer.classList.toggle('tab-actions-right', hasAction && options.tabActionLocation === 'right');
|
||||
tabContainer.classList.toggle('tab-actions-left', hasAction && options.tabActionLocation === 'left');
|
||||
|
||||
for (const option of ['left', 'right']) {
|
||||
tabContainer.classList.toggle(`tab-actions-${option}`, hasAction && options.tabActionLocation === option);
|
||||
}
|
||||
|
||||
const tabSizing = isTabSticky && options.pinnedTabSizing === 'shrink' ? 'shrink' /* treat sticky shrink tabs as tabSizing: 'shrink' */ : options.tabSizing;
|
||||
for (const option of ['fit', 'shrink', 'fixed']) {
|
||||
tabContainer.classList.toggle(`sizing-${option}`, tabSizing === option);
|
||||
}
|
||||
// Update tab sizing classes
|
||||
const tabSizing = isTabSticky && options.pinnedTabSizing === 'shrink' ? 'shrink' : options.tabSizing;
|
||||
tabContainer.classList.toggle('sizing-fit', tabSizing === 'fit');
|
||||
tabContainer.classList.toggle('sizing-shrink', tabSizing === 'shrink');
|
||||
tabContainer.classList.toggle('sizing-fixed', tabSizing === 'fixed');
|
||||
|
||||
tabContainer.classList.toggle('has-icon', options.showIcons && options.hasIcons);
|
||||
|
||||
// Update sticky classes
|
||||
tabContainer.classList.toggle('sticky', isTabSticky);
|
||||
for (const option of ['normal', 'compact', 'shrink']) {
|
||||
tabContainer.classList.toggle(`sticky-${option}`, isTabSticky && options.pinnedTabSizing === option);
|
||||
}
|
||||
tabContainer.classList.toggle('sticky-compact', isTabSticky && options.pinnedTabSizing === 'compact');
|
||||
tabContainer.classList.toggle('sticky-shrink', isTabSticky && options.pinnedTabSizing === 'shrink');
|
||||
|
||||
// If not wrapping tabs, sticky compact/shrink tabs need a position to remain at their location
|
||||
// when scrolling to stay in view (requirement for position: sticky)
|
||||
// Update tab position if needed
|
||||
if (!options.wrapTabs && isTabSticky && options.pinnedTabSizing !== 'normal') {
|
||||
let stickyTabWidth = 0;
|
||||
switch (options.pinnedTabSizing) {
|
||||
@@ -1569,16 +1571,13 @@ export class MultiEditorTabsControl extends EditorTabsControl {
|
||||
stickyTabWidth = MultiEditorTabsControl.TAB_WIDTH.shrink;
|
||||
break;
|
||||
}
|
||||
|
||||
tabContainer.style.left = `${tabIndex * stickyTabWidth}px`;
|
||||
} else {
|
||||
tabContainer.style.left = 'auto';
|
||||
}
|
||||
|
||||
// Borders / outline
|
||||
// Draw borders and selection state
|
||||
this.redrawTabBorders(tabIndex, tabContainer);
|
||||
|
||||
// Selection / active / dirty state
|
||||
this.redrawTabSelectedActiveAndDirty(this.groupsView.activeGroup === this.groupView, editor, tabContainer, tabActionBar);
|
||||
}
|
||||
|
||||
|
||||
@@ -342,7 +342,7 @@ registerAction2(class extends Action2 {
|
||||
constructor() {
|
||||
super({
|
||||
id: 'workbench.action.closeAuxiliaryBar',
|
||||
title: localize2('closeSecondarySideBar', 'Hide Secondary Side Bar'),
|
||||
title: localize2('closeSecondarySideBar', 'Hide Void Side Bar'),
|
||||
category: Categories.View,
|
||||
icon: closeIcon,
|
||||
menu: [{
|
||||
@@ -415,14 +415,16 @@ class MoveViewsBetweenPanelsAction extends Action2 {
|
||||
}
|
||||
}
|
||||
|
||||
// --- Move Panel Views To Secondary Side Bar
|
||||
// --- Move Panel Views To Void Side Bar
|
||||
|
||||
// these are just for the command pallette
|
||||
|
||||
class MovePanelToSidePanelAction extends MoveViewsBetweenPanelsAction {
|
||||
static readonly ID = 'workbench.action.movePanelToSidePanel';
|
||||
constructor() {
|
||||
super(ViewContainerLocation.Panel, ViewContainerLocation.AuxiliaryBar, {
|
||||
id: MovePanelToSidePanelAction.ID,
|
||||
title: localize2('movePanelToSecondarySideBar', "Move Panel Views To Secondary Side Bar"),
|
||||
title: localize2('movePanelToSecondarySideBar', "Move Panel Views To Void Side Bar"),
|
||||
category: Categories.View,
|
||||
f1: false
|
||||
});
|
||||
@@ -434,7 +436,7 @@ export class MovePanelToSecondarySideBarAction extends MoveViewsBetweenPanelsAct
|
||||
constructor() {
|
||||
super(ViewContainerLocation.Panel, ViewContainerLocation.AuxiliaryBar, {
|
||||
id: MovePanelToSecondarySideBarAction.ID,
|
||||
title: localize2('movePanelToSecondarySideBar', "Move Panel Views To Secondary Side Bar"),
|
||||
title: localize2('movePanelToSecondarySideBar', "Move Panel Views To Void Side Bar"),
|
||||
category: Categories.View,
|
||||
f1: true
|
||||
});
|
||||
@@ -452,7 +454,7 @@ class MoveSidePanelToPanelAction extends MoveViewsBetweenPanelsAction {
|
||||
constructor() {
|
||||
super(ViewContainerLocation.AuxiliaryBar, ViewContainerLocation.Panel, {
|
||||
id: MoveSidePanelToPanelAction.ID,
|
||||
title: localize2('moveSidePanelToPanel', "Move Secondary Side Bar Views To Panel"),
|
||||
title: localize2('moveSidePanelToPanel', "Move Side Bar Views To Panel"), // Void - this seemed to have a typo before
|
||||
category: Categories.View,
|
||||
f1: false
|
||||
});
|
||||
@@ -465,7 +467,7 @@ export class MoveSecondarySideBarToPanelAction extends MoveViewsBetweenPanelsAct
|
||||
constructor() {
|
||||
super(ViewContainerLocation.AuxiliaryBar, ViewContainerLocation.Panel, {
|
||||
id: MoveSecondarySideBarToPanelAction.ID,
|
||||
title: localize2('moveSidePanelToPanel', "Move Secondary Side Bar Views To Panel"),
|
||||
title: localize2('moveSidePanelToPanel', "Move Void Side Bar Views To Panel"),
|
||||
category: Categories.View,
|
||||
f1: true
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -512,7 +512,7 @@ const registry = Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Con
|
||||
'type': 'string',
|
||||
'enum': ['left', 'right'],
|
||||
'default': 'left',
|
||||
'description': localize('sideBarLocation', "Controls the location of the primary side bar and activity bar. They can either show on the left or right of the workbench. The secondary side bar will show on the opposite side of the workbench.")
|
||||
'description': localize('sideBarLocation', "Controls the location of the primary side bar and activity bar. They can either show on the left or right of the workbench. The Void side bar will show on the opposite side of the workbench.")
|
||||
},
|
||||
'workbench.panel.showLabel': {
|
||||
'type': 'boolean',
|
||||
@@ -545,12 +545,12 @@ const registry = Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Con
|
||||
'type': 'string',
|
||||
'enum': ['default', 'top', 'bottom', 'hidden'],
|
||||
'default': 'default',
|
||||
'markdownDescription': localize({ comment: ['This is the description for a setting'], key: 'activityBarLocation' }, "Controls the location of the Activity Bar relative to the Primary and Secondary Side Bars."),
|
||||
'markdownDescription': localize({ comment: ['This is the description for a setting'], key: 'activityBarLocation' }, "Controls the location of the Activity Bar relative to the Primary and Void Side Bars."),
|
||||
'enumDescriptions': [
|
||||
localize('workbench.activityBar.location.default', "Show the Activity Bar on the side of the Primary Side Bar and on top of the Secondary Side Bar."),
|
||||
localize('workbench.activityBar.location.top', "Show the Activity Bar on top of the Primary and Secondary Side Bars."),
|
||||
localize('workbench.activityBar.location.bottom', "Show the Activity Bar at the bottom of the Primary and Secondary Side Bars."),
|
||||
localize('workbench.activityBar.location.hide', "Hide the Activity Bar in the Primary and Secondary Side Bars.")
|
||||
localize('workbench.activityBar.location.default', "Show the Activity Bar on the side of the Primary Side Bar and on top of the Void Side Bar."),
|
||||
localize('workbench.activityBar.location.top', "Show the Activity Bar on top of the Primary and Void Side Bars."),
|
||||
localize('workbench.activityBar.location.bottom', "Show the Activity Bar at the bottom of the Primary and Void Side Bars."),
|
||||
localize('workbench.activityBar.location.hide', "Hide the Activity Bar in the Primary and Void Side Bars.")
|
||||
],
|
||||
},
|
||||
'workbench.activityBar.iconClickBehavior': {
|
||||
@@ -598,7 +598,7 @@ const registry = Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Con
|
||||
'description': localize('workbench.hover.delay', "Controls the delay in milliseconds after which the hover is shown for workbench items (ex. some extension provided tree view items). Already visible items may require a refresh before reflecting this setting change."),
|
||||
// Testing has indicated that on Windows and Linux 500 ms matches the native hovers most closely.
|
||||
// On Mac, the delay is 1500.
|
||||
'default': isMacintosh ? 1500 : 500,
|
||||
'default': isMacintosh ? 300 : 300, // <-- Void edited this to 300 : 300 (was 1500 : 500)
|
||||
'minimum': 0
|
||||
},
|
||||
'workbench.reduceMotion': {
|
||||
|
||||
@@ -289,6 +289,7 @@ export interface IFileTemplateData {
|
||||
readonly templateDisposables: DisposableStore;
|
||||
readonly elementDisposables: DisposableStore;
|
||||
readonly label: IResourceLabel;
|
||||
// readonly voidLabels: IResourceLabel;
|
||||
readonly container: HTMLElement;
|
||||
readonly contribs: IExplorerFileContribution[];
|
||||
currentContext?: ExplorerItem;
|
||||
@@ -347,15 +348,25 @@ export class FilesRenderer implements ICompressibleTreeRenderer<ExplorerItem, Fu
|
||||
|
||||
renderTemplate(container: HTMLElement): IFileTemplateData {
|
||||
const templateDisposables = new DisposableStore();
|
||||
|
||||
// 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(() => {
|
||||
try {
|
||||
if (templateData.currentContext) {
|
||||
this.updateWidth(templateData.currentContext);
|
||||
}
|
||||
} catch (e) {
|
||||
// noop since the element might no longer be in the tree, no update of width necessary
|
||||
}
|
||||
try { if (templateData.currentContext) this.updateWidth(templateData.currentContext); }
|
||||
catch (e) { /* noop since the element might no longer be in the tree, no update of width necessary*/ }
|
||||
}));
|
||||
|
||||
const contribs = explorerFileContribRegistry.create(this.instantiationService, container, templateDisposables);
|
||||
@@ -365,10 +376,15 @@ 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, container, contribs };
|
||||
|
||||
return templateData;
|
||||
}
|
||||
|
||||
|
||||
// Void cares about this function, this is where elements in the tree are rendered
|
||||
renderElement(node: ITreeNode<ExplorerItem, FuzzyScore>, index: number, templateData: IFileTemplateData): void {
|
||||
const stat = node.element;
|
||||
templateData.currentContext = stat;
|
||||
@@ -382,8 +398,7 @@ export class FilesRenderer implements ICompressibleTreeRenderer<ExplorerItem, Fu
|
||||
templateData.label.element.style.display = 'flex';
|
||||
this.renderStat(stat, stat.name, undefined, node.filterData, templateData);
|
||||
}
|
||||
|
||||
// Input Box
|
||||
// Input Box (Void - shown only if currently editing - this is the box that appears when user edits the name of the file)
|
||||
else {
|
||||
templateData.label.element.style.display = 'none';
|
||||
templateData.contribs.forEach(c => c.setResource(undefined));
|
||||
@@ -477,6 +492,13 @@ export class FilesRenderer implements ICompressibleTreeRenderer<ExplorerItem, Fu
|
||||
separator: this.labelService.getSeparator(stat.resource.scheme, stat.resource.authority),
|
||||
domId
|
||||
});
|
||||
|
||||
// templateData.voidLabels.setResource({ resource: undefined, name: 'hi', }, {
|
||||
// hideIcon: true,
|
||||
// extraClasses: realignNestedChildren ? [...extraClasses, 'align-nest-icon-with-parent-icon'] : extraClasses,
|
||||
// forceLabel: true,
|
||||
// });
|
||||
|
||||
}
|
||||
|
||||
private renderInputBox(container: HTMLElement, stat: ExplorerItem, editableData: IEditableData): IDisposable {
|
||||
|
||||
@@ -95,7 +95,12 @@ suite('Files - ExplorerView', () => {
|
||||
label: <any>{
|
||||
container: label,
|
||||
onDidRender: emitter.event
|
||||
}
|
||||
},
|
||||
// voidLabels: <any>{
|
||||
// container: label,
|
||||
// onDidRender: emitter.event
|
||||
// },
|
||||
|
||||
}, 1, false);
|
||||
|
||||
ds.add(navigationController);
|
||||
|
||||
@@ -151,7 +151,7 @@ export class ViewQuickAccessProvider extends PickerQuickAccessProvider<IViewQuic
|
||||
// Viewlets / Panels
|
||||
addPaneComposites(ViewContainerLocation.Sidebar, localize('views', "Side Bar"));
|
||||
addPaneComposites(ViewContainerLocation.Panel, localize('panels', "Panel"));
|
||||
addPaneComposites(ViewContainerLocation.AuxiliaryBar, localize('secondary side bar', "Secondary Side Bar"));
|
||||
addPaneComposites(ViewContainerLocation.AuxiliaryBar, localize('secondary side bar', "Void Side Bar"));
|
||||
|
||||
const addPaneCompositeViews = (location: ViewContainerLocation) => {
|
||||
const paneComposites = this.paneCompositeService.getPaneComposites(location);
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
void-imports/
|
||||
@@ -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,934 @@
|
||||
/*--------------------------------------------------------------------------------------
|
||||
* 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 { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
|
||||
import { ITextModel } from '../../../../editor/common/model.js';
|
||||
import { Position } from '../../../../editor/common/core/position.js';
|
||||
import { InlineCompletion, InlineCompletionContext, } from '../../../../editor/common/languages.js';
|
||||
import { CancellationToken } from '../../../../base/common/cancellation.js';
|
||||
import { Range } from '../../../../editor/common/core/range.js';
|
||||
import { 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 { isWindows } from '../../../../base/common/platform.js';
|
||||
import { registerWorkbenchContribution2, WorkbenchPhase } from '../../../common/contributions.js';
|
||||
import { ILLMMessageService } from '../common/llmMessageService.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
|
||||
|
||||
|
||||
/*
|
||||
A summary of autotab:
|
||||
|
||||
Postprocessing
|
||||
-one common problem for all models is outputting unbalanced parentheses
|
||||
we solve this by trimming all extra closing parentheses from the generated string
|
||||
in future, should make sure parentheses are always balanced
|
||||
|
||||
-another problem is completing the middle of a string, eg. "const [x, CURSOR] = useState()"
|
||||
we complete up to first matchup character
|
||||
but should instead complete the whole line / block (difficult because of parenthesis accuracy)
|
||||
|
||||
-too much info is bad. usually we want to show the user 1 line, and have a preloaded response afterwards
|
||||
this should happen automatically with caching system
|
||||
should break preloaded responses into \n\n chunks
|
||||
|
||||
Preprocessing
|
||||
- we don't generate if cursor is at end / beginning of a line (no spaces)
|
||||
- we generate 1 line if there is text to the right of cursor
|
||||
- we generate 1 line if variable declaration
|
||||
- (in many cases want to show 1 line but generate multiple)
|
||||
|
||||
State
|
||||
- cache based on prefix (and do some trimming first)
|
||||
- when press tab on one line, should have an immediate followup response
|
||||
to do this, show autocompletes before they're fully finished
|
||||
- [todo] remove each autotab when accepted
|
||||
!- [todo] provide type information
|
||||
|
||||
Details
|
||||
-generated results are trimmed up to 1 leading/trailing space
|
||||
-prefixes are cached up to 1 trailing newline
|
||||
-
|
||||
*/
|
||||
|
||||
class LRUCache<K, V> {
|
||||
public items: Map<K, V>;
|
||||
private keyOrder: K[];
|
||||
private maxSize: number;
|
||||
private disposeCallback?: (value: V, key?: K) => void;
|
||||
|
||||
constructor(maxSize: number, disposeCallback?: (value: V, key?: K) => void) {
|
||||
if (maxSize <= 0) throw new Error('Cache size must be greater than 0');
|
||||
|
||||
this.items = new Map();
|
||||
this.keyOrder = [];
|
||||
this.maxSize = maxSize;
|
||||
this.disposeCallback = disposeCallback;
|
||||
}
|
||||
|
||||
set(key: K, value: V): void {
|
||||
// If key exists, remove it from the order list
|
||||
if (this.items.has(key)) {
|
||||
this.keyOrder = this.keyOrder.filter(k => k !== key);
|
||||
}
|
||||
// If cache is full, remove least recently used item
|
||||
else if (this.items.size >= this.maxSize) {
|
||||
const key = this.keyOrder[0];
|
||||
const value = this.items.get(key);
|
||||
|
||||
// Call dispose callback if it exists
|
||||
if (this.disposeCallback && value !== undefined) {
|
||||
this.disposeCallback(value, key);
|
||||
}
|
||||
|
||||
this.items.delete(key);
|
||||
this.keyOrder.shift();
|
||||
}
|
||||
|
||||
// Add new item
|
||||
this.items.set(key, value);
|
||||
this.keyOrder.push(key);
|
||||
}
|
||||
|
||||
delete(key: K): boolean {
|
||||
const value = this.items.get(key);
|
||||
|
||||
if (value !== undefined) {
|
||||
// Call dispose callback if it exists
|
||||
if (this.disposeCallback) {
|
||||
this.disposeCallback(value, key);
|
||||
}
|
||||
|
||||
this.items.delete(key);
|
||||
this.keyOrder = this.keyOrder.filter(k => k !== key);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
// Call dispose callback for all items if it exists
|
||||
if (this.disposeCallback) {
|
||||
for (const [key, value] of this.items.entries()) {
|
||||
this.disposeCallback(value, key);
|
||||
}
|
||||
}
|
||||
|
||||
this.items.clear();
|
||||
this.keyOrder = [];
|
||||
}
|
||||
|
||||
get size(): number {
|
||||
return this.items.size;
|
||||
}
|
||||
|
||||
has(key: K): boolean {
|
||||
return this.items.has(key);
|
||||
}
|
||||
}
|
||||
|
||||
type 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: 'pending' | 'finished' | 'error',
|
||||
type: AutocompletionPredictionType,
|
||||
llmPromise: Promise<string> | undefined,
|
||||
insertText: string,
|
||||
requestId: string | null,
|
||||
_newlineCount: number,
|
||||
}
|
||||
|
||||
const DEBOUNCE_TIME = 500
|
||||
const TIMEOUT_TIME = 60000
|
||||
const MAX_CACHE_SIZE = 20
|
||||
const MAX_PENDING_REQUESTS = 2
|
||||
|
||||
// postprocesses the result
|
||||
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 ? ' ' : '');
|
||||
|
||||
}
|
||||
|
||||
|
||||
// trims the end of the prefix to improve cache hit rate
|
||||
const removeLeftTabsAndTrimEnds = (s: string): string => {
|
||||
const trimmedString = s.trimEnd();
|
||||
const trailingEnd = s.slice(trimmedString.length);
|
||||
|
||||
// keep only a single trailing newline
|
||||
if (trailingEnd.includes(_ln)) {
|
||||
s = trimmedString + _ln;
|
||||
}
|
||||
|
||||
s = s.replace(/^\s+/gm, ''); // remove left tabs
|
||||
|
||||
return s;
|
||||
}
|
||||
|
||||
|
||||
|
||||
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> = { ')': '(', '}': '{', ']': '[' };
|
||||
|
||||
// process all bracets in prefix
|
||||
let stack: string[] = []
|
||||
const firstOpenIdx = prefix.search(/[[({]/);
|
||||
if (firstOpenIdx !== -1) {
|
||||
const brackets = prefix.slice(firstOpenIdx).split('').filter(c => '()[]{}'.includes(c));
|
||||
|
||||
for (const bracket of brackets) {
|
||||
if (bracket === '(' || bracket === '{' || bracket === '[') {
|
||||
stack.push(bracket);
|
||||
} else {
|
||||
if (stack.length > 0 && stack[stack.length - 1] === pairs[bracket]) {
|
||||
stack.pop();
|
||||
} else {
|
||||
stack.push(bracket);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// iterate through each character
|
||||
for (let i = 0; i < s.length; i++) {
|
||||
const char = s[i];
|
||||
|
||||
if (char === '(' || char === '{' || char === '[') { stack.push(char); }
|
||||
else if (char === ')' || char === '}' || char === ']') {
|
||||
if (stack.length === 0 || stack.pop() !== pairs[char]) { return s.substring(0, i); }
|
||||
}
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
|
||||
// further trim the autocompletion
|
||||
const postprocessAutocompletion = ({ autocompletionMatchup, autocompletion, prefixAndSuffix }: { autocompletionMatchup: AutocompletionMatchupBounds, autocompletion: Autocompletion, prefixAndSuffix: PrefixAndSuffixInfo }) => {
|
||||
|
||||
const { prefix, prefixToTheLeftOfCursor, suffixToTheRightOfCursor } = prefixAndSuffix
|
||||
|
||||
const generatedMiddle = autocompletion.insertText
|
||||
|
||||
let startIdx = autocompletionMatchup.startIdx
|
||||
let endIdx = generatedMiddle.length // exclusive bounds
|
||||
|
||||
// const naiveReturnValue = generatedMiddle.slice(startIdx)
|
||||
// console.log('naiveReturnValue: ', JSON.stringify(naiveReturnValue))
|
||||
// return [{ insertText: naiveReturnValue, }]
|
||||
|
||||
// do postprocessing for better ux
|
||||
// this is a bit hacky but may change a lot
|
||||
|
||||
// if there is space at the start of the completion and user has added it, remove it
|
||||
const charToLeftOfCursor = prefixToTheLeftOfCursor.slice(-1)[0] || ''
|
||||
const userHasAddedASpace = charToLeftOfCursor === ' ' || charToLeftOfCursor === '\t'
|
||||
const rawFirstNonspaceIdx = generatedMiddle.slice(startIdx).search(/[^\t ]/)
|
||||
if (rawFirstNonspaceIdx > -1 && userHasAddedASpace) {
|
||||
const firstNonspaceIdx = rawFirstNonspaceIdx + startIdx;
|
||||
// console.log('p0', startIdx, rawFirstNonspaceIdx)
|
||||
startIdx = Math.max(startIdx, firstNonspaceIdx)
|
||||
}
|
||||
|
||||
// if user is on a blank line and the generation starts with newline(s), remove them
|
||||
const numStartingNewlines = generatedMiddle.slice(startIdx).match(new RegExp(`^${_ln}+`))?.[0].length || 0;
|
||||
if (
|
||||
!prefixToTheLeftOfCursor.trim()
|
||||
&& !suffixToTheRightOfCursor.trim()
|
||||
&& numStartingNewlines > 0
|
||||
) {
|
||||
// console.log('p1', numStartingNewlines)
|
||||
startIdx += numStartingNewlines
|
||||
}
|
||||
|
||||
// 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 (`{}()[]<>\`'"`.includes(matchChar)) {
|
||||
endIdx = Math.min(endIdx, matchIdx)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const restOfLineToGenerate = generatedMiddle.slice(startIdx).split(_ln)[0] ?? ''
|
||||
// condition to complete as a single line completion
|
||||
if (
|
||||
prefixToTheLeftOfCursor.trim()
|
||||
&& !suffixToTheRightOfCursor.trim()
|
||||
&& restOfLineToGenerate.trim()
|
||||
) {
|
||||
|
||||
const rawNewlineIdx = generatedMiddle.slice(startIdx).indexOf(_ln)
|
||||
if (rawNewlineIdx > -1) {
|
||||
// console.log('p3', startIdx, rawNewlineIdx)
|
||||
const newlineIdx = rawNewlineIdx + startIdx;
|
||||
endIdx = Math.min(endIdx, newlineIdx)
|
||||
}
|
||||
}
|
||||
|
||||
// // if a generated line matches with a suffix line, stop
|
||||
// if (suffixLines.length > 1) {
|
||||
// console.log('4')
|
||||
// const lines = []
|
||||
// for (const generatedLine of generatedLines) {
|
||||
// if (suffixLines.slice(0, 10).some(suffixLine =>
|
||||
// generatedLine.trim() !== '' && suffixLine.trim() !== ''
|
||||
// && generatedLine.trim().startsWith(suffixLine.trim())
|
||||
// )) break;
|
||||
// lines.push(generatedLine)
|
||||
// }
|
||||
// endIdx = lines.join('\n').length // this is hacky, remove or refactor in future
|
||||
// }
|
||||
|
||||
// console.log('pFinal', startIdx, endIdx)
|
||||
let completionStr = generatedMiddle.slice(startIdx, endIdx)
|
||||
|
||||
// filter out unbalanced parentheses
|
||||
completionStr = 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: trimmedInsertText,
|
||||
range: rangeToReplace,
|
||||
}]
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// returns whether this autocompletion is in the cache
|
||||
// const doesPrefixMatchAutocompletion = ({ prefix, autocompletion }: { prefix: string, autocompletion: Autocompletion }): boolean => {
|
||||
|
||||
// const originalPrefix = autocompletion.prefix
|
||||
// const generatedMiddle = autocompletion.result
|
||||
// const originalPrefixTrimmed = trimPrefix(originalPrefix)
|
||||
// const currentPrefixTrimmed = trimPrefix(prefix)
|
||||
|
||||
// if (currentPrefixTrimmed.length < originalPrefixTrimmed.length) {
|
||||
// return false
|
||||
// }
|
||||
|
||||
// const isMatch = (originalPrefixTrimmed + generatedMiddle).startsWith(currentPrefixTrimmed)
|
||||
// return isMatch
|
||||
|
||||
// }
|
||||
|
||||
|
||||
const allLinebreakSymbols = ['\r\n', '\n']
|
||||
const _ln = isWindows ? allLinebreakSymbols[0] : allLinebreakSymbols[1]
|
||||
|
||||
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();
|
||||
|
||||
const cursorOffset = model.getOffsetAt(position)
|
||||
const prefix = fullText.substring(0, cursorOffset)
|
||||
const suffix = fullText.substring(cursorOffset)
|
||||
|
||||
|
||||
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(_ln).slice(0, line).join(_ln).length + (line > 0 ? 1 : 0) + char;
|
||||
}
|
||||
const getLastLine = (s: string): string => {
|
||||
const matches = s.match(new RegExp(`[^${_ln}]*$`))
|
||||
return matches ? matches[0] : ''
|
||||
}
|
||||
|
||||
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 getAutocompletionMatchup = ({ prefix, autocompletion }: { prefix: string, autocompletion: Autocompletion }): AutocompletionMatchupBounds | undefined => {
|
||||
|
||||
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))
|
||||
// console.log('@trimmedCompletionPrefix: ', JSON.stringify(trimmedCompletionPrefix))
|
||||
// console.log('@trimmedCompletionMiddle: ', JSON.stringify(trimmedCompletionMiddle))
|
||||
|
||||
if (trimmedCurrentPrefix.length < trimmedCompletionPrefix.length) { // user must write text beyond the original prefix at generation time
|
||||
// console.log('@undefined1')
|
||||
return undefined
|
||||
}
|
||||
|
||||
if ( // check that completion starts with the prefix
|
||||
!(trimmedCompletionPrefix + trimmedCompletionMiddle)
|
||||
.startsWith(trimmedCurrentPrefix)
|
||||
) {
|
||||
// console.log('@undefined2')
|
||||
return undefined
|
||||
}
|
||||
|
||||
// reverse map to find position wrt `autocompletion.result`
|
||||
const lineStart =
|
||||
trimmedCurrentPrefix.split(_ln).length -
|
||||
trimmedCompletionPrefix.split(_ln).length;
|
||||
|
||||
if (lineStart < 0) {
|
||||
// console.log('@undefined3')
|
||||
|
||||
console.error('Error: No line found.');
|
||||
return undefined;
|
||||
}
|
||||
const currentPrefixLine = getLastLine(trimmedCurrentPrefix)
|
||||
const completionPrefixLine = lineStart === 0 ? getLastLine(trimmedCompletionPrefix) : ''
|
||||
const completionMiddleLine = autocompletion.insertText.split(_ln)[lineStart]
|
||||
const fullCompletionLine = completionPrefixLine + completionMiddleLine
|
||||
|
||||
// console.log('currentPrefixLine', currentPrefixLine)
|
||||
// console.log('completionPrefixLine', completionPrefixLine)
|
||||
// console.log('completionMiddleLine', completionMiddleLine)
|
||||
|
||||
const charMatchIdx = fullCompletionLine.indexOf(currentPrefixLine)
|
||||
if (charMatchIdx < 0) {
|
||||
// console.log('@undefined4', charMatchIdx)
|
||||
|
||||
console.error('Warning: Found character with negative index. This should never happen.')
|
||||
return undefined
|
||||
}
|
||||
|
||||
const character = (charMatchIdx +
|
||||
currentPrefixLine.length
|
||||
- completionPrefixLine.length
|
||||
)
|
||||
|
||||
const startIdx = getIndex(autocompletion.insertText, lineStart, character)
|
||||
|
||||
return {
|
||||
startLine: lineStart,
|
||||
startCharacter: character,
|
||||
startIdx,
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
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
|
||||
|
||||
// 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)
|
||||
|
||||
let completionOptions: CompletionOptions
|
||||
|
||||
// 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
|
||||
|
||||
// TODO add context to prefix
|
||||
// llmPrefix = '\n\n/* Relevant context:\n' + relevantContext + '\n*/\n' + llmPrefix
|
||||
|
||||
// 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: []
|
||||
}
|
||||
}
|
||||
|
||||
return completionOptions
|
||||
|
||||
}
|
||||
|
||||
export interface IAutocompleteService {
|
||||
readonly _serviceBrand: undefined;
|
||||
}
|
||||
|
||||
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 _lastCompletionStart = 0
|
||||
private _lastCompletionAccept = 0
|
||||
// private _lastPrefix: string = ''
|
||||
|
||||
// used internally by vscode
|
||||
// fires after every keystroke and returns the completion to show
|
||||
async _provideInlineCompletionItems(
|
||||
model: ITextModel,
|
||||
position: Position,
|
||||
context: InlineCompletionContext,
|
||||
token: CancellationToken,
|
||||
): Promise<InlineCompletion[]> {
|
||||
|
||||
const testMode = false
|
||||
|
||||
const docUriStr = model.uri.toString();
|
||||
|
||||
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,
|
||||
(autocompletion: Autocompletion) => {
|
||||
if (autocompletion.requestId)
|
||||
this._llmMessageService.abort(autocompletion.requestId)
|
||||
}
|
||||
)
|
||||
}
|
||||
// this._lastPrefix = prefix
|
||||
|
||||
// print all pending autocompletions
|
||||
// let _numPending = 0
|
||||
// this._autocompletionsOfDocument[docUriStr].items.forEach((a: Autocompletion) => { if (a.status === 'pending') _numPending += 1 })
|
||||
// console.log('@numPending: ' + _numPending)
|
||||
|
||||
// get autocompletion from cache
|
||||
let cachedAutocompletion: Autocompletion | undefined = undefined
|
||||
let autocompletionMatchup: AutocompletionMatchupBounds | undefined = undefined
|
||||
for (const autocompletion of this._autocompletionsOfDocument[docUriStr].items.values()) {
|
||||
// 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 && autocompletionMatchup) {
|
||||
|
||||
console.log('AA')
|
||||
|
||||
|
||||
// console.log('id: ' + cachedAutocompletion.id)
|
||||
|
||||
if (cachedAutocompletion.status === 'finished') {
|
||||
console.log('A1')
|
||||
|
||||
const inlineCompletions = toInlineCompletions({ autocompletionMatchup, autocompletion: cachedAutocompletion, prefixAndSuffix, position, debug: true })
|
||||
return inlineCompletions
|
||||
|
||||
} else if (cachedAutocompletion.status === 'pending') {
|
||||
console.log('A2')
|
||||
|
||||
try {
|
||||
await cachedAutocompletion.llmPromise;
|
||||
const inlineCompletions = toInlineCompletions({ autocompletionMatchup, autocompletion: cachedAutocompletion, prefixAndSuffix, position })
|
||||
return inlineCompletions
|
||||
|
||||
} catch (e) {
|
||||
this._autocompletionsOfDocument[docUriStr].delete(cachedAutocompletion.id)
|
||||
console.error('Error creating autocompletion (1): ' + e)
|
||||
}
|
||||
|
||||
} else if (cachedAutocompletion.status === 'error') {
|
||||
console.log('A3')
|
||||
} 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()
|
||||
|
||||
const justAcceptedAutocompletion = thisTime - this._lastCompletionAccept < 500
|
||||
|
||||
this._lastCompletionStart = thisTime
|
||||
const didTypingHappenDuringDebounce = await new Promise((resolve, reject) =>
|
||||
setTimeout(() => {
|
||||
if (this._lastCompletionStart === thisTime) {
|
||||
resolve(false)
|
||||
} else {
|
||||
resolve(true)
|
||||
}
|
||||
}, DEBOUNCE_TIME)
|
||||
)
|
||||
|
||||
// if more typing happened, then do not go forwards with the request
|
||||
if (didTypingHappenDuringDebounce) {
|
||||
return []
|
||||
}
|
||||
|
||||
|
||||
// if there are too many pending requests, cancel the oldest one
|
||||
let numPending = 0
|
||||
let oldestPending: Autocompletion | undefined = undefined
|
||||
for (const autocompletion of this._autocompletionsOfDocument[docUriStr].items.values()) {
|
||||
if (autocompletion.status === 'pending') {
|
||||
numPending += 1
|
||||
if (oldestPending === undefined) {
|
||||
oldestPending = autocompletion
|
||||
}
|
||||
if (numPending >= MAX_PENDING_REQUESTS) {
|
||||
// cancel the oldest pending request and remove it from cache
|
||||
this._autocompletionsOfDocument[docUriStr].delete(oldestPending.id)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 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 []
|
||||
|
||||
if (testMode && this._autocompletionId !== 0) { // TODO remove this
|
||||
return []
|
||||
}
|
||||
|
||||
|
||||
|
||||
// console.log('B')
|
||||
|
||||
// create a new autocompletion and add it to cache
|
||||
const newAutocompletion: Autocompletion = {
|
||||
id: this._autocompletionId++,
|
||||
prefix: prefix, // 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({
|
||||
messagesType: 'FIMMessage',
|
||||
messages: {
|
||||
prefix: llmPrefix,
|
||||
suffix: llmSuffix,
|
||||
stopTokens: stopTokens,
|
||||
},
|
||||
useProviderFor: 'Autocomplete',
|
||||
logging: { loggingName: 'Autocomplete' },
|
||||
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 }) => {
|
||||
|
||||
// console.log('____res: ', JSON.stringify(newAutocompletion.insertText))
|
||||
|
||||
newAutocompletion.endTime = Date.now()
|
||||
newAutocompletion.status = 'finished'
|
||||
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)
|
||||
|
||||
},
|
||||
onError: ({ message }) => {
|
||||
newAutocompletion.endTime = Date.now()
|
||||
newAutocompletion.status = 'error'
|
||||
reject(message)
|
||||
},
|
||||
})
|
||||
newAutocompletion.requestId = requestId
|
||||
|
||||
// if the request hasnt resolved in TIMEOUT_TIME seconds, reject it
|
||||
setTimeout(() => {
|
||||
if (newAutocompletion.status === 'pending') {
|
||||
reject('Timeout receiving message to LLM.')
|
||||
}
|
||||
}, TIMEOUT_TIME)
|
||||
|
||||
})
|
||||
|
||||
|
||||
|
||||
// add autocompletion to cache
|
||||
this._autocompletionsOfDocument[docUriStr].set(newAutocompletion.id, newAutocompletion)
|
||||
|
||||
// show autocompletion
|
||||
try {
|
||||
await newAutocompletion.llmPromise
|
||||
// console.log('id: ' + newAutocompletion.id)
|
||||
|
||||
const autocompletionMatchup: AutocompletionMatchupBounds = { startIdx: 0, startLine: 0, startCharacter: 0 }
|
||||
const inlineCompletions = toInlineCompletions({ autocompletionMatchup, autocompletion: newAutocompletion, prefixAndSuffix, position })
|
||||
return inlineCompletions
|
||||
|
||||
} catch (e) {
|
||||
this._autocompletionsOfDocument[docUriStr].delete(newAutocompletion.id)
|
||||
console.error('Error creating autocompletion (2): ' + e)
|
||||
return []
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
constructor(
|
||||
@ILanguageFeaturesService private _langFeatureService: ILanguageFeaturesService,
|
||||
@ILLMMessageService private readonly _llmMessageService: ILLMMessageService,
|
||||
@IEditorService private readonly _editorService: IEditorService,
|
||||
@IModelService private readonly _modelService: IModelService,
|
||||
// @IContextGatheringService private readonly _contextGatheringService: IContextGatheringService,
|
||||
) {
|
||||
super()
|
||||
|
||||
this._langFeatureService.inlineCompletionsProvider.register('*', {
|
||||
provideInlineCompletions: async (model, position, context, token) => {
|
||||
const items = await this._provideInlineCompletionItems(model, position, context, token)
|
||||
|
||||
// console.log('item: ', items?.[0]?.insertText)
|
||||
return { items: items, }
|
||||
},
|
||||
freeInlineCompletions: (completions) => {
|
||||
// get the `docUriStr` and the `position` of the cursor
|
||||
const activePane = this._editorService.activeEditorPane;
|
||||
if (!activePane) return;
|
||||
const control = activePane.getControl();
|
||||
if (!control || !isCodeEditor(control)) return;
|
||||
const position = control.getPosition();
|
||||
if (!position) return;
|
||||
const resource = EditorResourceAccessor.getCanonicalUri(this._editorService.activeEditor);
|
||||
if (!resource) return;
|
||||
const model = this._modelService.getModel(resource)
|
||||
if (!model) return;
|
||||
const docUriStr = resource.toString();
|
||||
if (!this._autocompletionsOfDocument[docUriStr]) return;
|
||||
|
||||
const { prefix, } = getPrefixAndSuffixInfo(model, position)
|
||||
|
||||
// go through cached items and remove matching ones
|
||||
// autocompletion.prefix + autocompletion.insertedText ~== insertedText
|
||||
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);
|
||||
}
|
||||
});
|
||||
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
registerWorkbenchContribution2(AutocompleteService.ID, AutocompleteService, WorkbenchPhase.BlockRestore);
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,552 @@
|
||||
/*--------------------------------------------------------------------------------------
|
||||
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Disposable } from '../../../../base/common/lifecycle.js';
|
||||
import { registerSingleton, InstantiationType } from '../../../../platform/instantiation/common/extensions.js';
|
||||
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
|
||||
import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js';
|
||||
|
||||
import { URI } from '../../../../base/common/uri.js';
|
||||
import { Emitter, Event } from '../../../../base/common/event.js';
|
||||
import { IRange } from '../../../../editor/common/core/range.js';
|
||||
import { ILLMMessageService } from '../common/llmMessageService.js';
|
||||
import { IModelService } from '../../../../editor/common/services/model.js';
|
||||
import { chat_userMessageContent, chat_systemMessage, chat_userMessageContentWithAllFilesToo as chat_userMessageContentWithAllFiles } from './prompt/prompts.js';
|
||||
import { LLMChatMessage } from '../common/llmMessageTypes.js';
|
||||
import { IFileService } from '../../../../platform/files/common/files.js';
|
||||
|
||||
// one of the square items that indicates a selection in a chat bubble (NOT a file, a Selection of text)
|
||||
export type CodeSelection = {
|
||||
type: 'Selection';
|
||||
fileURI: URI;
|
||||
selectionStr: string;
|
||||
range: IRange;
|
||||
}
|
||||
|
||||
export type FileSelection = {
|
||||
type: 'File';
|
||||
fileURI: URI;
|
||||
selectionStr: null;
|
||||
range: null;
|
||||
}
|
||||
|
||||
export type StagingSelectionItem = CodeSelection | FileSelection
|
||||
|
||||
// WARNING: changing this format is a big deal!!!!!! need to migrate old format to new format on users' computers so people don't get errors.
|
||||
export type ChatMessage =
|
||||
| {
|
||||
role: 'user';
|
||||
content: string | null; // content displayed to the LLM on future calls - allowed to be '', will be replaced with (empty)
|
||||
displayContent: string | null; // content displayed to user - allowed to be '', will be ignored
|
||||
selections: StagingSelectionItem[] | null; // the user's selection
|
||||
state: {
|
||||
stagingSelections: StagingSelectionItem[];
|
||||
isBeingEdited: boolean;
|
||||
}
|
||||
}
|
||||
| {
|
||||
role: 'assistant';
|
||||
content: string | null; // content received from LLM - allowed to be '', will be replaced with (empty)
|
||||
displayContent: string | null; // content displayed to user (this is the same as content for now) - allowed to be '', will be ignored
|
||||
}
|
||||
| {
|
||||
role: 'system';
|
||||
content: string;
|
||||
displayContent?: undefined;
|
||||
}
|
||||
|
||||
type UserMessageType = ChatMessage & { role: 'user' }
|
||||
type UserMessageState = UserMessageType['state']
|
||||
|
||||
export const defaultMessageState: UserMessageState = { stagingSelections: [], isBeingEdited: false }
|
||||
|
||||
// a 'thread' means a chat message history
|
||||
export type ChatThreads = {
|
||||
[id: string]: {
|
||||
id: string; // store the id here too
|
||||
createdAt: string; // ISO string
|
||||
lastModified: string; // ISO string
|
||||
messages: ChatMessage[];
|
||||
state: {
|
||||
stagingSelections: StagingSelectionItem[];
|
||||
focusedMessageIdx: number | undefined; // index of the message that is being edited (undefined if none)
|
||||
isCheckedOfSelectionId: { [selectionId: string]: boolean };
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
type ThreadType = ChatThreads[string]
|
||||
|
||||
const defaultThreadState: ThreadType['state'] = { stagingSelections: [], focusedMessageIdx: undefined, isCheckedOfSelectionId: {} }
|
||||
|
||||
export type ThreadsState = {
|
||||
allThreads: ChatThreads;
|
||||
currentThreadId: string; // intended for internal use only
|
||||
}
|
||||
|
||||
export type ThreadStreamState = {
|
||||
[threadId: string]: undefined | {
|
||||
error?: { message: string, fullError: Error | null, };
|
||||
messageSoFar?: string;
|
||||
streamingToken?: string;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const newThreadObject = () => {
|
||||
const now = new Date().toISOString()
|
||||
return {
|
||||
id: new Date().getTime().toString(),
|
||||
createdAt: now,
|
||||
lastModified: now,
|
||||
messages: [],
|
||||
state: {
|
||||
stagingSelections: [],
|
||||
focusedMessageIdx: undefined,
|
||||
isCheckedOfSelectionId: {}
|
||||
},
|
||||
|
||||
} satisfies ChatThreads[string]
|
||||
}
|
||||
|
||||
const THREAD_VERSION_KEY = 'void.chatThreadVersion'
|
||||
const THREAD_VERSION = 'v2'
|
||||
|
||||
const THREAD_STORAGE_KEY = 'void.chatThreadStorage'
|
||||
|
||||
export interface IChatThreadService {
|
||||
readonly _serviceBrand: undefined;
|
||||
|
||||
readonly state: ThreadsState;
|
||||
readonly streamState: ThreadStreamState;
|
||||
|
||||
onDidChangeCurrentThread: Event<void>;
|
||||
onDidChangeStreamState: Event<{ threadId: string }>
|
||||
|
||||
getCurrentThread(): ChatThreads[string];
|
||||
openNewThread(): void;
|
||||
switchToThread(threadId: string): void;
|
||||
|
||||
getFocusedMessageIdx(): number | undefined;
|
||||
isFocusingMessage(): boolean;
|
||||
setFocusedMessageIdx(messageIdx: number | undefined): void;
|
||||
|
||||
// _useFocusedStagingState(messageIdx?: number | undefined): readonly [StagingInfo, (stagingInfo: StagingInfo) => void];
|
||||
_useCurrentThreadState(): readonly [ThreadType['state'], (newState: Partial<ThreadType['state']>) => void];
|
||||
_useCurrentMessageState(messageIdx: number): readonly [UserMessageState, (newState: Partial<UserMessageState>) => void];
|
||||
|
||||
editUserMessageAndStreamResponse(userMessage: string, messageIdx: number): Promise<void>;
|
||||
addUserMessageAndStreamResponse(userMessage: string): Promise<void>;
|
||||
cancelStreaming(threadId: string): void;
|
||||
dismissStreamError(threadId: string): void;
|
||||
|
||||
}
|
||||
|
||||
export const IChatThreadService = createDecorator<IChatThreadService>('voidChatThreadService');
|
||||
class ChatThreadService extends Disposable implements IChatThreadService {
|
||||
_serviceBrand: undefined;
|
||||
|
||||
// this fires when the current thread changes at all (a switch of currentThread, or a message added to it, etc)
|
||||
private readonly _onDidChangeCurrentThread = new Emitter<void>();
|
||||
readonly onDidChangeCurrentThread: Event<void> = this._onDidChangeCurrentThread.event;
|
||||
|
||||
readonly streamState: ThreadStreamState = {}
|
||||
private readonly _onDidChangeStreamState = new Emitter<{ threadId: string }>();
|
||||
readonly onDidChangeStreamState: Event<{ threadId: string }> = this._onDidChangeStreamState.event;
|
||||
|
||||
state: ThreadsState // allThreads is persisted, currentThread is not
|
||||
|
||||
constructor(
|
||||
@IStorageService private readonly _storageService: IStorageService,
|
||||
@IModelService private readonly _modelService: IModelService,
|
||||
@IFileService private readonly _fileService: IFileService,
|
||||
@ILLMMessageService private readonly _llmMessageService: ILLMMessageService,
|
||||
) {
|
||||
super()
|
||||
|
||||
this.state = {
|
||||
allThreads: this._readAllThreads(),
|
||||
currentThreadId: null as unknown as string, // gets set in startNewThread()
|
||||
}
|
||||
|
||||
// always be in a thread
|
||||
this.openNewThread()
|
||||
|
||||
// for now just write the version, anticipating bigger changes in the future where we'll want to access this
|
||||
this._storageService.store(THREAD_VERSION_KEY, THREAD_VERSION, StorageScope.APPLICATION, StorageTarget.USER)
|
||||
|
||||
}
|
||||
|
||||
|
||||
private _readAllThreads(): ChatThreads {
|
||||
// PUT ANY VERSION CHANGE FORMAT CONVERSION CODE HERE
|
||||
// CAN ADD "v0" TAG IN STORAGE AND CONVERT
|
||||
|
||||
|
||||
const threadsStr = this._storageService.get(THREAD_STORAGE_KEY, StorageScope.APPLICATION)
|
||||
|
||||
const threads: ChatThreads = threadsStr ? JSON.parse(threadsStr) : {}
|
||||
|
||||
this._updateThreadsToVersion(threads, THREAD_VERSION)
|
||||
|
||||
return threads
|
||||
}
|
||||
|
||||
|
||||
private _updateThreadsToVersion(oldThreadsObject: any, toVersion: string) {
|
||||
|
||||
if (toVersion === 'v2') {
|
||||
|
||||
const threads: ChatThreads = oldThreadsObject
|
||||
|
||||
/** v1 -> v2
|
||||
- threads.state.currentStagingSelections: CodeStagingSelection[] | null;
|
||||
+ thread[threadIdx].state
|
||||
+ message.state
|
||||
*/
|
||||
|
||||
// check if we need to update
|
||||
let shouldUpdate = false
|
||||
for (const thread of Object.values(threads)) {
|
||||
if (!thread.state) {
|
||||
shouldUpdate = true
|
||||
}
|
||||
for (const chatMessage of Object.values(thread.messages)) {
|
||||
if (chatMessage.role === 'user' && !chatMessage.state) {
|
||||
shouldUpdate = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!shouldUpdate) return;
|
||||
|
||||
// update the threads
|
||||
for (const thread of Object.values(threads)) {
|
||||
if (!thread.state) {
|
||||
thread.state = defaultThreadState
|
||||
}
|
||||
for (const chatMessage of Object.values(thread.messages)) {
|
||||
if (chatMessage.role === 'user' && !chatMessage.state) {
|
||||
chatMessage.state = defaultMessageState
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// push the update
|
||||
this._storeAllThreads(threads)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private _storeAllThreads(threads: ChatThreads) {
|
||||
this._storageService.store(THREAD_STORAGE_KEY, JSON.stringify(threads), StorageScope.APPLICATION, StorageTarget.USER)
|
||||
}
|
||||
|
||||
// this should be the only place this.state = ... appears besides constructor
|
||||
private _setState(state: Partial<ThreadsState>, affectsCurrent: boolean) {
|
||||
this.state = {
|
||||
...this.state,
|
||||
...state
|
||||
}
|
||||
if (affectsCurrent)
|
||||
this._onDidChangeCurrentThread.fire()
|
||||
}
|
||||
|
||||
private _getAllSelections() {
|
||||
const thread = this.getCurrentThread()
|
||||
return thread.messages.flatMap(m => m.role === 'user' && m.selections || [])
|
||||
}
|
||||
|
||||
private _getSelectionsUpToMessageIdx(messageIdx: number) {
|
||||
const thread = this.getCurrentThread()
|
||||
const prevMessages = thread.messages.slice(0, messageIdx)
|
||||
return prevMessages.flatMap(m => m.role === 'user' && m.selections || [])
|
||||
}
|
||||
|
||||
private _setStreamState(threadId: string, state: Partial<NonNullable<ThreadStreamState[string]>>) {
|
||||
this.streamState[threadId] = {
|
||||
...this.streamState[threadId],
|
||||
...state
|
||||
}
|
||||
this._onDidChangeStreamState.fire({ threadId })
|
||||
}
|
||||
|
||||
|
||||
// ---------- streaming ----------
|
||||
|
||||
finishStreaming = (threadId: string, content: string, error?: { message: string, fullError: Error | null }) => {
|
||||
// add assistant's message to chat history, and clear selection
|
||||
const assistantHistoryElt: ChatMessage = { role: 'assistant', content, displayContent: content || null }
|
||||
this._addMessageToThread(threadId, assistantHistoryElt)
|
||||
this._setStreamState(threadId, { messageSoFar: undefined, streamingToken: undefined, error })
|
||||
}
|
||||
|
||||
|
||||
async editUserMessageAndStreamResponse(userMessage: string, messageIdx: number) {
|
||||
|
||||
const thread = this.getCurrentThread()
|
||||
|
||||
if (thread.messages?.[messageIdx]?.role !== 'user') {
|
||||
throw new Error("Error: editing a message with role !=='user'")
|
||||
}
|
||||
|
||||
// get prev and curr selections before clearing the message
|
||||
const prevSelns = this._getSelectionsUpToMessageIdx(messageIdx)
|
||||
const currSelns = thread.messages[messageIdx].selections || []
|
||||
|
||||
// clear messages up to the index
|
||||
const slicedMessages = thread.messages.slice(0, messageIdx)
|
||||
this._setState({
|
||||
allThreads: {
|
||||
...this.state.allThreads,
|
||||
[thread.id]: {
|
||||
...thread,
|
||||
messages: slicedMessages
|
||||
}
|
||||
}
|
||||
}, true)
|
||||
|
||||
// stream the edit
|
||||
this.addUserMessageAndStreamResponse(userMessage, { prevSelns, currSelns })
|
||||
|
||||
}
|
||||
|
||||
async addUserMessageAndStreamResponse(userMessage: string, options?: { prevSelns?: StagingSelectionItem[], currSelns?: StagingSelectionItem[] }) {
|
||||
|
||||
const thread = this.getCurrentThread()
|
||||
const threadId = thread.id
|
||||
|
||||
// add user's message to chat history
|
||||
const instructions = userMessage
|
||||
|
||||
const prevSelns: StagingSelectionItem[] = options?.prevSelns ?? this._getAllSelections()
|
||||
const currSelns: StagingSelectionItem[] = options?.currSelns ?? thread.state.stagingSelections
|
||||
|
||||
// read all curr+previous files on demand instead of adding them to the history
|
||||
const messageContent = await chat_userMessageContent(instructions, prevSelns, currSelns)
|
||||
const messageContentWithAllFiles = await chat_userMessageContentWithAllFiles(instructions, prevSelns, currSelns, this._modelService, this._fileService)
|
||||
const prevLLMMessages = this.getCurrentThread().messages.map(m => ({ role: m.role, content: m.content || '(empty model output)' }))
|
||||
const currLLMMessage: LLMChatMessage = { role: 'user', content: messageContentWithAllFiles }
|
||||
|
||||
const userHistoryElt: ChatMessage = { role: 'user', content: messageContent, displayContent: instructions, selections: currSelns, state: defaultMessageState }
|
||||
this._addMessageToThread(threadId, userHistoryElt)
|
||||
|
||||
this._setStreamState(threadId, { error: undefined })
|
||||
|
||||
console.log(`messageContent`)
|
||||
console.log([{ role: 'system', content: chat_systemMessage },
|
||||
...prevLLMMessages,
|
||||
currLLMMessage,])
|
||||
|
||||
const llmCancelToken = this._llmMessageService.sendLLMMessage({
|
||||
messagesType: 'chatMessages',
|
||||
logging: { loggingName: 'Chat' },
|
||||
useProviderFor: 'Ctrl+L',
|
||||
messages: [
|
||||
{ role: 'system', content: chat_systemMessage },
|
||||
...prevLLMMessages,
|
||||
currLLMMessage,
|
||||
],
|
||||
onText: ({ newText, fullText }) => {
|
||||
this._setStreamState(threadId, { messageSoFar: fullText })
|
||||
},
|
||||
onFinalMessage: ({ fullText: content }) => {
|
||||
this.finishStreaming(threadId, content)
|
||||
},
|
||||
onError: (error) => {
|
||||
this.finishStreaming(threadId, this.streamState[threadId]?.messageSoFar ?? '', error)
|
||||
},
|
||||
|
||||
})
|
||||
if (llmCancelToken === null) return
|
||||
this._setStreamState(threadId, { streamingToken: llmCancelToken })
|
||||
|
||||
}
|
||||
|
||||
cancelStreaming(threadId: string) {
|
||||
const llmCancelToken = this.streamState[threadId]?.streamingToken
|
||||
if (llmCancelToken !== undefined) this._llmMessageService.abort(llmCancelToken)
|
||||
this.finishStreaming(threadId, this.streamState[threadId]?.messageSoFar ?? '')
|
||||
}
|
||||
|
||||
dismissStreamError(threadId: string): void {
|
||||
this._setStreamState(threadId, { error: undefined })
|
||||
}
|
||||
|
||||
|
||||
|
||||
// ---------- the rest ----------
|
||||
|
||||
getCurrentThread(): ChatThreads[string] {
|
||||
const state = this.state
|
||||
return state.allThreads[state.currentThreadId]
|
||||
}
|
||||
|
||||
getFocusedMessageIdx() {
|
||||
const thread = this.getCurrentThread()
|
||||
|
||||
// get the focusedMessageIdx
|
||||
const focusedMessageIdx = thread.state.focusedMessageIdx
|
||||
if (focusedMessageIdx === undefined) return;
|
||||
|
||||
// check that the message is actually being edited
|
||||
const focusedMessage = thread.messages[focusedMessageIdx]
|
||||
if (focusedMessage.role !== 'user') return;
|
||||
if (!focusedMessage.state) return;
|
||||
|
||||
return focusedMessageIdx
|
||||
}
|
||||
|
||||
isFocusingMessage() {
|
||||
return this.getFocusedMessageIdx() !== undefined
|
||||
}
|
||||
|
||||
switchToThread(threadId: string) {
|
||||
this._setState({ currentThreadId: threadId }, true)
|
||||
}
|
||||
|
||||
|
||||
openNewThread() {
|
||||
// if a thread with 0 messages already exists, switch to it
|
||||
const { allThreads: currentThreads } = this.state
|
||||
for (const threadId in currentThreads) {
|
||||
if (currentThreads[threadId].messages.length === 0) {
|
||||
this.switchToThread(threadId)
|
||||
return
|
||||
}
|
||||
}
|
||||
// otherwise, start a new thread
|
||||
const newThread = newThreadObject()
|
||||
|
||||
// update state
|
||||
const newThreads: ChatThreads = {
|
||||
...currentThreads,
|
||||
[newThread.id]: newThread
|
||||
}
|
||||
this._storeAllThreads(newThreads)
|
||||
this._setState({ allThreads: newThreads, currentThreadId: newThread.id }, true)
|
||||
}
|
||||
|
||||
|
||||
_addMessageToThread(threadId: string, message: ChatMessage) {
|
||||
const { allThreads } = this.state
|
||||
|
||||
const oldThread = allThreads[threadId]
|
||||
|
||||
// update state and store it
|
||||
const newThreads = {
|
||||
...allThreads,
|
||||
[oldThread.id]: {
|
||||
...oldThread,
|
||||
lastModified: new Date().toISOString(),
|
||||
messages: [...oldThread.messages, message],
|
||||
}
|
||||
}
|
||||
this._storeAllThreads(newThreads)
|
||||
this._setState({ allThreads: newThreads }, true) // the current thread just changed (it had a message added to it)
|
||||
}
|
||||
|
||||
// sets the currently selected message (must be undefined if no message is selected)
|
||||
setFocusedMessageIdx(messageIdx: number | undefined) {
|
||||
|
||||
const threadId = this.state.currentThreadId
|
||||
const thread = this.state.allThreads[threadId]
|
||||
if (!thread) return
|
||||
|
||||
this._setState({
|
||||
allThreads: {
|
||||
...this.state.allThreads,
|
||||
[threadId]: {
|
||||
...thread,
|
||||
state: {
|
||||
...thread.state,
|
||||
focusedMessageIdx: messageIdx,
|
||||
}
|
||||
}
|
||||
}
|
||||
}, true)
|
||||
}
|
||||
|
||||
// set message.state
|
||||
private _setCurrentMessageState(state: Partial<UserMessageState>, messageIdx: number): void {
|
||||
|
||||
const threadId = this.state.currentThreadId
|
||||
const thread = this.state.allThreads[threadId]
|
||||
if (!thread) return
|
||||
|
||||
this._setState({
|
||||
allThreads: {
|
||||
...this.state.allThreads,
|
||||
[threadId]: {
|
||||
...thread,
|
||||
messages: thread.messages.map((m, i) =>
|
||||
i === messageIdx && m.role === 'user' ? {
|
||||
...m,
|
||||
state: {
|
||||
...m.state,
|
||||
...state
|
||||
},
|
||||
} : m
|
||||
)
|
||||
}
|
||||
}
|
||||
}, true)
|
||||
|
||||
}
|
||||
|
||||
// set thread.state
|
||||
private _setCurrentThreadState(state: Partial<ThreadType['state']>): void {
|
||||
|
||||
const threadId = this.state.currentThreadId
|
||||
const thread = this.state.allThreads[threadId]
|
||||
if (!thread) return
|
||||
|
||||
this._setState({
|
||||
allThreads: {
|
||||
...this.state.allThreads,
|
||||
[thread.id]: {
|
||||
...thread,
|
||||
state: {
|
||||
...thread.state,
|
||||
...state
|
||||
}
|
||||
}
|
||||
}
|
||||
}, true)
|
||||
|
||||
}
|
||||
|
||||
|
||||
_useCurrentMessageState(messageIdx: number) {
|
||||
|
||||
const thread = this.getCurrentThread()
|
||||
const messages = thread.messages
|
||||
const currMessage = messages[messageIdx]
|
||||
|
||||
if (currMessage.role !== 'user') {
|
||||
return [defaultMessageState, (s: any) => { }] as const
|
||||
}
|
||||
|
||||
const state = currMessage.state
|
||||
const setState = (newState: Partial<UserMessageState>) => this._setCurrentMessageState(newState, messageIdx)
|
||||
|
||||
return [state, setState] as const
|
||||
|
||||
}
|
||||
|
||||
_useCurrentThreadState() {
|
||||
const thread = this.getCurrentThread()
|
||||
|
||||
const state = thread.state
|
||||
const setState = this._setCurrentThreadState.bind(this)
|
||||
|
||||
return [state, setState] as const
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
registerSingleton(IChatThreadService, ChatThreadService, InstantiationType.Eager);
|
||||
|
||||
@@ -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,18 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Glass Devtools, Inc. All rights reserved.
|
||||
* Void Editor additions licensed under the AGPLv3 License.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { OperatingSystem, OS } from '../../../../base/common/platform.js';
|
||||
|
||||
export function getCmdKey(): string {
|
||||
if (OS === OperatingSystem.Macintosh) {
|
||||
return '⌘';
|
||||
} else {
|
||||
return 'Ctrl';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,422 @@
|
||||
/*--------------------------------------------------------------------------------------
|
||||
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Disposable } from '../../../../../base/common/lifecycle.js';
|
||||
import { URI } from '../../../../../base/common/uri.js';
|
||||
import { generateUuid } from '../../../../../base/common/uuid.js';
|
||||
import { ICodeEditor } from '../../../../../editor/browser/editorBrowser.js';
|
||||
import { ICodeEditorService } from '../../../../../editor/browser/services/codeEditorService.js';
|
||||
import { InstantiationType, registerSingleton } from '../../../../../platform/instantiation/common/extensions.js';
|
||||
import { createDecorator } from '../../../../../platform/instantiation/common/instantiation.js';
|
||||
|
||||
|
||||
// lets you add a "consistent" item to a Model (aka URI), instead of just to a single editor
|
||||
|
||||
type AddItemInputs = { uri: URI; fn: (editor: ICodeEditor) => (() => void); }
|
||||
|
||||
export interface IConsistentItemService {
|
||||
readonly _serviceBrand: undefined;
|
||||
getEditorsOnURI(uri: URI): ICodeEditor[];
|
||||
addConsistentItemToURI(inputs: AddItemInputs): string;
|
||||
removeConsistentItemFromURI(consistentItemId: string): void;
|
||||
}
|
||||
|
||||
export const IConsistentItemService = createDecorator<IConsistentItemService>('ConsistentItemService');
|
||||
|
||||
export class ConsistentItemService extends Disposable {
|
||||
|
||||
readonly _serviceBrand: undefined
|
||||
|
||||
// the items that are attached to each URI, completely independent from current state of editors
|
||||
private readonly consistentItemIdsOfURI: Record<string, Set<string> | undefined> = {}
|
||||
private readonly infoOfConsistentItemId: Record<string, AddItemInputs> = {}
|
||||
|
||||
|
||||
// current state of items on each editor, and the fns to call to remove them
|
||||
private readonly itemIdsOfEditorId: Record<string, Set<string> | undefined> = {}
|
||||
private readonly consistentItemIdOfItemId: Record<string, string> = {}
|
||||
private readonly disposeFnOfItemId: Record<string, () => void> = {}
|
||||
|
||||
|
||||
constructor(
|
||||
@ICodeEditorService private readonly _editorService: ICodeEditorService,
|
||||
) {
|
||||
super()
|
||||
|
||||
|
||||
const removeItemsFromEditor = (editor: ICodeEditor) => {
|
||||
const editorId = editor.getId()
|
||||
for (const itemId of this.itemIdsOfEditorId[editorId] ?? [])
|
||||
this._removeItemFromEditor(editor, itemId)
|
||||
}
|
||||
|
||||
// put items on the editor, based on the consistent items for that URI
|
||||
const putItemsOnEditor = (editor: ICodeEditor, uri: URI | null) => {
|
||||
if (!uri) return
|
||||
for (const consistentItemId of this.consistentItemIdsOfURI[uri.fsPath] ?? [])
|
||||
this._putItemOnEditor(editor, consistentItemId)
|
||||
}
|
||||
|
||||
|
||||
// when editor switches tabs (models)
|
||||
const addTabSwitchListeners = (editor: ICodeEditor) => {
|
||||
this._register(
|
||||
editor.onDidChangeModel(e => {
|
||||
removeItemsFromEditor(editor)
|
||||
putItemsOnEditor(editor, e.newModelUrl)
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
// when editor is disposed
|
||||
const addDisposeListener = (editor: ICodeEditor) => {
|
||||
this._register(editor.onDidDispose(() => {
|
||||
// anything on the editor has been disposed already
|
||||
for (const itemId of this.itemIdsOfEditorId[editor.getId()] ?? [])
|
||||
delete this.disposeFnOfItemId[itemId]
|
||||
}))
|
||||
}
|
||||
|
||||
const initializeEditor = (editor: ICodeEditor) => {
|
||||
// if (editor.getModel()?.uri.scheme !== 'file') return // THIS BREAKS THINGS
|
||||
addTabSwitchListeners(editor)
|
||||
addDisposeListener(editor)
|
||||
putItemsOnEditor(editor, editor.getModel()?.uri ?? null)
|
||||
}
|
||||
|
||||
// initialize current editors + any new editors
|
||||
for (let editor of this._editorService.listCodeEditors()) initializeEditor(editor)
|
||||
this._register(this._editorService.onCodeEditorAdd(editor => { initializeEditor(editor) }))
|
||||
|
||||
// when an editor is deleted, remove its items
|
||||
this._register(this._editorService.onCodeEditorRemove(editor => { removeItemsFromEditor(editor) }))
|
||||
}
|
||||
|
||||
|
||||
|
||||
_putItemOnEditor(editor: ICodeEditor, consistentItemId: string) {
|
||||
const { fn } = this.infoOfConsistentItemId[consistentItemId]
|
||||
|
||||
// add item
|
||||
const dispose = fn(editor)
|
||||
|
||||
const itemId = generateUuid()
|
||||
const editorId = editor.getId()
|
||||
|
||||
if (!(editorId in this.itemIdsOfEditorId))
|
||||
this.itemIdsOfEditorId[editorId] = new Set()
|
||||
this.itemIdsOfEditorId[editorId]!.add(itemId)
|
||||
|
||||
|
||||
this.consistentItemIdOfItemId[itemId] = consistentItemId
|
||||
|
||||
this.disposeFnOfItemId[itemId] = () => {
|
||||
// console.log('calling remove for', itemId)
|
||||
dispose?.()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
_removeItemFromEditor(editor: ICodeEditor, itemId: string) {
|
||||
|
||||
const editorId = editor.getId()
|
||||
this.itemIdsOfEditorId[editorId]?.delete(itemId)
|
||||
|
||||
this.disposeFnOfItemId[itemId]?.()
|
||||
delete this.disposeFnOfItemId[itemId]
|
||||
|
||||
delete this.consistentItemIdOfItemId[itemId]
|
||||
}
|
||||
|
||||
getEditorsOnURI(uri: URI) {
|
||||
const editors = this._editorService.listCodeEditors().filter(editor => editor.getModel()?.uri.fsPath === uri.fsPath)
|
||||
return editors
|
||||
}
|
||||
|
||||
consistentItemIdPool = 0
|
||||
addConsistentItemToURI({ uri, fn }: AddItemInputs) {
|
||||
const consistentItemId = (this.consistentItemIdPool++) + ''
|
||||
|
||||
if (!(uri.fsPath in this.consistentItemIdsOfURI))
|
||||
this.consistentItemIdsOfURI[uri.fsPath] = new Set()
|
||||
this.consistentItemIdsOfURI[uri.fsPath]!.add(consistentItemId)
|
||||
|
||||
this.infoOfConsistentItemId[consistentItemId] = { fn, uri }
|
||||
|
||||
const editors = this.getEditorsOnURI(uri)
|
||||
for (const editor of editors)
|
||||
this._putItemOnEditor(editor, consistentItemId)
|
||||
|
||||
return consistentItemId
|
||||
}
|
||||
|
||||
|
||||
removeConsistentItemFromURI(consistentItemId: string) {
|
||||
if (!(consistentItemId in this.infoOfConsistentItemId))
|
||||
return
|
||||
|
||||
const { uri } = this.infoOfConsistentItemId[consistentItemId]
|
||||
const editors = this.getEditorsOnURI(uri)
|
||||
|
||||
for (const editor of editors) {
|
||||
for (const itemId of this.itemIdsOfEditorId[editor.getId()] ?? []) {
|
||||
if (this.consistentItemIdOfItemId[itemId] === consistentItemId)
|
||||
this._removeItemFromEditor(editor, itemId)
|
||||
}
|
||||
}
|
||||
|
||||
// clear
|
||||
this.consistentItemIdsOfURI[uri.fsPath]?.delete(consistentItemId)
|
||||
|
||||
delete this.infoOfConsistentItemId[consistentItemId]
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
registerSingleton(IConsistentItemService, ConsistentItemService, InstantiationType.Eager);
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// mostly generated by o1 (almost the same as above, but just for 1 editor)
|
||||
export interface IConsistentEditorItemService {
|
||||
readonly _serviceBrand: undefined;
|
||||
addToEditor(editor: ICodeEditor, fn: () => () => void): string;
|
||||
removeFromEditor(itemId: string): void;
|
||||
}
|
||||
export const IConsistentEditorItemService = createDecorator<IConsistentEditorItemService>('ConsistentEditorItemService');
|
||||
|
||||
|
||||
export class ConsistentEditorItemService extends Disposable {
|
||||
readonly _serviceBrand: undefined;
|
||||
|
||||
/**
|
||||
* For each editorId, we track the set of itemIds that have been "added" to that editor.
|
||||
* This does *not* necessarily mean they're currently mounted (the user may have switched models).
|
||||
*/
|
||||
private readonly itemIdsByEditorId: Record<string, Set<string>> = {};
|
||||
|
||||
/**
|
||||
* For each itemId, we store relevant info (the fn to call on the editor, the editorId, the uri, and the current dispose function).
|
||||
*/
|
||||
private readonly itemInfoById: Record<
|
||||
string,
|
||||
{
|
||||
editorId: string;
|
||||
uriFsPath: string;
|
||||
fn: (editor: ICodeEditor) => () => void;
|
||||
disposeFn?: () => void;
|
||||
}
|
||||
> = {};
|
||||
|
||||
constructor(
|
||||
@ICodeEditorService private readonly _editorService: ICodeEditorService,
|
||||
) {
|
||||
super();
|
||||
|
||||
//
|
||||
// Wire up listeners to watch for new editors, removed editors, etc.
|
||||
//
|
||||
|
||||
// Initialize any already-existing editors
|
||||
for (const editor of this._editorService.listCodeEditors()) {
|
||||
this._initializeEditor(editor);
|
||||
}
|
||||
|
||||
// When an editor is added, track it
|
||||
this._register(
|
||||
this._editorService.onCodeEditorAdd((editor) => {
|
||||
this._initializeEditor(editor);
|
||||
})
|
||||
);
|
||||
|
||||
// When an editor is removed, remove all items associated with that editor
|
||||
this._register(
|
||||
this._editorService.onCodeEditorRemove((editor) => {
|
||||
this._removeAllItemsFromEditor(editor);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up listeners on the provided editor so that:
|
||||
* - If the editor changes models, we remove items and re-mount only if the new model matches.
|
||||
* - If the editor is disposed, we do the needed cleanup.
|
||||
*/
|
||||
private _initializeEditor(editor: ICodeEditor) {
|
||||
const editorId = editor.getId();
|
||||
|
||||
//
|
||||
// Listen for model changes
|
||||
//
|
||||
this._register(
|
||||
editor.onDidChangeModel((e) => {
|
||||
this._removeAllItemsFromEditor(editor);
|
||||
if (!e.newModelUrl) {
|
||||
return;
|
||||
}
|
||||
// Re-mount any items that belong to this editor and match the new URI
|
||||
const itemsForEditor = this.itemIdsByEditorId[editorId];
|
||||
if (itemsForEditor) {
|
||||
for (const itemId of itemsForEditor) {
|
||||
const itemInfo = this.itemInfoById[itemId];
|
||||
if (itemInfo && itemInfo.uriFsPath === e.newModelUrl.fsPath) {
|
||||
this._mountItemOnEditor(editor, itemId);
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
//
|
||||
// When the editor is disposed, remove all items from it
|
||||
//
|
||||
this._register(
|
||||
editor.onDidDispose(() => {
|
||||
this._removeAllItemsFromEditor(editor);
|
||||
})
|
||||
);
|
||||
|
||||
//
|
||||
// If the editor already has a model (e.g. on initial load), try mounting items
|
||||
//
|
||||
const uri = editor.getModel()?.uri;
|
||||
if (!uri) {
|
||||
return;
|
||||
}
|
||||
|
||||
const itemsForEditor = this.itemIdsByEditorId[editorId];
|
||||
if (itemsForEditor) {
|
||||
for (const itemId of itemsForEditor) {
|
||||
const itemInfo = this.itemInfoById[itemId];
|
||||
if (itemInfo && itemInfo.uriFsPath === uri.fsPath) {
|
||||
this._mountItemOnEditor(editor, itemId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Actually calls the item-creation function `fn(editor)` and saves the resulting disposeFn
|
||||
* so we can later clean it up.
|
||||
*/
|
||||
private _mountItemOnEditor(editor: ICodeEditor, itemId: string) {
|
||||
const info = this.itemInfoById[itemId];
|
||||
if (!info) {
|
||||
return;
|
||||
}
|
||||
const { fn } = info;
|
||||
const disposeFn = fn(editor);
|
||||
info.disposeFn = disposeFn;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a single item from an editor (calling its `disposeFn` if present).
|
||||
*/
|
||||
private _removeItemFromEditor(editor: ICodeEditor, itemId: string) {
|
||||
const info = this.itemInfoById[itemId];
|
||||
if (info?.disposeFn) {
|
||||
info.disposeFn();
|
||||
info.disposeFn = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes *all* items from the given editor. Typically called when the editor changes model or is disposed.
|
||||
*/
|
||||
private _removeAllItemsFromEditor(editor: ICodeEditor) {
|
||||
const editorId = editor.getId();
|
||||
const itemsForEditor = this.itemIdsByEditorId[editorId];
|
||||
if (!itemsForEditor) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const itemId of itemsForEditor) {
|
||||
this._removeItemFromEditor(editor, itemId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Public API: Adds an item to an *individual* editor (determined by editor ID),
|
||||
* but only when that editor is showing the same model (uri.fsPath).
|
||||
*/
|
||||
addToEditor(editor: ICodeEditor, fn: () => () => void): string {
|
||||
const uri = editor.getModel()?.uri
|
||||
if (!uri) {
|
||||
throw new Error('No URI on the provided editor or in AddItemInputs.');
|
||||
}
|
||||
|
||||
const editorId = editor.getId();
|
||||
|
||||
// Create an ID for this item
|
||||
const itemId = generateUuid();
|
||||
|
||||
// Record the info
|
||||
this.itemInfoById[itemId] = {
|
||||
editorId,
|
||||
uriFsPath: uri.fsPath,
|
||||
fn,
|
||||
};
|
||||
|
||||
// Add to the editor's known items
|
||||
if (!this.itemIdsByEditorId[editorId]) {
|
||||
this.itemIdsByEditorId[editorId] = new Set();
|
||||
}
|
||||
this.itemIdsByEditorId[editorId].add(itemId);
|
||||
|
||||
// If the editor's current URI matches, mount it now
|
||||
if (editor.getModel()?.uri.fsPath === uri.fsPath) {
|
||||
this._mountItemOnEditor(editor, itemId);
|
||||
}
|
||||
|
||||
return itemId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Public API: Removes an item from the *specific* editor. We look up which editor
|
||||
* had this item and remove it from that editor.
|
||||
*/
|
||||
removeFromEditor(itemId: string): void {
|
||||
const info = this.itemInfoById[itemId];
|
||||
if (!info) {
|
||||
// Nothing to remove
|
||||
return;
|
||||
}
|
||||
|
||||
const { editorId } = info;
|
||||
|
||||
// Find the editor in question
|
||||
const editor = this._editorService.listCodeEditors().find(
|
||||
(ed) => ed.getId() === editorId
|
||||
);
|
||||
if (editor) {
|
||||
// Dispose on that editor
|
||||
this._removeItemFromEditor(editor, itemId);
|
||||
}
|
||||
|
||||
// Clean up references
|
||||
this.itemIdsByEditorId[editorId]?.delete(itemId);
|
||||
delete this.itemInfoById[itemId];
|
||||
}
|
||||
}
|
||||
|
||||
registerSingleton(IConsistentEditorItemService, ConsistentEditorItemService, InstantiationType.Eager);
|
||||
|
||||
|
||||
@@ -0,0 +1,170 @@
|
||||
|
||||
// eg "bash" -> "shell"
|
||||
export const nameToVscodeLanguage: { [key: string]: string } = {
|
||||
// Web Technologies
|
||||
'html': 'html',
|
||||
'css': 'css',
|
||||
'scss': 'scss',
|
||||
'sass': 'scss',
|
||||
'less': 'less',
|
||||
'javascript': 'typescript',
|
||||
'js': 'typescript', // use more general renderer
|
||||
'jsx': 'typescript',
|
||||
'typescript': 'typescript',
|
||||
'ts': 'typescript',
|
||||
'tsx': 'typescript',
|
||||
'json': 'json',
|
||||
'jsonc': 'json',
|
||||
|
||||
// Programming Languages
|
||||
'python': 'python',
|
||||
'py': 'python',
|
||||
'java': 'java',
|
||||
'cpp': 'cpp',
|
||||
'c++': 'cpp',
|
||||
'c': 'c',
|
||||
'csharp': 'csharp',
|
||||
'cs': 'csharp',
|
||||
'c#': 'csharp',
|
||||
'go': 'go',
|
||||
'golang': 'go',
|
||||
'rust': 'rust',
|
||||
'rs': 'rust',
|
||||
'ruby': 'ruby',
|
||||
'rb': 'ruby',
|
||||
'php': 'php',
|
||||
'shell': 'shell',
|
||||
'bash': 'shell',
|
||||
'sh': 'shell',
|
||||
'zsh': 'shell',
|
||||
|
||||
// Markup and Config
|
||||
'markdown': 'markdown',
|
||||
'md': 'markdown',
|
||||
'xml': 'xml',
|
||||
'svg': 'xml',
|
||||
'yaml': 'yaml',
|
||||
'yml': 'yaml',
|
||||
'ini': 'ini',
|
||||
'toml': 'ini',
|
||||
|
||||
// Database and Query Languages
|
||||
'sql': 'sql',
|
||||
'mysql': 'sql',
|
||||
'postgresql': 'sql',
|
||||
'graphql': 'graphql',
|
||||
'gql': 'graphql',
|
||||
|
||||
// Others
|
||||
'dockerfile': 'dockerfile',
|
||||
'docker': 'dockerfile',
|
||||
'makefile': 'makefile',
|
||||
'plaintext': 'plaintext',
|
||||
'text': 'plaintext'
|
||||
};
|
||||
|
||||
|
||||
|
||||
// eg ".ts" -> "typescript"
|
||||
const fileExtensionToVscodeLanguage: { [key: string]: string } = {
|
||||
// Web
|
||||
'html': 'html',
|
||||
'htm': 'html',
|
||||
'css': 'css',
|
||||
'scss': 'scss',
|
||||
'less': 'less',
|
||||
'js': 'javascript',
|
||||
'jsx': 'javascript',
|
||||
'ts': 'typescript',
|
||||
'tsx': 'typescript',
|
||||
'json': 'json',
|
||||
'jsonc': 'json',
|
||||
|
||||
// Programming Languages
|
||||
'py': 'python',
|
||||
'java': 'java',
|
||||
'cpp': 'cpp',
|
||||
'cc': 'cpp',
|
||||
'c': 'c',
|
||||
'h': 'cpp',
|
||||
'hpp': 'cpp',
|
||||
'cs': 'csharp',
|
||||
'go': 'go',
|
||||
'rs': 'rust',
|
||||
'rb': 'ruby',
|
||||
'php': 'php',
|
||||
'sh': 'shell',
|
||||
'bash': 'shell',
|
||||
'zsh': 'shell',
|
||||
|
||||
// Markup/Config
|
||||
'md': 'markdown',
|
||||
'markdown': 'markdown',
|
||||
'xml': 'xml',
|
||||
'svg': 'xml',
|
||||
'yaml': 'yaml',
|
||||
'yml': 'yaml',
|
||||
'ini': 'ini',
|
||||
'toml': 'ini',
|
||||
|
||||
// Other
|
||||
'sql': 'sql',
|
||||
'graphql': 'graphql',
|
||||
'gql': 'graphql',
|
||||
'dockerfile': 'dockerfile',
|
||||
'docker': 'dockerfile',
|
||||
'mk': 'makefile',
|
||||
|
||||
// Config Files and Dot Files
|
||||
'npmrc': 'ini',
|
||||
'env': 'ini',
|
||||
'gitignore': 'ignore',
|
||||
'dockerignore': 'ignore',
|
||||
'eslintrc': 'json',
|
||||
'babelrc': 'json',
|
||||
'prettierrc': 'json',
|
||||
'stylelintrc': 'json',
|
||||
'editorconfig': 'ini',
|
||||
'htaccess': 'apacheconf',
|
||||
'conf': 'ini',
|
||||
'config': 'ini',
|
||||
|
||||
// Package Files
|
||||
'package': 'json',
|
||||
'package-lock': 'json',
|
||||
'gemfile': 'ruby',
|
||||
'podfile': 'ruby',
|
||||
'rakefile': 'ruby',
|
||||
|
||||
// Build Systems
|
||||
'cmake': 'cmake',
|
||||
'makefile': 'makefile',
|
||||
'gradle': 'groovy',
|
||||
|
||||
// Shell Scripts
|
||||
'bashrc': 'shell',
|
||||
'zshrc': 'shell',
|
||||
'fish': 'shell',
|
||||
|
||||
// Version Control
|
||||
'gitconfig': 'ini',
|
||||
'hgrc': 'ini',
|
||||
'svnconfig': 'ini',
|
||||
|
||||
// Web Server
|
||||
'nginx': 'nginx',
|
||||
|
||||
// Misc Config
|
||||
'properties': 'properties',
|
||||
'cfg': 'ini',
|
||||
'reg': 'ini'
|
||||
};
|
||||
|
||||
|
||||
export function filenameToVscodeLanguage(filename: string): string | undefined {
|
||||
|
||||
const ext = filename.toLowerCase().split('.').pop();
|
||||
if (!ext) return undefined;
|
||||
|
||||
return fileExtensionToVscodeLanguage[ext];
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
/*--------------------------------------------------------------------------------------
|
||||
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
class SurroundingsRemover {
|
||||
readonly originalS: string
|
||||
i: number
|
||||
j: number
|
||||
|
||||
// string is s[i...j]
|
||||
|
||||
constructor(s: string) {
|
||||
this.originalS = s
|
||||
this.i = 0
|
||||
this.j = s.length - 1
|
||||
}
|
||||
value() {
|
||||
return this.originalS.substring(this.i, this.j + 1)
|
||||
}
|
||||
|
||||
// returns whether it removed the whole prefix
|
||||
removePrefix = (prefix: string): boolean => {
|
||||
let offset = 0
|
||||
// console.log('prefix', prefix, Math.min(this.j, prefix.length - 1))
|
||||
while (this.i <= this.j && offset <= prefix.length - 1) {
|
||||
if (this.originalS.charAt(this.i) !== prefix.charAt(offset))
|
||||
break
|
||||
offset += 1
|
||||
this.i += 1
|
||||
}
|
||||
return offset === prefix.length
|
||||
}
|
||||
|
||||
// // removes suffix from right to left
|
||||
removeSuffix = (suffix: string): boolean => {
|
||||
// e.g. suffix = <PRE/>, the string is <PRE>hi<P
|
||||
const s = this.value()
|
||||
// for every possible prefix of `suffix`, check if string ends with it
|
||||
for (let len = Math.min(s.length, suffix.length); len >= 1; len -= 1) {
|
||||
if (s.endsWith(suffix.substring(0, len))) { // the end of the string equals a prefix
|
||||
this.j -= len
|
||||
return len === suffix.length
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
// removeSuffix = (suffix: string): boolean => {
|
||||
// let offset = 0
|
||||
|
||||
// while (this.j >= Math.max(this.i, 0)) {
|
||||
// if (this.originalS.charAt(this.j) !== suffix.charAt(suffix.length - 1 - offset))
|
||||
// break
|
||||
// offset += 1
|
||||
// this.j -= 1
|
||||
// }
|
||||
// return offset === suffix.length
|
||||
// }
|
||||
|
||||
removeFromStartUntil = (until: string, alsoRemoveUntilStr: boolean) => {
|
||||
const index = this.originalS.indexOf(until, this.i)
|
||||
|
||||
if (index === -1) {
|
||||
this.i = this.j + 1
|
||||
return false
|
||||
}
|
||||
// console.log('index', index, until.length)
|
||||
|
||||
if (alsoRemoveUntilStr)
|
||||
this.i = index + until.length
|
||||
else
|
||||
this.i = index
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
removeCodeBlock = () => {
|
||||
// Match either:
|
||||
// 1. ```language\n<code>\n```\n?
|
||||
// 2. ```<code>\n```\n?
|
||||
|
||||
const pm = this
|
||||
const foundCodeBlock = pm.removePrefix('```')
|
||||
if (!foundCodeBlock) return false
|
||||
|
||||
pm.removeFromStartUntil('\n', true) // language
|
||||
|
||||
const j = pm.j
|
||||
let foundCodeBlockEnd = pm.removeSuffix('```')
|
||||
|
||||
if (pm.j === j) foundCodeBlockEnd = pm.removeSuffix('```\n') // if no change, try again with \n after ```
|
||||
|
||||
if (!foundCodeBlockEnd) return false
|
||||
|
||||
pm.removeSuffix('\n') // remove the newline before ```
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
deltaInfo = (recentlyAddedTextLen: number) => {
|
||||
// aaaaaatextaaaaaa{recentlyAdded}
|
||||
// ^ i j len
|
||||
// |
|
||||
// recentyAddedIdx
|
||||
const recentlyAddedIdx = this.originalS.length - recentlyAddedTextLen
|
||||
const actualDelta = this.originalS.substring(Math.max(this.i, recentlyAddedIdx), this.j + 1)
|
||||
const ignoredSuffix = this.originalS.substring(Math.max(this.j + 1, recentlyAddedIdx), Infinity)
|
||||
return [actualDelta, ignoredSuffix] as const
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
export const extractCodeFromRegular = ({ text, recentlyAddedTextLen }: { text: string, recentlyAddedTextLen: number }): [string, string, string] => {
|
||||
|
||||
const pm = new SurroundingsRemover(text)
|
||||
|
||||
pm.removeCodeBlock()
|
||||
|
||||
const s = pm.value()
|
||||
const [delta, ignoredSuffix] = pm.deltaInfo(recentlyAddedTextLen)
|
||||
|
||||
return [s, delta, ignoredSuffix]
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// Ollama has its own FIM, we should not use this if we use that
|
||||
export const extractCodeFromFIM = ({ text, recentlyAddedTextLen, midTag, }: { text: string, recentlyAddedTextLen: number, midTag: string }): [string, string, string] => {
|
||||
|
||||
/* ------------- summary of the regex -------------
|
||||
[optional ` | `` | ```]
|
||||
(match optional_language_name)
|
||||
[optional strings here]
|
||||
[required <MID> tag]
|
||||
(match the stuff between mid tags)
|
||||
[optional <MID/> tag]
|
||||
[optional ` | `` | ```]
|
||||
*/
|
||||
|
||||
const pm = new SurroundingsRemover(text)
|
||||
|
||||
pm.removeCodeBlock()
|
||||
|
||||
const foundMid = pm.removePrefix(`<${midTag}>`)
|
||||
|
||||
if (foundMid) {
|
||||
pm.removeSuffix(`</${midTag}>`)
|
||||
}
|
||||
const s = pm.value()
|
||||
const [delta, ignoredSuffix] = pm.deltaInfo(recentlyAddedTextLen)
|
||||
|
||||
return [s, delta, ignoredSuffix]
|
||||
|
||||
|
||||
// // const regex = /[\s\S]*?(?:`{1,3}\s*([a-zA-Z_]+[\w]*)?[\s\S]*?)?<MID>([\s\S]*?)(?:<\/MID>|`{1,3}|$)/;
|
||||
// const regex = new RegExp(
|
||||
// `[\\s\\S]*?(?:\`{1,3}\\s*([a-zA-Z_]+[\\w]*)?[\\s\\S]*?)?<${midTag}>([\\s\\S]*?)(?:</${midTag}>|\`{1,3}|$)`,
|
||||
// ''
|
||||
// );
|
||||
// const match = text.match(regex);
|
||||
// if (match) {
|
||||
// const [_, languageName, codeBetweenMidTags] = match;
|
||||
// return [languageName, codeBetweenMidTags] as const
|
||||
|
||||
// } else {
|
||||
// return [undefined, extractCodeFromRegular(text)] as const
|
||||
// }
|
||||
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Glass Devtools, Inc. All rights reserved.
|
||||
* Void Editor additions licensed under the AGPLv3 License.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
/*--------------------------------------------------------------------------------------
|
||||
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import { diffLines } from './react/out/util/diffLines.js'
|
||||
import { diffLines } from '../react/out/diff/index.js'
|
||||
|
||||
export type ComputedDiff = {
|
||||
type: 'edit';
|
||||
@@ -0,0 +1,44 @@
|
||||
import { URI } from '../../../../../base/common/uri'
|
||||
import { EndOfLinePreference } from '../../../../../editor/common/model'
|
||||
import { IModelService } from '../../../../../editor/common/services/model.js'
|
||||
import { IFileService } from '../../../../../platform/files/common/files'
|
||||
|
||||
|
||||
// attempts to read URI of currently opened model, then of raw file
|
||||
export const VSReadFile = async (modelService: IModelService, fileService: IFileService, uri: URI) => {
|
||||
|
||||
const modelResult = await _VSReadModel(modelService, uri)
|
||||
if (modelResult) return modelResult
|
||||
|
||||
const fileResult = await _VSReadFileRaw(fileService, uri)
|
||||
if (fileResult) return fileResult
|
||||
|
||||
return ''
|
||||
|
||||
}
|
||||
|
||||
// read files from VSCode. preferred (but appears to only work if the model of this URI already exists. If it doesn't use the other function.)
|
||||
export const _VSReadModel = async (modelService: IModelService, uri: URI): Promise<string | null> => {
|
||||
|
||||
// attempt to read saved model (sometimes doesn't work if page is reloaded)
|
||||
const model = modelService.getModel(uri)
|
||||
if (model) {
|
||||
return model.getValue(EndOfLinePreference.LF)
|
||||
}
|
||||
|
||||
// look at all opened models and check if they have the same `fsPath`
|
||||
const models = modelService.getModels();
|
||||
for (const model of models) {
|
||||
if (model.uri.fsPath.toString() === uri.fsPath.toString()) {
|
||||
return model.getValue(EndOfLinePreference.LF);
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
export const _VSReadFileRaw = async (fileService: IFileService, uri: URI) => {
|
||||
const res = await fileService.readFile(uri)
|
||||
const str = res.value.toString()
|
||||
return str
|
||||
}
|
||||
@@ -1,4 +1,9 @@
|
||||
.monaco-editor .void-sweepIdxBG {
|
||||
/*--------------------------------------------------------------------------------------
|
||||
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
.monaco-editor .void-sweepIdxBG {
|
||||
background-color: var(--vscode-void-sweepIdxBG);
|
||||
}
|
||||
|
||||
@@ -6,6 +11,10 @@
|
||||
background-color: var(--vscode-void-sweepBG);
|
||||
}
|
||||
|
||||
.void-highlightBG {
|
||||
background-color: var(--vscode-void-highlightBG);
|
||||
}
|
||||
|
||||
.void-greenBG {
|
||||
background-color: var(--vscode-void-greenBG);
|
||||
}
|
||||
@@ -13,3 +22,147 @@
|
||||
.void-redBG {
|
||||
background-color: var(--vscode-void-redBG);
|
||||
}
|
||||
|
||||
.void-watermark-button {
|
||||
margin: 8px 0;
|
||||
padding: 8px 20px;
|
||||
background-color: #3b82f6;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
outline: none !important;
|
||||
box-shadow: none !important;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
.void-watermark-button:hover {
|
||||
background-color: #2563eb;
|
||||
}
|
||||
.void-watermark-button:active {
|
||||
background-color: #2563eb;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
.void-settings-watermark-button {
|
||||
margin: 8px 0;
|
||||
padding: 8px 20px;
|
||||
background-color: var(--vscode-input-background);
|
||||
color: var(--vscode-input-foreground);
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
outline: none !important;
|
||||
box-shadow: none !important;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
.void-settings-watermark-button:hover {
|
||||
filter: brightness(1.1);
|
||||
}
|
||||
.void-settings-watermark-button:active {
|
||||
filter: brightness(1.1);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
.void-link {
|
||||
color: #3b82f6;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
.void-link:hover {
|
||||
opacity: 80%;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
.void-scrollable-element::-webkit-scrollbar,
|
||||
.void-scrollable-element *::-webkit-scrollbar {
|
||||
width: 14px !important;
|
||||
height: 14px !important;
|
||||
}
|
||||
|
||||
.void-scrollable-element::-webkit-scrollbar-track,
|
||||
.void-scrollable-element *::-webkit-scrollbar-track {
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
.void-scrollable-element::-webkit-scrollbar-thumb,
|
||||
.void-scrollable-element *::-webkit-scrollbar-thumb {
|
||||
background-color: transparent !important;
|
||||
border-radius: 0px !important;
|
||||
}
|
||||
|
||||
.void-scrollable-element::-webkit-scrollbar-thumb:hover,
|
||||
.void-scrollable-element *::-webkit-scrollbar-thumb:hover {
|
||||
background-color: var(--vscode-scrollbarSlider-hoverBackground) !important;
|
||||
}
|
||||
|
||||
.void-scrollable-element::-webkit-scrollbar-thumb:active,
|
||||
.void-scrollable-element *::-webkit-scrollbar-thumb:active {
|
||||
background-color: var(--vscode-scrollbarSlider-activeBackground) !important;
|
||||
}
|
||||
|
||||
.void-scrollable-element::-webkit-scrollbar-corner,
|
||||
.void-scrollable-element *::-webkit-scrollbar-corner {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
.void-scrollable-element.show-scrollbar-0::-webkit-scrollbar-thumb,
|
||||
.void-scrollable-element.show-scrollbar-0 *::-webkit-scrollbar-thumb {
|
||||
background-color: color-mix(in srgb, var(--vscode-scrollbarSlider-background) 0%, transparent) !important;
|
||||
}
|
||||
|
||||
.void-scrollable-element.show-scrollbar-1::-webkit-scrollbar-thumb,
|
||||
.void-scrollable-element.show-scrollbar-1 *::-webkit-scrollbar-thumb {
|
||||
background-color: color-mix(in srgb, var(--vscode-scrollbarSlider-background) 10%, transparent) !important;
|
||||
}
|
||||
|
||||
.void-scrollable-element.show-scrollbar-2::-webkit-scrollbar-thumb,
|
||||
.void-scrollable-element.show-scrollbar-2 *::-webkit-scrollbar-thumb {
|
||||
background-color: color-mix(in srgb, var(--vscode-scrollbarSlider-background) 20%, transparent) !important;
|
||||
}
|
||||
|
||||
.void-scrollable-element.show-scrollbar-3::-webkit-scrollbar-thumb,
|
||||
.void-scrollable-element.show-scrollbar-3 *::-webkit-scrollbar-thumb {
|
||||
background-color: color-mix(in srgb, var(--vscode-scrollbarSlider-background) 30%, transparent) !important;
|
||||
}
|
||||
|
||||
.void-scrollable-element.show-scrollbar-4::-webkit-scrollbar-thumb,
|
||||
.void-scrollable-element.show-scrollbar-4 *::-webkit-scrollbar-thumb {
|
||||
background-color: color-mix(in srgb, var(--vscode-scrollbarSlider-background) 40%, transparent) !important;
|
||||
}
|
||||
|
||||
.void-scrollable-element.show-scrollbar-5::-webkit-scrollbar-thumb,
|
||||
.void-scrollable-element.show-scrollbar-5 *::-webkit-scrollbar-thumb {
|
||||
background-color: color-mix(in srgb, var(--vscode-scrollbarSlider-background) 50%, transparent) !important;
|
||||
}
|
||||
|
||||
.void-scrollable-element.show-scrollbar-6::-webkit-scrollbar-thumb,
|
||||
.void-scrollable-element.show-scrollbar-6 *::-webkit-scrollbar-thumb {
|
||||
background-color: color-mix(in srgb, var(--vscode-scrollbarSlider-background) 60%, transparent) !important;
|
||||
}
|
||||
|
||||
.void-scrollable-element.show-scrollbar-7::-webkit-scrollbar-thumb,
|
||||
.void-scrollable-element.show-scrollbar-7 *::-webkit-scrollbar-thumb {
|
||||
background-color: color-mix(in srgb, var(--vscode-scrollbarSlider-background) 70%, transparent) !important;
|
||||
}
|
||||
|
||||
.void-scrollable-element.show-scrollbar-8::-webkit-scrollbar-thumb,
|
||||
.void-scrollable-element.show-scrollbar-8 *::-webkit-scrollbar-thumb {
|
||||
background-color: color-mix(in srgb, var(--vscode-scrollbarSlider-background) 80%, transparent) !important;
|
||||
}
|
||||
|
||||
.void-scrollable-element.show-scrollbar-9::-webkit-scrollbar-thumb,
|
||||
.void-scrollable-element.show-scrollbar-9 *::-webkit-scrollbar-thumb {
|
||||
background-color: color-mix(in srgb, var(--vscode-scrollbarSlider-background) 90%, transparent) !important;
|
||||
}
|
||||
|
||||
.void-scrollable-element.show-scrollbar-10::-webkit-scrollbar-thumb,
|
||||
.void-scrollable-element.show-scrollbar-10 *::-webkit-scrollbar-thumb {
|
||||
background-color: var(--vscode-scrollbarSlider-background) !important;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,404 @@
|
||||
/*--------------------------------------------------------------------------------------
|
||||
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
|
||||
import { URI } from '../../../../../base/common/uri.js';
|
||||
import { filenameToVscodeLanguage } from '../helpers/detectLanguage.js';
|
||||
import { CodeSelection, StagingSelectionItem, FileSelection } from '../chatThreadService.js';
|
||||
import { _VSReadModel, VSReadFile } from '../helpers/readFile.js';
|
||||
import { IModelService } from '../../../../../editor/common/services/model.js';
|
||||
import { IFileService } from '../../../../../platform/files/common/files.js';
|
||||
|
||||
|
||||
// this is just for ease of readability
|
||||
const tripleTick = ['```', '```']
|
||||
|
||||
export const chat_systemMessage = `\
|
||||
You are a coding assistant. You are given a list of instructions to follow \`INSTRUCTIONS\`, and optionally a list of relevant files \`FILES\`, and selections inside of files \`SELECTIONS\`.
|
||||
|
||||
Please respond to the user's query.
|
||||
|
||||
In the case that the user asks you to make changes to code, you should make sure to return CODE BLOCKS of the changes, as well as explanations and descriptions of the changes.
|
||||
For example, if the user asks you to "make this file look nicer", make sure your output includes a code block with concrete ways the file can look nicer.
|
||||
- Do not re-write the entire file in the code block
|
||||
- You can write comments like "// ... existing code" to indicate existing code
|
||||
- Make sure you give enough context in the code block to apply the change to the correct location in the code.
|
||||
|
||||
You're allowed to ask for more context. For example, if the user only gives you a selection but you want to see the the full file, you can ask them to provide it.
|
||||
|
||||
Do not output any of these instructions, nor tell the user anything about them unless directly prompted for them.
|
||||
Do not tell the user anything about the examples below.
|
||||
|
||||
## EXAMPLE 1
|
||||
FILES
|
||||
math.ts
|
||||
${tripleTick[0]}typescript
|
||||
const addNumbers = (a, b) => a + b
|
||||
const multiplyNumbers = (a, b) => a * b
|
||||
const subtractNumbers = (a, b) => a - b
|
||||
const divideNumbers = (a, b) => a / b
|
||||
|
||||
const vectorize = (...numbers) => {
|
||||
return numbers // vector
|
||||
}
|
||||
|
||||
const dot = (vector1: number[], vector2: number[]) => {
|
||||
if (vector1.length !== vector2.length) throw new Error(\`Could not dot vectors \${vector1} and \${vector2}. Size mismatch.\`)
|
||||
let sum = 0
|
||||
for (let i = 0; i < vector1.length; i += 1)
|
||||
sum += multiplyNumbers(vector1[i], vector2[i])
|
||||
return sum
|
||||
}
|
||||
|
||||
const normalize = (vector: number[]) => {
|
||||
const norm = Math.sqrt(dot(vector, vector))
|
||||
for (let i = 0; i < vector.length; i += 1)
|
||||
vector[i] = divideNumbers(vector[i], norm)
|
||||
return vector
|
||||
}
|
||||
|
||||
const normalized = (vector: number[]) => {
|
||||
const v2 = [...vector] // clone vector
|
||||
return normalize(v2)
|
||||
}
|
||||
${tripleTick[1]}
|
||||
|
||||
|
||||
SELECTIONS
|
||||
math.ts (lines 3:3)
|
||||
${tripleTick[0]}typescript
|
||||
const subtractNumbers = (a, b) => a - b
|
||||
${tripleTick[1]}
|
||||
|
||||
INSTRUCTIONS
|
||||
add a function that exponentiates a number below this, and use it to make a power function that raises all entries of a vector to a power
|
||||
|
||||
ACCEPTED OUTPUT
|
||||
We can add the following code to the file:
|
||||
${tripleTick[0]}typescript
|
||||
// existing code...
|
||||
const subtractNumbers = (a, b) => a - b
|
||||
const exponentiateNumbers = (a, b) => Math.pow(a, b)
|
||||
const divideNumbers = (a, b) => a / b
|
||||
// existing code...
|
||||
|
||||
const raiseAll = (vector: number[], power: number) => {
|
||||
for (let i = 0; i < vector.length; i += 1)
|
||||
vector[i] = exponentiateNumbers(vector[i], power)
|
||||
return vector
|
||||
}
|
||||
${tripleTick[1]}
|
||||
|
||||
|
||||
## EXAMPLE 2
|
||||
FILES
|
||||
fib.ts
|
||||
${tripleTick[0]}typescript
|
||||
|
||||
const dfs = (root) => {
|
||||
if (!root) return;
|
||||
console.log(root.val);
|
||||
dfs(root.left);
|
||||
dfs(root.right);
|
||||
}
|
||||
const fib = (n) => {
|
||||
if (n < 1) return 1
|
||||
return fib(n - 1) + fib(n - 2)
|
||||
}
|
||||
${tripleTick[1]}
|
||||
|
||||
SELECTIONS
|
||||
fib.ts (lines 10:10)
|
||||
${tripleTick[0]}typescript
|
||||
return fib(n - 1) + fib(n - 2)
|
||||
${tripleTick[1]}
|
||||
|
||||
INSTRUCTIONS
|
||||
memoize results
|
||||
|
||||
ACCEPTED OUTPUT
|
||||
To implement memoization in your Fibonacci function, you can use a JavaScript object to store previously computed results. This will help avoid redundant calculations and improve performance. Here's how you can modify your function:
|
||||
${tripleTick[0]}typescript
|
||||
// existing code...
|
||||
const fib = (n, memo = {}) => {
|
||||
if (n < 1) return 1;
|
||||
if (memo[n]) return memo[n]; // Check if result is already computed
|
||||
memo[n] = fib(n - 1, memo) + fib(n - 2, memo); // Store result in memo
|
||||
return memo[n];
|
||||
}
|
||||
${tripleTick[1]}
|
||||
Explanation:
|
||||
Memoization Object: A memo object is used to store the results of Fibonacci calculations for each n.
|
||||
Check Memo: Before computing fib(n), the function checks if the result is already in memo. If it is, it returns the stored result.
|
||||
Store Result: After computing fib(n), the result is stored in memo for future reference.
|
||||
|
||||
## END EXAMPLES\
|
||||
`
|
||||
|
||||
|
||||
type FileSelnLocal = { fileURI: URI, content: string }
|
||||
const stringifyFileSelection = ({ fileURI, content }: FileSelnLocal) => {
|
||||
return `\
|
||||
${fileURI.fsPath}
|
||||
${tripleTick[0]}${filenameToVscodeLanguage(fileURI.fsPath) ?? ''}
|
||||
${content}
|
||||
${tripleTick[1]}
|
||||
`
|
||||
}
|
||||
const stringifyCodeSelection = ({ fileURI, selectionStr, range }: CodeSelection) => {
|
||||
return `\
|
||||
${fileURI.fsPath} (lines ${range.startLineNumber}:${range.endLineNumber})
|
||||
${tripleTick[0]}${filenameToVscodeLanguage(fileURI.fsPath) ?? ''}
|
||||
${selectionStr}
|
||||
${tripleTick[1]}
|
||||
`
|
||||
}
|
||||
|
||||
const failToReadStr = 'Could not read content. This file may have been deleted. If you expected content here, you can tell the user about this as they might not know.'
|
||||
const stringifyFileSelections = async (fileSelections: FileSelection[], modelService: IModelService, fileService: IFileService) => {
|
||||
if (fileSelections.length === 0) return null
|
||||
const fileSlns: FileSelnLocal[] = await Promise.all(fileSelections.map(async (sel) => {
|
||||
const content = await VSReadFile(modelService, fileService, sel.fileURI) ?? failToReadStr
|
||||
return { ...sel, content }
|
||||
}))
|
||||
return fileSlns.map(sel => stringifyFileSelection(sel)).join('\n')
|
||||
}
|
||||
const stringifyCodeSelections = (codeSelections: CodeSelection[]) => {
|
||||
return codeSelections.map(sel => stringifyCodeSelection(sel)).join('\n')
|
||||
}
|
||||
const stringifySelectionNames = (currSelns: StagingSelectionItem[] | null): string => {
|
||||
if (!currSelns) return ''
|
||||
return currSelns.map(s => `${s.fileURI.fsPath}${s.range ? ` (lines ${s.range.startLineNumber}:${s.range.endLineNumber})` : ''}`).join('\n')
|
||||
}
|
||||
|
||||
export const chat_userMessageContent = async (instructions: string, prevSelns: StagingSelectionItem[] | null, currSelns: StagingSelectionItem[] | null) => {
|
||||
|
||||
const selnsStr = stringifySelectionNames(currSelns)
|
||||
|
||||
let str = ''
|
||||
if (selnsStr) { str += `SELECTIONS\n${selnsStr}\n` }
|
||||
str += `\nINSTRUCTIONS\n${instructions}`
|
||||
return str;
|
||||
};
|
||||
|
||||
export const chat_userMessageContentWithAllFilesToo = async (instructions: string, prevSelns: StagingSelectionItem[] | null, currSelns: StagingSelectionItem[] | null, modelService: IModelService, fileService: IFileService) => {
|
||||
|
||||
// ADD IN FILES AT TOP
|
||||
const allSelections = [...currSelns || [], ...prevSelns || []]
|
||||
|
||||
const codeSelections: CodeSelection[] = []
|
||||
const fileSelections: FileSelection[] = []
|
||||
const filesURIs = new Set<string>()
|
||||
|
||||
for (const selection of allSelections) {
|
||||
if (selection.type === 'Selection') {
|
||||
codeSelections.push(selection)
|
||||
}
|
||||
else if (selection.type === 'File') {
|
||||
const fileSelection = selection
|
||||
const path = fileSelection.fileURI.fsPath
|
||||
if (!filesURIs.has(path)) {
|
||||
filesURIs.add(path)
|
||||
fileSelections.push(fileSelection)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const filesStr = await stringifyFileSelections(fileSelections, modelService, fileService)
|
||||
const selnsStr = stringifyCodeSelections(codeSelections)
|
||||
|
||||
// ACTUAL MESSAGE CONTENT
|
||||
const messageContent = await chat_userMessageContent(instructions, prevSelns, currSelns)
|
||||
|
||||
|
||||
let str = ''
|
||||
|
||||
str += 'ALL FILE CONTENTS\n'
|
||||
if (filesStr) str += `${filesStr}\n`
|
||||
if (selnsStr) str += `${selnsStr}\n`
|
||||
if (messageContent) str += `\n${messageContent}\n`
|
||||
|
||||
return str;
|
||||
};
|
||||
|
||||
|
||||
|
||||
export const fastApply_rewritewholething_systemMessage = `\
|
||||
You are a coding assistant that re-writes an entire file to make a change. You are given the original file \`ORIGINAL_FILE\` and a change \`CHANGE\`.
|
||||
|
||||
Directions:
|
||||
1. Please rewrite the original file \`ORIGINAL_FILE\`, making the change \`CHANGE\`. You must completely re-write the whole file.
|
||||
2. Keep all of the original comments, spaces, newlines, and other details whenever possible.
|
||||
3. ONLY output the full new file. Do not add any other explanations or text.
|
||||
`
|
||||
|
||||
|
||||
|
||||
|
||||
export const fastApply_rewritewholething_userMessage = ({ originalCode, applyStr, uri }: { originalCode: string, applyStr: string, uri: URI }) => {
|
||||
|
||||
const language = filenameToVscodeLanguage(uri.fsPath) ?? ''
|
||||
|
||||
return `\
|
||||
ORIGINAL_FILE
|
||||
${tripleTick[0]}${language}
|
||||
${originalCode}
|
||||
${tripleTick[1]}
|
||||
|
||||
CHANGE
|
||||
${tripleTick[0]}
|
||||
${applyStr}
|
||||
${tripleTick[1]}
|
||||
|
||||
INSTRUCTIONS
|
||||
Please finish writing the new file by applying the change to the original file. Return ONLY the completion of the file, without any explanation.
|
||||
`
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
export const fastApply_searchreplace_systemMessage = `\
|
||||
You are a coding assistant that re-writes an entire file to make a change. You are given the original file \`ORIGINAL_FILE\` and a change \`CHANGE\`.
|
||||
|
||||
Directions:
|
||||
1. Please rewrite the original file \`ORIGINAL_FILE\`, making the change \`CHANGE\`. You must completely re-write the whole file.
|
||||
2. Keep all of the original comments, spaces, newlines, and other details whenever possible.
|
||||
3. ONLY output the full new file. Do not add any other explanations or text.
|
||||
`
|
||||
|
||||
|
||||
export const fastApply_searchreplace_userMessage = ({ originalCode, applyStr, uri }: { originalCode: string, applyStr: string, uri: URI }) => {
|
||||
|
||||
const language = filenameToVscodeLanguage(uri.fsPath) ?? ''
|
||||
|
||||
return `\
|
||||
ORIGINAL_FILE
|
||||
\`\`\`${language}
|
||||
${originalCode}
|
||||
\`\`\`
|
||||
|
||||
CHANGE
|
||||
\`\`\`
|
||||
${applyStr}
|
||||
\`\`\`
|
||||
|
||||
INSTRUCTIONS
|
||||
Please finish writing the new file by applying the change to the original file. Return ONLY the completion of the file, without any explanation.
|
||||
`
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
export const voidPrefixAndSuffix = ({ fullFileStr, startLine, endLine }: { fullFileStr: string, startLine: number, endLine: number }) => {
|
||||
|
||||
const fullFileLines = fullFileStr.split('\n')
|
||||
|
||||
// we can optimize this later
|
||||
const MAX_PREFIX_SUFFIX_CHARS = 20_000
|
||||
/*
|
||||
|
||||
a
|
||||
a
|
||||
a <-- final i (prefix = a\na\n)
|
||||
a
|
||||
|b <-- startLine-1 (middle = b\nc\nd\n) <-- initial i (moves up)
|
||||
c
|
||||
d| <-- endLine-1 <-- initial j (moves down)
|
||||
e
|
||||
e <-- final j (suffix = e\ne\n)
|
||||
e
|
||||
e
|
||||
*/
|
||||
|
||||
let prefix = ''
|
||||
let i = startLine - 1 // 0-indexed exclusive
|
||||
// we'll include fullFileLines[i...(startLine-1)-1].join('\n') in the prefix.
|
||||
while (i !== 0) {
|
||||
const newLine = fullFileLines[i - 1]
|
||||
if (newLine.length + 1 + prefix.length <= MAX_PREFIX_SUFFIX_CHARS) { // +1 to include the \n
|
||||
prefix = `${newLine}\n${prefix}`
|
||||
i -= 1
|
||||
}
|
||||
else break
|
||||
}
|
||||
|
||||
let suffix = ''
|
||||
let j = endLine - 1
|
||||
while (j !== fullFileLines.length - 1) {
|
||||
const newLine = fullFileLines[j + 1]
|
||||
if (newLine.length + 1 + suffix.length <= MAX_PREFIX_SUFFIX_CHARS) { // +1 to include the \n
|
||||
suffix = `${suffix}\n${newLine}`
|
||||
j += 1
|
||||
}
|
||||
else break
|
||||
}
|
||||
|
||||
return { prefix, suffix }
|
||||
|
||||
}
|
||||
|
||||
|
||||
export type FimTagsType = {
|
||||
preTag: string,
|
||||
sufTag: string,
|
||||
midTag: string
|
||||
}
|
||||
export const defaultFimTags: FimTagsType = {
|
||||
preTag: 'ABOVE',
|
||||
sufTag: 'BELOW',
|
||||
midTag: 'SELECTION',
|
||||
}
|
||||
|
||||
// this should probably be longer
|
||||
export const ctrlKStream_systemMessage = ({ fimTags: { preTag, midTag, sufTag } }: { fimTags: FimTagsType }) => {
|
||||
return `\
|
||||
You are a FIM (fill-in-the-middle) coding assistant. Your task is to fill in the middle SELECTION marked by <${midTag}> tags.
|
||||
|
||||
The user will give you INSTRUCTIONS, as well as code that comes BEFORE the SELECTION, indicated with <${preTag}>...before</${preTag}>, and code that comes AFTER the SELECTION, indicated with <${sufTag}>...after</${sufTag}>.
|
||||
The user will also give you the existing original SELECTION that will be be replaced by the SELECTION that you output, for additional context.
|
||||
|
||||
Instructions:
|
||||
1. Your OUTPUT should be a SINGLE PIECE OF CODE of the form <${midTag}>...new_code</${midTag}>. Do NOT output any text or explanations before or after this.
|
||||
2. You may ONLY CHANGE the original SELECTION, and NOT the content in the <${preTag}>...</${preTag}> or <${sufTag}>...</${sufTag}> tags.
|
||||
3. Make sure all brackets in the new selection are balanced the same as in the original selection.
|
||||
4. Be careful not to duplicate or remove variables, comments, or other syntax by mistake.
|
||||
`
|
||||
}
|
||||
|
||||
export const ctrlKStream_userMessage = ({ selection, prefix, suffix, instructions, fimTags, isOllamaFIM, language }: {
|
||||
selection: string, prefix: string, suffix: string, instructions: string, fimTags: FimTagsType, language: string,
|
||||
isOllamaFIM: false, // we require this be false for clarity
|
||||
}) => {
|
||||
const { preTag, sufTag, midTag } = fimTags
|
||||
|
||||
// prompt the model artifically on how to do FIM
|
||||
// const preTag = 'BEFORE'
|
||||
// const sufTag = 'AFTER'
|
||||
// const midTag = 'SELECTION'
|
||||
return `\
|
||||
|
||||
CURRENT SELECTION
|
||||
${tripleTick[0]}${language}
|
||||
<${midTag}>${selection}</${midTag}>
|
||||
${tripleTick[1]}
|
||||
|
||||
INSTRUCTIONS
|
||||
${instructions}
|
||||
|
||||
<${preTag}>${prefix}</${preTag}>
|
||||
<${sufTag}>${suffix}</${sufTag}>
|
||||
|
||||
Return only the completion block of code (of the form ${tripleTick[0]}${language}
|
||||
<${midTag}>...new code</${midTag}>
|
||||
${tripleTick[1]}).`
|
||||
};
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
|
||||
import { CodeSelection } from '../registerThreads.js';
|
||||
|
||||
export const filesStr = (selections: CodeSelection[]) => {
|
||||
|
||||
return selections.map(({ fileURI, content, selectionStr }) =>
|
||||
`\
|
||||
File: ${fileURI.fsPath}
|
||||
\`\`\`
|
||||
${content}
|
||||
\`\`\`${selectionStr === null ? '' : `
|
||||
Selection: ${selectionStr}`}
|
||||
`).join('\n')
|
||||
}
|
||||
|
||||
|
||||
export const userInstructionsStr = (instructions: string, selections: CodeSelection[] | null) => {
|
||||
let str = '';
|
||||
if (selections && selections.length > 0) {
|
||||
str += filesStr(selections);
|
||||
str += `Please edit the selected code following these instructions:\n`
|
||||
}
|
||||
str += `${instructions}`;
|
||||
return str;
|
||||
};
|
||||
@@ -1,411 +0,0 @@
|
||||
|
||||
|
||||
|
||||
// // used for ctrl+l
|
||||
// const partialGenerationInstructions = ``
|
||||
|
||||
|
||||
// // used for ctrl+k, autocomplete
|
||||
// const fimInstructions = ``
|
||||
|
||||
|
||||
export const generateDiffInstructions = `
|
||||
You are a coding assistant. You are given a list of relevant files \`files\`, a selection that the user is making \`selection\`, and instructions to follow \`instructions\`.
|
||||
|
||||
Please edit the selected file following the user's instructions (or, if appropriate, answer their question instead).
|
||||
|
||||
All changes made to files must be outputted in unified diff format.
|
||||
Unified diff format instructions:
|
||||
1. Each diff must begin with \`\`\`@@ ... @@\`\`\`.
|
||||
2. Each line must start with a \`+\` or \`-\` or \` \` symbol.
|
||||
3. Make diffs more than a few lines.
|
||||
4. Make high-level diffs rather than many one-line diffs.
|
||||
|
||||
Here's an example of unified diff format:
|
||||
|
||||
\`\`\`
|
||||
@@ ... @@
|
||||
-def factorial(n):
|
||||
- if n == 0:
|
||||
- return 1
|
||||
- else:
|
||||
- return n * factorial(n-1)
|
||||
+def factorial(number):
|
||||
+ if number == 0:
|
||||
+ return 1
|
||||
+ else:
|
||||
+ return number * factorial(number-1)
|
||||
\`\`\`
|
||||
|
||||
Please create high-level diffs where you group edits together if they are near each other, like in the above example. Another way to represent the above example is to make many small line edits. However, this is less preferred, because the edits are not high-level. The edits are close together and should be grouped:
|
||||
|
||||
\`\`\`
|
||||
@@ ... @@ # This is less preferred because edits are close together and should be grouped:
|
||||
-def factorial(n):
|
||||
+def factorial(number):
|
||||
- if n == 0:
|
||||
+ if number == 0:
|
||||
return 1
|
||||
else:
|
||||
- return n * factorial(n-1)
|
||||
+ return number * factorial(number-1)
|
||||
\`\`\`
|
||||
|
||||
# Example 1:
|
||||
|
||||
FILES
|
||||
selected file \`test.ts\`:
|
||||
\`\`\`
|
||||
x = 1
|
||||
|
||||
{{selection}}
|
||||
|
||||
z = 3
|
||||
\`\`\`
|
||||
|
||||
SELECTION
|
||||
\`\`\`const y = 2\`\`\`
|
||||
|
||||
INSTRUCTIONS
|
||||
\`\`\`y = 3\`\`\`
|
||||
|
||||
EXPECTED RESULT
|
||||
|
||||
We should change the selection from \`\`\`y = 2\`\`\` to \`\`\`y = 3\`\`\`.
|
||||
\`\`\`
|
||||
@@ ... @@
|
||||
-x = 1
|
||||
-
|
||||
-y = 2
|
||||
+x = 1
|
||||
+
|
||||
+y = 3
|
||||
\`\`\`
|
||||
|
||||
# Example 2:
|
||||
|
||||
FILES
|
||||
selected file \`Sidebar.tsx\`:
|
||||
\`\`\`
|
||||
import React from 'react';
|
||||
import styles from './Sidebar.module.css';
|
||||
|
||||
interface SidebarProps {
|
||||
items: { label: string; href: string }[];
|
||||
onItemSelect?: (label: string) => void;
|
||||
onExtraButtonClick?: () => void;
|
||||
}
|
||||
|
||||
const Sidebar: React.FC<SidebarProps> = ({ items, onItemSelect, onExtraButtonClick }) => {
|
||||
return (
|
||||
<div className={styles.sidebar}>
|
||||
<ul>
|
||||
{items.map((item, index) => (
|
||||
<li key={index}>
|
||||
{{selection}}
|
||||
className={styles.sidebarButton}
|
||||
onClick={() => onItemSelect?.(item.label)}
|
||||
>
|
||||
{item.label}
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<button className={styles.extraButton} onClick={onExtraButtonClick}>
|
||||
Extra Action
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Sidebar;
|
||||
\`\`\`
|
||||
|
||||
SELECTION
|
||||
\`\`\` <button\`\`\`
|
||||
|
||||
INSTRUCTIONS
|
||||
\`\`\`make all the buttons like this into divs\`\`\`
|
||||
|
||||
EXPECTED OUTPUT
|
||||
|
||||
We should change all the buttons like the one selected into a div component. Here is the change:
|
||||
\`\`\`
|
||||
@@ ... @@
|
||||
-<div className={styles.sidebar}>
|
||||
-<ul>
|
||||
- {items.map((item, index) => (
|
||||
- <li key={index}>
|
||||
- <button
|
||||
- className={styles.sidebarButton}
|
||||
- onClick={() => onItemSelect?.(item.label)}
|
||||
- >
|
||||
- {item.label}
|
||||
- </button>
|
||||
- </li>
|
||||
- ))}
|
||||
-</ul>
|
||||
-<button className={styles.extraButton} onClick={onExtraButtonClick}>
|
||||
- Extra Action
|
||||
-</button>
|
||||
-</div>
|
||||
+<div className={styles.sidebar}>
|
||||
+<ul>
|
||||
+ {items.map((item, index) => (
|
||||
+ <li key={index}>
|
||||
+ <div
|
||||
+ className={styles.sidebarButton}
|
||||
+ onClick={() => onItemSelect?.(item.label)}
|
||||
+ >
|
||||
+ {item.label}
|
||||
+ </div>
|
||||
+ </li>
|
||||
+ ))}
|
||||
+</ul>
|
||||
+<div className={styles.extraButton} onClick={onExtraButtonClick}>
|
||||
+ Extra Action
|
||||
+</div>
|
||||
+</div>
|
||||
\`\`\`
|
||||
`;
|
||||
|
||||
|
||||
export const searchDiffChunkInstructions = `
|
||||
You are a coding assistant that applies a diff to a file. You are given a diff \`diff\`, a list of files \`files\` to apply the diff to, and a selection \`selection\` that you are currently considering in the file.
|
||||
|
||||
Determine whether you should modify ANY PART of the selection \`selection\` following the \`diff\`. Return \`true\` if you should modify any part of the selection, and \`false\` if you should not modify any part of it.
|
||||
|
||||
# Example 1:
|
||||
|
||||
FILES
|
||||
selected file \`Sidebar.tsx\`:
|
||||
\`\`\`
|
||||
import React from 'react';
|
||||
import styles from './Sidebar.module.css';
|
||||
|
||||
interface SidebarProps {
|
||||
items: { label: string; href: string }[];
|
||||
onItemSelect?: (label: string) => void;
|
||||
onExtraButtonClick?: () => void;
|
||||
}
|
||||
|
||||
const Sidebar: React.FC<SidebarProps> = ({ items, onItemSelect, onExtraButtonClick }) => {
|
||||
return (
|
||||
<div className={styles.sidebar}>
|
||||
<ul>
|
||||
{items.map((item, index) => (
|
||||
<li key={index}>
|
||||
<button
|
||||
className={styles.sidebarButton}
|
||||
onClick={() => onItemSelect?.(item.label)}
|
||||
>
|
||||
{item.label}
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<button className={styles.extraButton} onClick={onExtraButtonClick}>
|
||||
Extra Action
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Sidebar;
|
||||
\`\`\`
|
||||
|
||||
DIFF
|
||||
\`\`\`
|
||||
@@ ... @@
|
||||
-<div className={styles.sidebar}>
|
||||
-<ul>
|
||||
- {items.map((item, index) => (
|
||||
- <li key={index}>
|
||||
- <button
|
||||
- className={styles.sidebarButton}
|
||||
- onClick={() => onItemSelect?.(item.label)}
|
||||
- >
|
||||
- {item.label}
|
||||
- </button>
|
||||
- </li>
|
||||
- ))}
|
||||
-</ul>
|
||||
-<button className={styles.extraButton} onClick={onExtraButtonClick}>
|
||||
- Extra Action
|
||||
-</button>
|
||||
-</div>
|
||||
+<div className={styles.sidebar}>
|
||||
+<ul>
|
||||
+ {items.map((item, index) => (
|
||||
+ <li key={index}>
|
||||
+ <div
|
||||
+ className={styles.sidebarButton}
|
||||
+ onClick={() => onItemSelect?.(item.label)}
|
||||
+ >
|
||||
+ {item.label}
|
||||
+ </div>
|
||||
+ </li>
|
||||
+ ))}
|
||||
+</ul>
|
||||
+<div className={styles.extraButton} onClick={onExtraButtonClick}>
|
||||
+ Extra Action
|
||||
+</div>
|
||||
+</div>
|
||||
\`\`\`
|
||||
|
||||
SELECTION
|
||||
\`\`\`
|
||||
import React from 'react';
|
||||
import styles from './Sidebar.module.css';
|
||||
|
||||
interface SidebarProps {
|
||||
items: { label: string; href: string }[];
|
||||
onItemSelect?: (label: string) => void;
|
||||
onExtraButtonClick?: () => void;
|
||||
}
|
||||
|
||||
const Sidebar: React.FC<SidebarProps> = ({ items, onItemSelect, onExtraButtonClick }) => {
|
||||
return (
|
||||
<div className={styles.sidebar}>
|
||||
<ul>
|
||||
{items.map((item, index) => (
|
||||
\`\`\`
|
||||
|
||||
RESULT
|
||||
The output should be \`true\` because the diff begins on the line with \`<div className={styles.sidebar}>\` and this line is present in the selection.
|
||||
|
||||
OUTPUT
|
||||
\`true\`
|
||||
`
|
||||
|
||||
|
||||
export const writeFileWithDiffInstructions = `
|
||||
You are a coding assistant that applies a diff to a file. You are given the original file \`original_file\`, a diff \`diff\`, and a new file that you are applying the diff to \`new_file\`.
|
||||
|
||||
Please finish writing the new file \`new_file\`, according to the diff \`diff\`. You must completely re-write the whole file, using the diff.
|
||||
|
||||
Directions:
|
||||
1. Continue exactly where the new file \`new_file\` left off.
|
||||
2. Keep all of the original comments, spaces, newlines, and other details whenever possible.
|
||||
3. Note that \`+\` lines represent additions, \`-\` lines represent removals, and space lines \` \` represent no change.
|
||||
|
||||
# Example 1:
|
||||
|
||||
ORIGINAL_FILE
|
||||
\`Sidebar.tsx\`:
|
||||
\`\`\`
|
||||
import React from 'react';
|
||||
import styles from './Sidebar.module.css';
|
||||
|
||||
interface SidebarProps {
|
||||
items: { label: string; href: string }[];
|
||||
onItemSelect?: (label: string) => void;
|
||||
onExtraButtonClick?: () => void;
|
||||
}
|
||||
|
||||
const Sidebar: React.FC<SidebarProps> = ({ items, onItemSelect, onExtraButtonClick }) => {
|
||||
return (
|
||||
<div className={styles.sidebar}>
|
||||
<ul>
|
||||
{items.map((item, index) => (
|
||||
<li key={index}>
|
||||
<button
|
||||
className={styles.sidebarButton}
|
||||
onClick={() => onItemSelect?.(item.label)}
|
||||
>
|
||||
{item.label}
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<button className={styles.extraButton} onClick={onExtraButtonClick}>
|
||||
Extra Action
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Sidebar;
|
||||
\`\`\`
|
||||
|
||||
DIFF
|
||||
\`\`\`
|
||||
@@ ... @@
|
||||
-<div className={styles.sidebar}>
|
||||
-<ul>
|
||||
- {items.map((item, index) => (
|
||||
- <li key={index}>
|
||||
- <button
|
||||
- className={styles.sidebarButton}
|
||||
- onClick={() => onItemSelect?.(item.label)}
|
||||
- >
|
||||
- {item.label}
|
||||
- </button>
|
||||
- </li>
|
||||
- ))}
|
||||
-</ul>
|
||||
-<button className={styles.extraButton} onClick={onExtraButtonClick}>
|
||||
- Extra Action
|
||||
-</button>
|
||||
-</div>
|
||||
+<div className={styles.sidebar}>
|
||||
+<ul>
|
||||
+ {items.map((item, index) => (
|
||||
+ <li key={index}>
|
||||
+ <div
|
||||
+ className={styles.sidebarButton}
|
||||
+ onClick={() => onItemSelect?.(item.label)}
|
||||
+ >
|
||||
+ {item.label}
|
||||
+ </div>
|
||||
+ </li>
|
||||
+ ))}
|
||||
+</ul>
|
||||
+<div className={styles.extraButton} onClick={onExtraButtonClick}>
|
||||
+ Extra Action
|
||||
+</div>
|
||||
+</div>
|
||||
\`\`\`
|
||||
|
||||
NEW_FILE
|
||||
\`\`\`
|
||||
import React from 'react';
|
||||
import styles from './Sidebar.module.css';
|
||||
|
||||
interface SidebarProps {
|
||||
items: { label: string; href: string }[];
|
||||
onItemSelect?: (label: string) => void;
|
||||
onExtraButtonClick?: () => void;
|
||||
}
|
||||
|
||||
const Sidebar: React.FC<SidebarProps> = ({ items, onItemSelect, onExtraButtonClick }) => {
|
||||
return (
|
||||
\`\`\`
|
||||
|
||||
COMPLETION
|
||||
\`\`\`
|
||||
<div className={styles.sidebar}>
|
||||
<ul>
|
||||
{items.map((item, index) => (
|
||||
<li key={index}>
|
||||
<div
|
||||
className={styles.sidebarButton}
|
||||
onClick={() => onItemSelect?.(item.label)}
|
||||
>
|
||||
{item.label}
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<div className={styles.extraButton} onClick={onExtraButtonClick}>
|
||||
Extra Action
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Sidebar;\`\`\`
|
||||
`
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
/*--------------------------------------------------------------------------------------
|
||||
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import { KeyCode, KeyMod } from '../../../../base/common/keyCodes.js';
|
||||
import { Action2, registerAction2 } from '../../../../platform/actions/common/actions.js';
|
||||
import { ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js';
|
||||
import { KeybindingWeight } from '../../../../platform/keybinding/common/keybindingsRegistry.js';
|
||||
import { ICodeEditorService } from '../../../../editor/browser/services/codeEditorService.js';
|
||||
import { IInlineDiffsService } from './inlineDiffsService.js';
|
||||
import { roundRangeToLines } from './sidebarActions.js';
|
||||
import { VOID_CTRL_K_ACTION_ID } from './actionIDs.js';
|
||||
import { localize2 } from '../../../../nls.js';
|
||||
import { IMetricsService } from '../common/metricsService.js';
|
||||
|
||||
|
||||
export type QuickEditPropsType = {
|
||||
diffareaid: number,
|
||||
initStreamingDiffZoneId: number | null,
|
||||
textAreaRef: (ref: HTMLTextAreaElement | null) => void;
|
||||
onChangeHeight: (height: number) => void;
|
||||
onChangeText: (text: string) => void;
|
||||
initText: string | null;
|
||||
}
|
||||
|
||||
export type QuickEdit = {
|
||||
startLine: number, // 0-indexed
|
||||
beforeCode: string,
|
||||
afterCode?: string,
|
||||
instructions?: string,
|
||||
responseText?: string, // model can produce a text response too
|
||||
}
|
||||
|
||||
|
||||
registerAction2(class extends Action2 {
|
||||
constructor(
|
||||
) {
|
||||
super({
|
||||
id: VOID_CTRL_K_ACTION_ID,
|
||||
f1: true,
|
||||
title: localize2('voidQuickEditAction', 'Void: Quick Edit'),
|
||||
keybinding: {
|
||||
primary: KeyMod.CtrlCmd | KeyCode.KeyK,
|
||||
weight: KeybindingWeight.VoidExtension,
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async run(accessor: ServicesAccessor): Promise<void> {
|
||||
|
||||
const editorService = accessor.get(ICodeEditorService)
|
||||
const metricsService = accessor.get(IMetricsService)
|
||||
metricsService.capture('Ctrl+K', {})
|
||||
|
||||
const editor = editorService.getActiveCodeEditor()
|
||||
if (!editor) return;
|
||||
const model = editor.getModel()
|
||||
if (!model) return;
|
||||
const selection = roundRangeToLines(editor.getSelection(), { emptySelectionBehavior: 'line' })
|
||||
if (!selection) return;
|
||||
|
||||
|
||||
const { startLineNumber: startLine, endLineNumber: endLine } = selection
|
||||
|
||||
const inlineDiffsService = accessor.get(IInlineDiffsService)
|
||||
inlineDiffsService.addCtrlKZone({ startLine, endLine, editor })
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,77 @@
|
||||
/*--------------------------------------------------------------------------------------
|
||||
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Emitter, Event } from '../../../../base/common/event.js';
|
||||
import { Disposable } from '../../../../base/common/lifecycle.js';
|
||||
import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js';
|
||||
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
|
||||
import { QuickEdit } from './quickEditActions.js';
|
||||
|
||||
|
||||
|
||||
// service that manages state
|
||||
export type VoidQuickEditState = {
|
||||
quickEditsOfDocument: { [uri: string]: QuickEdit }
|
||||
}
|
||||
|
||||
export interface IQuickEditStateService {
|
||||
readonly _serviceBrand: undefined;
|
||||
|
||||
readonly state: VoidQuickEditState; // readonly to the user
|
||||
setState(newState: Partial<VoidQuickEditState>): void;
|
||||
onDidChangeState: Event<void>;
|
||||
|
||||
onDidFocusChat: Event<void>;
|
||||
onDidBlurChat: Event<void>;
|
||||
fireFocusChat(): void;
|
||||
fireBlurChat(): void;
|
||||
|
||||
}
|
||||
|
||||
export const IQuickEditStateService = createDecorator<IQuickEditStateService>('voidQuickEditStateService');
|
||||
class VoidQuickEditStateService extends Disposable implements IQuickEditStateService {
|
||||
_serviceBrand: undefined;
|
||||
|
||||
static readonly ID = 'voidQuickEditStateService';
|
||||
|
||||
private readonly _onDidChangeState = new Emitter<void>();
|
||||
readonly onDidChangeState: Event<void> = this._onDidChangeState.event;
|
||||
|
||||
private readonly _onFocusChat = new Emitter<void>();
|
||||
readonly onDidFocusChat: Event<void> = this._onFocusChat.event;
|
||||
|
||||
private readonly _onBlurChat = new Emitter<void>();
|
||||
readonly onDidBlurChat: Event<void> = this._onBlurChat.event;
|
||||
|
||||
|
||||
// state
|
||||
state: VoidQuickEditState
|
||||
|
||||
constructor(
|
||||
) {
|
||||
super()
|
||||
|
||||
// initial state
|
||||
this.state = { quickEditsOfDocument: {} }
|
||||
}
|
||||
|
||||
|
||||
setState(newState: Partial<VoidQuickEditState>) {
|
||||
|
||||
this.state = { ...this.state, ...newState }
|
||||
this._onDidChangeState.fire()
|
||||
}
|
||||
|
||||
fireFocusChat() {
|
||||
this._onFocusChat.fire()
|
||||
}
|
||||
|
||||
fireBlurChat() {
|
||||
this._onBlurChat.fire()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
registerSingleton(IQuickEditStateService, VoidQuickEditStateService, InstantiationType.Eager);
|
||||
@@ -1,13 +1,153 @@
|
||||
import { execSync } from 'child_process';
|
||||
/*--------------------------------------------------------------------------------------
|
||||
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
// clear temp dirs
|
||||
execSync('npx rimraf out/ && npx rimraf src2/')
|
||||
import { spawn, execSync } from 'child_process';
|
||||
// Added lines below
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
// build and scope tailwind
|
||||
execSync('npx scope-tailwind ./src -o src2/ -s void-scope -c styles.css -p "prefix-" ')
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
// tsup to build src2/ into out/
|
||||
execSync('npx tsup')
|
||||
function doesPathExist(filePath) {
|
||||
try {
|
||||
const stats = fs.statSync(filePath);
|
||||
|
||||
return stats.isFile();
|
||||
} catch (err) {
|
||||
if (err.code === 'ENOENT') {
|
||||
return false;
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
console.log('✅ Done building! Press Cmd+Shift+B again.')
|
||||
/*
|
||||
|
||||
This function finds `globalDesiredPath` given `localDesiredPath` and `currentPath`
|
||||
|
||||
Diagram:
|
||||
|
||||
...basePath/
|
||||
└── void/
|
||||
├── ...currentPath/ (defined globally)
|
||||
└── ...localDesiredPath/ (defined locally)
|
||||
|
||||
*/
|
||||
function findDesiredPathFromLocalPath(localDesiredPath, currentPath) {
|
||||
|
||||
// walk upwards until currentPath + localDesiredPath exists
|
||||
while (!doesPathExist(path.join(currentPath, localDesiredPath))) {
|
||||
const parentDir = path.dirname(currentPath);
|
||||
|
||||
if (parentDir === currentPath) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
currentPath = parentDir;
|
||||
}
|
||||
|
||||
// return the `globallyDesiredPath`
|
||||
const globalDesiredPath = path.join(currentPath, localDesiredPath)
|
||||
return globalDesiredPath;
|
||||
}
|
||||
|
||||
// hack to refresh styles automatically
|
||||
function saveStylesFile() {
|
||||
setTimeout(() => {
|
||||
try {
|
||||
const pathToCssFile = findDesiredPathFromLocalPath('./src/vs/workbench/contrib/void/browser/react/src2/styles.css', __dirname);
|
||||
|
||||
if (pathToCssFile === undefined) {
|
||||
console.error('[scope-tailwind] Error finding styles.css');
|
||||
return;
|
||||
}
|
||||
|
||||
// Or re-write with the same content:
|
||||
const content = fs.readFileSync(pathToCssFile, 'utf8');
|
||||
fs.writeFileSync(pathToCssFile, content, 'utf8');
|
||||
console.log('[scope-tailwind] Force-saved styles.css');
|
||||
} catch (err) {
|
||||
console.error('[scope-tailwind] Error saving styles.css:', err);
|
||||
}
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
const isWatch = args.includes('--watch') || args.includes('-w');
|
||||
|
||||
if (isWatch) {
|
||||
// this just builds it if it doesn't exist instead of waiting for the watcher to trigger
|
||||
// Check if src2/ exists; if not, do an initial scope-tailwind build
|
||||
if (!fs.existsSync('src2')) {
|
||||
try {
|
||||
console.log('🔨 Running initial scope-tailwind build to create src2 folder...');
|
||||
execSync(
|
||||
'npx scope-tailwind ./src -o src2/ -s void-scope -c styles.css -p "void-"',
|
||||
{ stdio: 'inherit' }
|
||||
);
|
||||
console.log('✅ src2/ created successfully.');
|
||||
} catch (err) {
|
||||
console.error('❌ Error running initial scope-tailwind build:', err);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Watch mode
|
||||
const scopeTailwindWatcher = spawn('npx', [
|
||||
'nodemon',
|
||||
'--watch', 'src',
|
||||
'--ext', 'ts,tsx,css',
|
||||
'--exec',
|
||||
'npx scope-tailwind ./src -o src2/ -s void-scope -c styles.css -p "void-"'
|
||||
]);
|
||||
|
||||
const tsupWatcher = spawn('npx', [
|
||||
'tsup',
|
||||
'--watch'
|
||||
]);
|
||||
|
||||
scopeTailwindWatcher.stdout.on('data', (data) => {
|
||||
console.log(`[scope-tailwind] ${data}`);
|
||||
// If the output mentions "styles.css", trigger the save:
|
||||
if (data.toString().includes('styles.css')) {
|
||||
saveStylesFile();
|
||||
}
|
||||
});
|
||||
|
||||
scopeTailwindWatcher.stderr.on('data', (data) => {
|
||||
console.error(`[scope-tailwind] ${data}`);
|
||||
});
|
||||
|
||||
// Handle tsup watcher output
|
||||
tsupWatcher.stdout.on('data', (data) => {
|
||||
console.log(`[tsup] ${data}`);
|
||||
});
|
||||
|
||||
tsupWatcher.stderr.on('data', (data) => {
|
||||
console.error(`[tsup] ${data}`);
|
||||
});
|
||||
|
||||
// Handle process termination
|
||||
process.on('SIGINT', () => {
|
||||
scopeTailwindWatcher.kill();
|
||||
tsupWatcher.kill();
|
||||
process.exit();
|
||||
});
|
||||
|
||||
console.log('🔄 Watchers started! Press Ctrl+C to stop both watchers.');
|
||||
} else {
|
||||
// Build mode
|
||||
console.log('📦 Building...');
|
||||
|
||||
// Run scope-tailwind once
|
||||
execSync('npx scope-tailwind ./src -o src2/ -s void-scope -c styles.css -p "void-"', { stdio: 'inherit' });
|
||||
|
||||
// Run tsup once
|
||||
execSync('npx tsup', { stdio: 'inherit' });
|
||||
|
||||
console.log('✅ Build complete!');
|
||||
}
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
/*--------------------------------------------------------------------------------------
|
||||
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import { diffLines, Change } from 'diff';
|
||||
|
||||
export { diffLines, Change }
|
||||