diff --git a/.npmrc b/.npmrc index 97baacfce6f4c..e2305b402ea33 100644 --- a/.npmrc +++ b/.npmrc @@ -1,6 +1,6 @@ disturl="https://electronjs.org/headers" -target="39.5.2" -ms_build_id="13298971" +target="39.6.0" +ms_build_id="13312042" runtime="electron" ignore-scripts=false build_from_source="true" diff --git a/build/checksums/electron.txt b/build/checksums/electron.txt index 68a137efaa2eb..3df57a48a97d2 100644 --- a/build/checksums/electron.txt +++ b/build/checksums/electron.txt @@ -1,75 +1,75 @@ -a87acdd1cb412151f71c25154dccdf26744b80471acbef03dd20cfea2e0bd2d1 *chromedriver-v39.5.2-darwin-arm64.zip -2181b4bfb4c9e543013bd08e0d6d1e1b3b80a7d99e1589a7e75089760d1d46c2 *chromedriver-v39.5.2-darwin-x64.zip -f18c47c5544f158eaa1a3705bbcaab7a879c1829926310ebfd876749393ed2a0 *chromedriver-v39.5.2-linux-arm64.zip -74435311880444074e0fedd2a52ecdf9a18f13689e9da700f6fae4c5b553e4d5 *chromedriver-v39.5.2-linux-armv7l.zip -dd240f331c4ae2dd8e83aa517bc17a107f0747d62bb4321901c18ad4e48d8ea6 *chromedriver-v39.5.2-linux-x64.zip -fa02c2e7f7b37b0944f796297545227e65b0ed09914b30a7469560db631e0b92 *chromedriver-v39.5.2-mas-arm64.zip -b74448c03567849ec9226ddcb66b4c6f538608e73adc02fe52bdac740ecd8836 *chromedriver-v39.5.2-mas-x64.zip -e8677c94445aaf9093b2dcdcaca6d95502001986cf2def6831f27d821997783e *chromedriver-v39.5.2-win32-arm64.zip -7151dfd5c39e2e9e974e2039916d27250b9d6e4f9acbc77abd8a3957c64a5e73 *chromedriver-v39.5.2-win32-ia32.zip -3cc66a7d98d67835b01c6732ad5f6083a30d8d91c6256dd1b1b6aee89df3b07e *chromedriver-v39.5.2-win32-x64.zip -ed614289f57a5240db7c6ae20725468f4ffaaddd9d2a3472db2637af7cf73c52 *electron-api.json -f6a4d5f3f66b200aa71ea10526ad6a0963d6595f90121cea148a93312d1e9478 *electron-v39.5.2-darwin-arm64-dsym-snapshot.zip -885cec937ece1e1b975e3b21ad01481e6fea89906a83fc88ff303fb8e5111117 *electron-v39.5.2-darwin-arm64-dsym.zip -9425e8286707e6c3cc280e8d415c9f1d7655e59c2e5c9e43866285a81cf8e5cf *electron-v39.5.2-darwin-arm64-symbols.zip -618ada5c3ea7fdd3b2405d6e2b62c895d6531b9b0e12120e26a2e20e58d68e67 *electron-v39.5.2-darwin-arm64.zip -4e311c4f48aed9d9c21ef99f39a70ce77bda85c7a419f604fc02c278b8d3f30f *electron-v39.5.2-darwin-x64-dsym-snapshot.zip -55e4be9743fb9269161db3c6a57b2684d2d47f65d945fd03958fec58e7d081b5 *electron-v39.5.2-darwin-x64-dsym.zip -8705af037514b7fd336ea90ef8990acbc5cce7d16f02925fd77e7f1b30ab0e5a *electron-v39.5.2-darwin-x64-symbols.zip -5f51d106849b2afaa0a233f487485f0a484ac9dcb57c432feb6ae2731ed5cdfd *electron-v39.5.2-darwin-x64.zip -829f10fad95dee51c74b221ab2569fea39102e27f2a00647bf04da6e8a90d386 *electron-v39.5.2-linux-arm64-debug.zip -bd88a964bde7aec2490e599c32d8a872653a80fa07ae06f89b8b68bebb4a678b *electron-v39.5.2-linux-arm64-symbols.zip -c1ed7b797fa534931b36cdd6a69ea6ed5de351d5cc4086aa155a8ed92fb518ed *electron-v39.5.2-linux-arm64.zip -de56f65b0d9543a92230b9484de58797602a6008625d429415bc8221cf45525f *electron-v39.5.2-linux-armv7l-debug.zip -dd6cecefe3fa2dd4ff8c97595bcb962814c92c989ae0a70dae73479709c5fe63 *electron-v39.5.2-linux-armv7l-symbols.zip -0aefd0e66439ea4eb3db3e57742ca88988bb8d694eaa9c7d332c733c373ee32d *electron-v39.5.2-linux-armv7l.zip -3c70967c5ec60a5bd3feba64052276e6920d659af87e994fc3b908e21cd93d33 *electron-v39.5.2-linux-x64-debug.zip -51a567542bc8556ef82bfc7d3de4badb44ddbf8f240e4482be77eb30e95ad67b *electron-v39.5.2-linux-x64-symbols.zip -1fddb169ed3a342ddaff1807c82c388966dbf4697349dd4902f2987152257337 *electron-v39.5.2-linux-x64.zip -84d32cbf59f3fd2fc8d83c0dbe66b08b3a5f408f229066a2a4e71db5194e69c2 *electron-v39.5.2-mas-arm64-dsym-snapshot.zip -f3a2701498c4b1ae3d59105225b4ba2b9a0fb0725d5f0591049462e643b63822 *electron-v39.5.2-mas-arm64-dsym.zip -32ebb3e5a653f2042697fb411b4e4d52fdbd2af959472aa8b51e79420fbf9c78 *electron-v39.5.2-mas-arm64-symbols.zip -92b9d5f559aed75931c744dd2c5a7124befe6c5d12e4cb777586107412375a11 *electron-v39.5.2-mas-arm64.zip -1fc599b7efa4017d441c68795df78d6d83ff952aee42efe5de2d3eb1c964d240 *electron-v39.5.2-mas-x64-dsym-snapshot.zip -af86806db4e0ee81e6949560d8a98ed56dbaf88698a7b9d15a30c64be05909f7 *electron-v39.5.2-mas-x64-dsym.zip -1f81e8e29125f5f693eb3b1cec18ee16487123319add8d19f3f0327d1b16a8f6 *electron-v39.5.2-mas-x64-symbols.zip -6d14692b43d0525f7fc1ce33485e586d1f1d6de68c2e7f4304917beea84bf576 *electron-v39.5.2-mas-x64.zip -287fbc58d5635423e5762f37f551d039d4aebcf14b3ef58d27da67a8f448d344 *electron-v39.5.2-win32-arm64-pdb.zip -6af15c530a187075418ff095c946f3fdb7b45709f402a61e7f9c93bc094f6bc7 *electron-v39.5.2-win32-arm64-symbols.zip -efec460f92ff99a9d5970dd7a67fd0be5272989cfacc9389dec954c706b23f7d *electron-v39.5.2-win32-arm64-toolchain-profile.zip -13db5c25d118485e8edc6ecaa9b239a57403e728db1a085de9441b35543f398b *electron-v39.5.2-win32-arm64.zip -83fe5123b5810ee624ee5717d1466612a8b378a79c789f8963ac5d314efbc8d2 *electron-v39.5.2-win32-ia32-pdb.zip -1abf6ac223b42e755486248218b91ebc5a8d862cea9c0bafe8abeca7c0cfce2e *electron-v39.5.2-win32-ia32-symbols.zip -efec460f92ff99a9d5970dd7a67fd0be5272989cfacc9389dec954c706b23f7d *electron-v39.5.2-win32-ia32-toolchain-profile.zip -9a5026091de20d4959b941ba594353ee5e8c7d7b542e5e729525d73c5cb67458 *electron-v39.5.2-win32-ia32.zip -b36c477788cc258f14faf9dd35a16d46512605e0378305ecbab8f587d16ca985 *electron-v39.5.2-win32-x64-pdb.zip -eba02395e7c3e6e508747800e36e529c000a6087f3f62a9c7a52328b3d91f378 *electron-v39.5.2-win32-x64-symbols.zip -efec460f92ff99a9d5970dd7a67fd0be5272989cfacc9389dec954c706b23f7d *electron-v39.5.2-win32-x64-toolchain-profile.zip -f3658e4e9eb7585f9646db2469f6d4c30837bd524013ef677bd305cc90f4fb11 *electron-v39.5.2-win32-x64.zip -78d7e90d7dc1f753868011256075afcd24cbac74971f30d769105cff516f49fb *electron.d.ts -27cf8e375bc22ceea6b3d42132f2927ea544edac2b8b2c5dc3c10b5df8dfb027 *ffmpeg-v39.5.2-darwin-arm64.zip -321d9c07f74c6cf77027ec07d888fb7b634d6589207e3c9e016c43e277ca9944 *ffmpeg-v39.5.2-darwin-x64.zip -52ae6eccbdb4a9403a6c3eb46b356a28940ec25958b6b9181fb2f38e612e40ed *ffmpeg-v39.5.2-linux-arm64.zip -622cb781fb1e3b9617e7e60c36384427f7b0d9b5ad888e9bc356a83b050e13f1 *ffmpeg-v39.5.2-linux-armv7l.zip -ba441851788008362f013bf2983b22b0042af8df31bf90123328f928cc067492 *ffmpeg-v39.5.2-linux-x64.zip -27cf8e375bc22ceea6b3d42132f2927ea544edac2b8b2c5dc3c10b5df8dfb027 *ffmpeg-v39.5.2-mas-arm64.zip -321d9c07f74c6cf77027ec07d888fb7b634d6589207e3c9e016c43e277ca9944 *ffmpeg-v39.5.2-mas-x64.zip -6aabdb0f00966fb37d41a5ab193b0fdabb32a9556eae3c847ea22b946cd67f0d *ffmpeg-v39.5.2-win32-arm64.zip -b3af48c3fdbccc0e0d97abd569892eaf871a844c7308de8a583ea8e7541491d2 *ffmpeg-v39.5.2-win32-ia32.zip -8b53aae4bc3ae924e6d9e4b006708b53a335e7853c44db24ef49c982ab35acd4 *ffmpeg-v39.5.2-win32-x64.zip -cc48430bbe7690d7e8117b10edaf34587ef664012f04152158aa1c75852737dc *hunspell_dictionaries.zip -3c365db75791b6c6273d9b182698966194e0a1706286e1114c5846972efa92e9 *libcxx-objects-v39.5.2-linux-arm64.zip -e674701d184288a61d2fa4920ef3c9cd9ceac1e5b8df87694c3db6599abc02bc *libcxx-objects-v39.5.2-linux-armv7l.zip -998552c78147aeac569c1e7a5cb3fda94f6f6690aca105198280a28c7feb662b *libcxx-objects-v39.5.2-linux-x64.zip -8082c0d7d5f00631e57139a4dddfbfd9bfe80656abf78737fc3f5e729bd110e5 *libcxx_headers.zip -edf24979ef27199ad5cd9de9b52a56ef9bad022c7d9daff59c81a873bf0185cc *libcxxabi_headers.zip -9dafc93aa576649d79d7df97c21d1644783c3a522d2cef3f7a484f7b02e67d9c *mksnapshot-v39.5.2-darwin-arm64.zip -924fafb200efd7375cedd83e25202309229afe19e1df6efc4e2c8a1f344f1510 *mksnapshot-v39.5.2-darwin-x64.zip -274ed43bbe1af8cd822ad13bfc670ada8ff9b0a604b25e13b0aa26f4e519099d *mksnapshot-v39.5.2-linux-arm64-x64.zip -0a741013de490fb53fb230e812ae332dc9cebc93614c340abe5c427acae124bb *mksnapshot-v39.5.2-linux-armv7l-x64.zip -ce12c4dea98046cbe39cf1393983e34784f5d7722fd920f01ea59697fc192cc3 *mksnapshot-v39.5.2-linux-x64.zip -bdd5d316d30605d93527473d343fd4f7661ebaa415d07342be3d7450a9e46ade *mksnapshot-v39.5.2-mas-arm64.zip -250c8e1736e7da045db478cb85ddc0ba8ae02ca22b2fe5fc033c444335734f3a *mksnapshot-v39.5.2-mas-x64.zip -aeaf6d7f67dfd6b28e6c8be5a0d147e11bbb843c2b953c20fd8fcbc16178b015 *mksnapshot-v39.5.2-win32-arm64-x64.zip -583a12e704ae437cefbfc3bda11a413535d92bc598133d32517db1e9bb406566 *mksnapshot-v39.5.2-win32-ia32.zip -8d730a8ede9874203449b1d7f4486582714ebed3b00e7c5c2158926d26cec040 *mksnapshot-v39.5.2-win32-x64.zip +1a1bb622d9788793310458b7bf9eedcea8347da9556dd1d7661b757c15ebfdd5 *chromedriver-v39.6.0-darwin-arm64.zip +c84565c127adeca567ca69e85bbd8f387fff1f83c09e69f6f851528f5602dc4e *chromedriver-v39.6.0-darwin-x64.zip +f50df11f99a2e3df84560d5331608cd0a9d7a147a1490f25edfd8a95531918a2 *chromedriver-v39.6.0-linux-arm64.zip +a571fd25e33f3b3bded91506732a688319d93eb652e959bb19a09cd3f67f9e5f *chromedriver-v39.6.0-linux-armv7l.zip +2a50751190bbfe07984f7d8cbf2f12c257a4c132a36922a78c4e320169b8f498 *chromedriver-v39.6.0-linux-x64.zip +cf6034c20b727c48a6f44bb87b1ec89fd4189f56200a32cd39cedaab3f19e007 *chromedriver-v39.6.0-mas-arm64.zip +d2107db701c41fa5f3aaa04c279275ac4dcffde4542c032c806939acd8c6cd6c *chromedriver-v39.6.0-mas-x64.zip +1593ed5550fa11c549fd4ff5baea5cb7806548bff15b79340343ac24a86d6de3 *chromedriver-v39.6.0-win32-arm64.zip +deee89cbeed935a57551294fbc59f6a346b76769e27dd78a59a35a82ae3037d9 *chromedriver-v39.6.0-win32-ia32.zip +f88a23ebc246ed2a506d6d172eb9ffbb4c9d285103285a735e359268fcd08895 *chromedriver-v39.6.0-win32-x64.zip +2e1ec8568f4fda21dc4bb7231cdb0427fa31bb03c4bc39f8aa36659894f2d23e *electron-api.json +03e743428685b44beeab9aa51bad7437387dc2ce299b94745ed8fb0923dd9a07 *electron-v39.6.0-darwin-arm64-dsym-snapshot.zip +723d64530286ebd58539bc29deb65e9334ae8450a714b075d369013b4bbfdce0 *electron-v39.6.0-darwin-arm64-dsym.zip +8f529fbbed8c386f3485614fa059ea9408ebe17d3f0c793269ea52ef3efdf8df *electron-v39.6.0-darwin-arm64-symbols.zip +dace1f9e5c49f4f63f32341f8b0fb7f16b8cf07ce5fcb17abcc0b33782966b8c *electron-v39.6.0-darwin-arm64.zip +e2425514469c4382be374e676edff6779ef98ca1c679b1500337fa58aa863e98 *electron-v39.6.0-darwin-x64-dsym-snapshot.zip +877e72afd7d8695e8a4420a74765d45c30fad30606d3dbab07a0e88fe600e3f6 *electron-v39.6.0-darwin-x64-dsym.zip +ae958c150c6fe76fc7989a28ddb6104851f15d2e24bd32fe60f51e308954a816 *electron-v39.6.0-darwin-x64-symbols.zip +bed88dac3ac28249a020397d83f3f61871c7eaea2099d5bf6b1e92878cb14f19 *electron-v39.6.0-darwin-x64.zip +a86e9470d6084611f38849c9f9b3311584393fa81b55d0bbf7e284a649b729cf *electron-v39.6.0-linux-arm64-debug.zip +e7d7aec3873a6d2f2c9fe406a27a8668910f8b4fdf55a36b5302d9db3ec390db *electron-v39.6.0-linux-arm64-symbols.zip +d6ded47a49046eb031800cf70f2b5d763ccac11dac64e70a874c62aaa115ccba *electron-v39.6.0-linux-arm64.zip +2bf6a75c9f3c2400698c325e48c9b6444d108e4d76544fb130d04605002ae084 *electron-v39.6.0-linux-armv7l-debug.zip +421d02c8a063602b22e4f16a2614fe6cc13e07f9d4ead309fe40aeac296fe951 *electron-v39.6.0-linux-armv7l-symbols.zip +ee34896d1317f1572ed4f3ed8eb1719f599f250d442fc6afb6ec40091c4f4cdc *electron-v39.6.0-linux-armv7l.zip +233f55caae4514144310928248a96bd3a3ce7ac6dc1ff99e7531737a579793b1 *electron-v39.6.0-linux-x64-debug.zip +eca69e741b00ce141b9c2e6e63c1f77cd834a85aa095385f032fdb58d3154fff *electron-v39.6.0-linux-x64-symbols.zip +94bf4bee48f3c657edffd4556abbe62556ca8225cbb4528d62eb858233a3c34b *electron-v39.6.0-linux-x64.zip +6dfebeb760627df74c65ff8da7088fb77e0ae222cab5590fea4cdd37c060ea06 *electron-v39.6.0-mas-arm64-dsym-snapshot.zip +b327d41507546799451a684b6061caed10f1c16ee39a7e686aac71187f8b7afe *electron-v39.6.0-mas-arm64-dsym.zip +02a56a9c3c3522ebc653f03ad88be9a2f46594c730a767a28e7322ddb7a789b7 *electron-v39.6.0-mas-arm64-symbols.zip +2fe93cd39521371bb5722c358feebadc5e79d79628b07a79a00a9d918e261de4 *electron-v39.6.0-mas-arm64.zip +f25ddc8a9b2b699d6d9e54fdf66220514e387ae36e45efeb4d8217b1462503f6 *electron-v39.6.0-mas-x64-dsym-snapshot.zip +6732026b6a3728bea928af0c5928bf82d565eebeb3f5dc5b6991639d27e7c457 *electron-v39.6.0-mas-x64-dsym.zip +5260dabf5b0fc369e0f69d3286fbcce9d67bc65e3364e17f7bb13dd49e320422 *electron-v39.6.0-mas-x64-symbols.zip +905f7cf95270afa92972b6c9242fc50c0afd65ffd475a81ded6033588f27a613 *electron-v39.6.0-mas-x64.zip +9204c9844e89f5ca0b32a8347cf9141d8dcb66671906e299afa06004f464d9b0 *electron-v39.6.0-win32-arm64-pdb.zip +6778c54d8cf7a0d305e4334501c3b877daf4737197187120ac18064f4e093b23 *electron-v39.6.0-win32-arm64-symbols.zip +efec460f92ff99a9d5970dd7a67fd0be5272989cfacc9389dec954c706b23f7d *electron-v39.6.0-win32-arm64-toolchain-profile.zip +22b96aca4cf8f7823b98e3b20b6131e521e0100c5cd03ab76f106eefbd0399cf *electron-v39.6.0-win32-arm64.zip +f5b69c8c1c9349a1f3b4309fb3fa1cf6326953e0807d2063fc27ba9f1400232e *electron-v39.6.0-win32-ia32-pdb.zip +1d6e103869acdeb0330b26ee08089667e0b5afc506efcd7021ba761ed8b786b5 *electron-v39.6.0-win32-ia32-symbols.zip +efec460f92ff99a9d5970dd7a67fd0be5272989cfacc9389dec954c706b23f7d *electron-v39.6.0-win32-ia32-toolchain-profile.zip +2b30e5bc923fff1443e2a4d1971cb9b26f61bd6a454cfbb991042457bab4d623 *electron-v39.6.0-win32-ia32.zip +5f93924c317206a2a4800628854e44e68662a9c40b3457c9e72690d6fff884d3 *electron-v39.6.0-win32-x64-pdb.zip +eab07439f0a21210cd560c1169c04ea5e23c6fe0ab65bd60cffce2b9f69fd36e *electron-v39.6.0-win32-x64-symbols.zip +efec460f92ff99a9d5970dd7a67fd0be5272989cfacc9389dec954c706b23f7d *electron-v39.6.0-win32-x64-toolchain-profile.zip +e8eee36be3bb85ba6fd8fcd26cf3a264bc946ac0717762c64e168896695c8e34 *electron-v39.6.0-win32-x64.zip +2e84c606e40c7bab5530e4c83bbf3a24c28143b0a768dafa5ecf78b18d889297 *electron.d.ts +27cf8e375bc22ceea6b3d42132f2927ea544edac2b8b2c5dc3c10b5df8dfb027 *ffmpeg-v39.6.0-darwin-arm64.zip +321d9c07f74c6cf77027ec07d888fb7b634d6589207e3c9e016c43e277ca9944 *ffmpeg-v39.6.0-darwin-x64.zip +52ae6eccbdb4a9403a6c3eb46b356a28940ec25958b6b9181fb2f38e612e40ed *ffmpeg-v39.6.0-linux-arm64.zip +622cb781fb1e3b9617e7e60c36384427f7b0d9b5ad888e9bc356a83b050e13f1 *ffmpeg-v39.6.0-linux-armv7l.zip +ba441851788008362f013bf2983b22b0042af8df31bf90123328f928cc067492 *ffmpeg-v39.6.0-linux-x64.zip +27cf8e375bc22ceea6b3d42132f2927ea544edac2b8b2c5dc3c10b5df8dfb027 *ffmpeg-v39.6.0-mas-arm64.zip +321d9c07f74c6cf77027ec07d888fb7b634d6589207e3c9e016c43e277ca9944 *ffmpeg-v39.6.0-mas-x64.zip +2a358c2dbeeb259c0b6a18057b52ffb0109de69112086cb2ce02f3a79bd70cee *ffmpeg-v39.6.0-win32-arm64.zip +4555510880a7b8dff5d5d0520f641665c62494689782adbed67fa0e24b45ae67 *ffmpeg-v39.6.0-win32-ia32.zip +091ab3c97d5a1cda1e04c6bd263a2c07ea63ed7ec3fd06600af6d7e23bbbbe15 *ffmpeg-v39.6.0-win32-x64.zip +650fb5fbc7e6cc27e5caeb016f72aba756469772bbfdfb3ec0b229f973d8ad46 *hunspell_dictionaries.zip +669ef1bf8ed0f6378e67f4f8bc23d2907d7cc1db7369dbdf468e164f4ef49365 *libcxx-objects-v39.6.0-linux-arm64.zip +996d81ad796524246144e15e22ffef75faff055a102c49021d70b03f039c3541 *libcxx-objects-v39.6.0-linux-armv7l.zip +1ffb610613c11169640fa76e4790137034a0deb3b48e2aef51a01c9b96b7700a *libcxx-objects-v39.6.0-linux-x64.zip +6dd8db57473992367c7914b50d06cae3a1b713cc09ceebecfcd4107df333e759 *libcxx_headers.zip +e5c18f813cc64a7d3b0404ee9adeb9cbb49e7ee5e1054b62c71fa7d1a448ad1b *libcxxabi_headers.zip +7f58d6e1d8c75b990f7d2259de8d0896414d0f2cff2f0fe4e5c7f8037d8fe879 *mksnapshot-v39.6.0-darwin-arm64.zip +be1178e4aa1f4910ba2b8f35b5655e12182657b9e32d509b47f0b2db033f0ac5 *mksnapshot-v39.6.0-darwin-x64.zip +5e36a594067fea08bb3d7bcd60873c3e240ebcee2208bcebfbc9f77d3075cc0d *mksnapshot-v39.6.0-linux-arm64-x64.zip +2db9196d2af0148ebb7b6f1f597f46a535b7af482f95739bd1ced78e1ebf39e7 *mksnapshot-v39.6.0-linux-armv7l-x64.zip +cd673e0a908fc950e0b4246e2b099018a8ee879d12a62973a01cb7de522f5bcf *mksnapshot-v39.6.0-linux-x64.zip +0749d8735a1fd8c666862cd7020b81317c45203d01319c9be089d1e750cb2c15 *mksnapshot-v39.6.0-mas-arm64.zip +81ae98e064485f8c6c69cd6c875ee72666c0cc801a8549620d382c2d0cea3b5c *mksnapshot-v39.6.0-mas-x64.zip +2e44f75df797922e7c8bad61a1b41fed14b070a54257a6a751892b2b8b9dfe29 *mksnapshot-v39.6.0-win32-arm64-x64.zip +fb5d73a8bf4b8db80f61b7073aa8458b5c46cce5c2a4b23591e851c6fcbd0144 *mksnapshot-v39.6.0-win32-ia32.zip +118ae88dbcd6b260cfa370e46ccfb0ab00af5efbf59495aaeea56a2831f604b2 *mksnapshot-v39.6.0-win32-x64.zip diff --git a/build/next/index.ts b/build/next/index.ts index 73a08933db167..57a33f69d316c 100644 --- a/build/next/index.ts +++ b/build/next/index.ts @@ -466,7 +466,7 @@ async function compileStandaloneFiles(outDir: string, doMinify: boolean, target: format: 'cjs', // CommonJS for Electron preload platform: 'node', target: ['es2024'], - sourcemap: 'external', + sourcemap: 'linked', sourcesContent: false, minify: doMinify, banner: { js: banner }, @@ -781,7 +781,7 @@ ${tslib}`, platform: 'neutral', target: ['es2024'], packages: 'external', - sourcemap: 'external', + sourcemap: 'linked', sourcesContent: true, minify: doMinify, treeShaking: true, @@ -833,7 +833,7 @@ ${tslib}`, platform: 'node', target: ['es2024'], packages: 'external', - sourcemap: 'external', + sourcemap: 'linked', sourcesContent: true, minify: doMinify, treeShaking: true, diff --git a/cgmanifest.json b/cgmanifest.json index 402496fb37438..21554434500a7 100644 --- a/cgmanifest.json +++ b/cgmanifest.json @@ -529,13 +529,13 @@ "git": { "name": "electron", "repositoryUrl": "https://github.com/electron/electron", - "commitHash": "c49899af4c7079170a189a43c533b17a11808631", - "tag": "39.5.2" + "commitHash": "a229dbf7a56336b847b34dfff1bac79afc311eee", + "tag": "39.6.0" } }, "isOnlyProductionDependency": true, "license": "MIT", - "version": "39.5.2" + "version": "39.6.0" }, { "component": { diff --git a/extensions/git/src/quickDiffProvider.ts b/extensions/git/src/quickDiffProvider.ts new file mode 100644 index 0000000000000..3b1aa64c8faae --- /dev/null +++ b/extensions/git/src/quickDiffProvider.ts @@ -0,0 +1,120 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { FileType, l10n, LogOutputChannel, QuickDiffProvider, Uri, workspace } from 'vscode'; +import { IRepositoryResolver, Repository } from './repository'; +import { isDescendant, pathEquals } from './util'; +import { toGitUri } from './uri'; +import { Status } from './api/git'; + +export class GitQuickDiffProvider implements QuickDiffProvider { + readonly label = l10n.t('Git Local Changes (Working Tree)'); + + constructor( + private readonly repository: Repository, + private readonly repositoryResolver: IRepositoryResolver, + private readonly logger: LogOutputChannel + ) { } + + async provideOriginalResource(uri: Uri): Promise { + this.logger.trace(`[Repository][provideOriginalResource] Resource: ${uri.toString()}`); + + if (uri.scheme !== 'file') { + this.logger.trace(`[Repository][provideOriginalResource] Resource is not a file: ${uri.scheme}`); + return undefined; + } + + // Ignore path that is inside the .git directory (ex: COMMIT_EDITMSG) + if (isDescendant(this.repository.dotGit.commonPath ?? this.repository.dotGit.path, uri.fsPath)) { + this.logger.trace(`[Repository][provideOriginalResource] Resource is inside .git directory: ${uri.toString()}`); + return undefined; + } + + // Ignore symbolic links + const stat = await workspace.fs.stat(uri); + if ((stat.type & FileType.SymbolicLink) !== 0) { + this.logger.trace(`[Repository][provideOriginalResource] Resource is a symbolic link: ${uri.toString()}`); + return undefined; + } + + // Ignore path that is not inside the current repository + if (this.repositoryResolver.getRepository(uri) !== this.repository) { + this.logger.trace(`[Repository][provideOriginalResource] Resource is not part of the repository: ${uri.toString()}`); + return undefined; + } + + // Ignore path that is inside a hidden repository + if (this.repository.isHidden === true) { + this.logger.trace(`[Repository][provideOriginalResource] Repository is hidden: ${uri.toString()}`); + return undefined; + } + + // Ignore path that is inside a merge group + if (this.repository.mergeGroup.resourceStates.some(r => pathEquals(r.resourceUri.fsPath, uri.fsPath))) { + this.logger.trace(`[Repository][provideOriginalResource] Resource is part of a merge group: ${uri.toString()}`); + return undefined; + } + + // Ignore path that is untracked + if (this.repository.untrackedGroup.resourceStates.some(r => pathEquals(r.resourceUri.path, uri.path)) || + this.repository.workingTreeGroup.resourceStates.some(r => pathEquals(r.resourceUri.path, uri.path) && r.type === Status.UNTRACKED)) { + this.logger.trace(`[Repository][provideOriginalResource] Resource is untracked: ${uri.toString()}`); + return undefined; + } + + // Ignore path that is git ignored + const ignored = await this.repository.checkIgnore([uri.fsPath]); + if (ignored.size > 0) { + this.logger.trace(`[Repository][provideOriginalResource] Resource is git ignored: ${uri.toString()}`); + return undefined; + } + + const originalResource = toGitUri(uri, '', { replaceFileExtension: true }); + this.logger.trace(`[Repository][provideOriginalResource] Original resource: ${originalResource.toString()}`); + + return originalResource; + } +} + +export class StagedResourceQuickDiffProvider implements QuickDiffProvider { + readonly label = l10n.t('Git Local Changes (Index)'); + + constructor( + private readonly _repository: Repository, + private readonly logger: LogOutputChannel + ) { } + + async provideOriginalResource(uri: Uri): Promise { + this.logger.trace(`[StagedResourceQuickDiffProvider][provideOriginalResource] Resource: ${uri.toString()}`); + + if (uri.scheme !== 'file') { + this.logger.trace(`[StagedResourceQuickDiffProvider][provideOriginalResource] Resource is not a file: ${uri.scheme}`); + return undefined; + } + + // Ignore path that is inside a hidden repository + if (this._repository.isHidden === true) { + this.logger.trace(`[StagedResourceQuickDiffProvider][provideOriginalResource] Repository is hidden: ${uri.toString()}`); + return undefined; + } + + // Ignore symbolic links + const stat = await workspace.fs.stat(uri); + if ((stat.type & FileType.SymbolicLink) !== 0) { + this.logger.trace(`[StagedResourceQuickDiffProvider][provideOriginalResource] Resource is a symbolic link: ${uri.toString()}`); + return undefined; + } + + // Ignore resources that are not in the index group + if (!this._repository.indexGroup.resourceStates.some(r => pathEquals(r.resourceUri.fsPath, uri.fsPath))) { + this.logger.trace(`[StagedResourceQuickDiffProvider][provideOriginalResource] Resource is not part of a index group: ${uri.toString()}`); + return undefined; + } + + const originalResource = toGitUri(uri, 'HEAD', { replaceFileExtension: true }); + this.logger.trace(`[StagedResourceQuickDiffProvider][provideOriginalResource] Original resource: ${originalResource.toString()}`); + return originalResource; + } +} diff --git a/extensions/git/src/repository.ts b/extensions/git/src/repository.ts index 8175e9b576a52..24a6dca48c388 100644 --- a/extensions/git/src/repository.ts +++ b/extensions/git/src/repository.ts @@ -8,7 +8,7 @@ import * as fs from 'fs'; import * as fsPromises from 'fs/promises'; import * as path from 'path'; import picomatch from 'picomatch'; -import { CancellationError, CancellationToken, CancellationTokenSource, Command, commands, Disposable, Event, EventEmitter, ExcludeSettingOptions, FileDecoration, FileType, l10n, LogLevel, LogOutputChannel, Memento, ProgressLocation, ProgressOptions, QuickDiffProvider, RelativePattern, scm, SourceControl, SourceControlInputBox, SourceControlInputBoxValidation, SourceControlInputBoxValidationType, SourceControlResourceDecorations, SourceControlResourceGroup, SourceControlResourceState, TabInputNotebookDiff, TabInputTextDiff, TabInputTextMultiDiff, ThemeColor, ThemeIcon, Uri, window, workspace, WorkspaceEdit } from 'vscode'; +import { CancellationError, CancellationToken, CancellationTokenSource, Command, commands, Disposable, Event, EventEmitter, ExcludeSettingOptions, FileDecoration, l10n, LogLevel, LogOutputChannel, Memento, ProgressLocation, ProgressOptions, RelativePattern, scm, SourceControl, SourceControlInputBox, SourceControlInputBoxValidation, SourceControlInputBoxValidationType, SourceControlResourceDecorations, SourceControlResourceGroup, SourceControlResourceState, TabInputNotebookDiff, TabInputTextDiff, TabInputTextMultiDiff, ThemeColor, ThemeIcon, Uri, window, workspace, WorkspaceEdit } from 'vscode'; import { ActionButton } from './actionButton'; import { ApiRepository } from './api/api1'; import { Branch, BranchQuery, Change, CommitOptions, DiffChange, FetchOptions, ForcePushMode, GitErrorCodes, LogOptions, Ref, RefType, Remote, RepositoryKind, Status } from './api/git'; @@ -28,6 +28,7 @@ import { IFileWatcher, watch } from './watch'; import { ISourceControlHistoryItemDetailsProviderRegistry } from './historyItemDetailsProvider'; import { GitArtifactProvider } from './artifactProvider'; import { RepositoryCache } from './repositoryCache'; +import { GitQuickDiffProvider, StagedResourceQuickDiffProvider } from './quickDiffProvider'; const timeout = (millis: number) => new Promise(c => setTimeout(c, millis)); @@ -970,7 +971,7 @@ export class Repository implements Disposable { this._sourceControl = scm.createSourceControl('git', 'Git', root, icon, this._isHidden, parent); this._sourceControl.contextValue = repository.kind; - this._sourceControl.quickDiffProvider = this; + this._sourceControl.quickDiffProvider = new GitQuickDiffProvider(this, this.repositoryResolver, logger); this._sourceControl.secondaryQuickDiffProvider = new StagedResourceQuickDiffProvider(this, logger); this._historyProvider = new GitHistoryProvider(historyItemDetailProviderRegistry, this, logger); @@ -1102,72 +1103,6 @@ export class Repository implements Disposable { return undefined; } - /** - * Quick diff label - */ - get label(): string { - return l10n.t('Git Local Changes (Working Tree)'); - } - - async provideOriginalResource(uri: Uri): Promise { - this.logger.trace(`[Repository][provideOriginalResource] Resource: ${uri.toString()}`); - - if (uri.scheme !== 'file') { - this.logger.trace(`[Repository][provideOriginalResource] Resource is not a file: ${uri.scheme}`); - return undefined; - } - - // Ignore path that is inside the .git directory (ex: COMMIT_EDITMSG) - if (isDescendant(this.dotGit.commonPath ?? this.dotGit.path, uri.fsPath)) { - this.logger.trace(`[Repository][provideOriginalResource] Resource is inside .git directory: ${uri.toString()}`); - return undefined; - } - - // Ignore symbolic links - const stat = await workspace.fs.stat(uri); - if ((stat.type & FileType.SymbolicLink) !== 0) { - this.logger.trace(`[Repository][provideOriginalResource] Resource is a symbolic link: ${uri.toString()}`); - return undefined; - } - - // Ignore path that is not inside the current repository - if (this.repositoryResolver.getRepository(uri) !== this) { - this.logger.trace(`[Repository][provideOriginalResource] Resource is not part of the repository: ${uri.toString()}`); - return undefined; - } - - // Ignore path that is inside a hidden repository - if (this.isHidden === true) { - this.logger.trace(`[Repository][provideOriginalResource] Repository is hidden: ${uri.toString()}`); - return undefined; - } - - // Ignore path that is inside a merge group - if (this.mergeGroup.resourceStates.some(r => pathEquals(r.resourceUri.fsPath, uri.fsPath))) { - this.logger.trace(`[Repository][provideOriginalResource] Resource is part of a merge group: ${uri.toString()}`); - return undefined; - } - - // Ignore path that is untracked - if (this.untrackedGroup.resourceStates.some(r => pathEquals(r.resourceUri.path, uri.path)) || - this.workingTreeGroup.resourceStates.some(r => pathEquals(r.resourceUri.path, uri.path) && r.type === Status.UNTRACKED)) { - this.logger.trace(`[Repository][provideOriginalResource] Resource is untracked: ${uri.toString()}`); - return undefined; - } - - // Ignore path that is git ignored - const ignored = await this.checkIgnore([uri.fsPath]); - if (ignored.size > 0) { - this.logger.trace(`[Repository][provideOriginalResource] Resource is git ignored: ${uri.toString()}`); - return undefined; - } - - const originalResource = toGitUri(uri, '', { replaceFileExtension: true }); - this.logger.trace(`[Repository][provideOriginalResource] Original resource: ${originalResource.toString()}`); - - return originalResource; - } - async getInputTemplate(): Promise { const commitMessage = (await Promise.all([this.repository.getMergeMessage(), this.repository.getSquashMessage()])).find(msg => !!msg); @@ -3286,44 +3221,3 @@ export class Repository implements Disposable { this.disposables = dispose(this.disposables); } } - -export class StagedResourceQuickDiffProvider implements QuickDiffProvider { - readonly label = l10n.t('Git Local Changes (Index)'); - - constructor( - private readonly _repository: Repository, - private readonly logger: LogOutputChannel - ) { } - - async provideOriginalResource(uri: Uri): Promise { - this.logger.trace(`[StagedResourceQuickDiffProvider][provideOriginalResource] Resource: ${uri.toString()}`); - - if (uri.scheme !== 'file') { - this.logger.trace(`[StagedResourceQuickDiffProvider][provideOriginalResource] Resource is not a file: ${uri.scheme}`); - return undefined; - } - - // Ignore path that is inside a hidden repository - if (this._repository.isHidden === true) { - this.logger.trace(`[StagedResourceQuickDiffProvider][provideOriginalResource] Repository is hidden: ${uri.toString()}`); - return undefined; - } - - // Ignore symbolic links - const stat = await workspace.fs.stat(uri); - if ((stat.type & FileType.SymbolicLink) !== 0) { - this.logger.trace(`[StagedResourceQuickDiffProvider][provideOriginalResource] Resource is a symbolic link: ${uri.toString()}`); - return undefined; - } - - // Ignore resources that are not in the index group - if (!this._repository.indexGroup.resourceStates.some(r => pathEquals(r.resourceUri.fsPath, uri.fsPath))) { - this.logger.trace(`[StagedResourceQuickDiffProvider][provideOriginalResource] Resource is not part of a index group: ${uri.toString()}`); - return undefined; - } - - const originalResource = toGitUri(uri, 'HEAD', { replaceFileExtension: true }); - this.logger.trace(`[StagedResourceQuickDiffProvider][provideOriginalResource] Original resource: ${originalResource.toString()}`); - return originalResource; - } -} diff --git a/package-lock.json b/package-lock.json index 66ff655b1b9e6..c44531c484a8e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,7 +15,7 @@ "@microsoft/1ds-post-js": "^3.2.13", "@parcel/watcher": "^2.5.6", "@types/semver": "^7.5.8", - "@vscode/codicons": "^0.0.45-7", + "@vscode/codicons": "^0.0.45-8", "@vscode/deviceid": "^0.1.1", "@vscode/iconv-lite-umd": "0.7.1", "@vscode/native-watchdog": "^1.4.6", @@ -100,7 +100,7 @@ "css-loader": "^6.9.1", "debounce": "^1.0.0", "deemon": "^1.13.6", - "electron": "39.5.2", + "electron": "39.6.0", "eslint": "^9.36.0", "eslint-formatter-compact": "^8.40.0", "eslint-plugin-header": "3.1.1", @@ -2947,9 +2947,9 @@ ] }, "node_modules/@vscode/codicons": { - "version": "0.0.45-7", - "resolved": "https://registry.npmjs.org/@vscode/codicons/-/codicons-0.0.45-7.tgz", - "integrity": "sha512-z7B3hI6LfrFY8uo0PrAHkZ0K0XQbKTshIHlDe5qyf0j6sjM2vNlzdj2FA8HgasYKBQ3zzpZzD/GK8on2A1AKRA==", + "version": "0.0.45-8", + "resolved": "https://registry.npmjs.org/@vscode/codicons/-/codicons-0.0.45-8.tgz", + "integrity": "sha512-5MfjQ+LBXnzLB/+nfpB8EpvHPdUkoW57cFcrIAHz52L/sBjwOxZER3+K2+nwb+/ejAiPmogTBDoJP/NM85uBtQ==", "license": "CC-BY-4.0" }, "node_modules/@vscode/deviceid": { @@ -6586,9 +6586,9 @@ "dev": true }, "node_modules/electron": { - "version": "39.5.2", - "resolved": "https://registry.npmjs.org/electron/-/electron-39.5.2.tgz", - "integrity": "sha512-EiGFKoTjCuJTsdNxSwOiKJvRbWOFHTmqnnVftpUUZf7rdMkvM6yn9i55uLNDKefvTE69M+vfMgGLa7HuY94WZg==", + "version": "39.6.0", + "resolved": "https://registry.npmjs.org/electron/-/electron-39.6.0.tgz", + "integrity": "sha512-KQK3sJ6JCyymY3HQxV0N/bVBQwKQETRW0N/+OYcrL9H6tZhpmTSaZY3qSxcruWrPIuouvoiP3Vk/JKUpw05ZIw==", "dev": true, "hasInstallScript": true, "license": "MIT", diff --git a/package.json b/package.json index e8a6f0832f2d9..c5522f49881ed 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "code-oss-dev", "version": "1.110.0", - "distro": "68c946526275fa6c9bec4d4cfe1eb331f1062ee4", + "distro": "7ae76fff0c4611dd384784edb63189ef932327d6", "author": { "name": "Microsoft Corporation" }, @@ -80,7 +80,7 @@ "@microsoft/1ds-post-js": "^3.2.13", "@parcel/watcher": "^2.5.6", "@types/semver": "^7.5.8", - "@vscode/codicons": "^0.0.45-7", + "@vscode/codicons": "^0.0.45-8", "@vscode/deviceid": "^0.1.1", "@vscode/iconv-lite-umd": "0.7.1", "@vscode/native-watchdog": "^1.4.6", @@ -165,7 +165,7 @@ "css-loader": "^6.9.1", "debounce": "^1.0.0", "deemon": "^1.13.6", - "electron": "39.5.2", + "electron": "39.6.0", "eslint": "^9.36.0", "eslint-formatter-compact": "^8.40.0", "eslint-plugin-header": "3.1.1", @@ -243,4 +243,4 @@ "optionalDependencies": { "windows-foreground-love": "0.6.1" } -} \ No newline at end of file +} diff --git a/remote/web/package-lock.json b/remote/web/package-lock.json index 663f8c5c185d2..f443df36ae51d 100644 --- a/remote/web/package-lock.json +++ b/remote/web/package-lock.json @@ -10,7 +10,7 @@ "dependencies": { "@microsoft/1ds-core-js": "^3.2.13", "@microsoft/1ds-post-js": "^3.2.13", - "@vscode/codicons": "^0.0.45-7", + "@vscode/codicons": "^0.0.45-8", "@vscode/iconv-lite-umd": "0.7.1", "@vscode/tree-sitter-wasm": "^0.3.0", "@vscode/vscode-languagedetection": "1.0.21", @@ -73,9 +73,9 @@ "integrity": "sha512-n1VPsljTSkthsAFYdiWfC+DKzK2WwcRp83Y1YAqdX552BstvsDjft9YXppjUzp11BPsapDoO1LDgrDB0XVsfNQ==" }, "node_modules/@vscode/codicons": { - "version": "0.0.45-7", - "resolved": "https://registry.npmjs.org/@vscode/codicons/-/codicons-0.0.45-7.tgz", - "integrity": "sha512-z7B3hI6LfrFY8uo0PrAHkZ0K0XQbKTshIHlDe5qyf0j6sjM2vNlzdj2FA8HgasYKBQ3zzpZzD/GK8on2A1AKRA==", + "version": "0.0.45-8", + "resolved": "https://registry.npmjs.org/@vscode/codicons/-/codicons-0.0.45-8.tgz", + "integrity": "sha512-5MfjQ+LBXnzLB/+nfpB8EpvHPdUkoW57cFcrIAHz52L/sBjwOxZER3+K2+nwb+/ejAiPmogTBDoJP/NM85uBtQ==", "license": "CC-BY-4.0" }, "node_modules/@vscode/iconv-lite-umd": { diff --git a/remote/web/package.json b/remote/web/package.json index 97b681d67b2af..fa7ad6cca706f 100644 --- a/remote/web/package.json +++ b/remote/web/package.json @@ -5,7 +5,7 @@ "dependencies": { "@microsoft/1ds-core-js": "^3.2.13", "@microsoft/1ds-post-js": "^3.2.13", - "@vscode/codicons": "^0.0.45-7", + "@vscode/codicons": "^0.0.45-8", "@vscode/iconv-lite-umd": "0.7.1", "@vscode/tree-sitter-wasm": "^0.3.0", "@vscode/vscode-languagedetection": "1.0.21", diff --git a/src/vs/platform/editor/common/editor.ts b/src/vs/platform/editor/common/editor.ts index 4fb1a9eb9aa4c..281ee03246a20 100644 --- a/src/vs/platform/editor/common/editor.ts +++ b/src/vs/platform/editor/common/editor.ts @@ -320,6 +320,41 @@ export interface IEditorOptions { */ alwaysOnTop?: boolean; }; + + /** + * Options that only apply when `MODAL_GROUP` is used for opening. + */ + modal?: IModalEditorPartOptions; +} + +export interface IModalEditorPartOptions { + + /** + * The navigation context for navigating between items + * within this modal editor. Pass `undefined` to clear. + */ + readonly navigation?: IModalEditorNavigation; +} + +/** + * Context for navigating between items within a modal editor. + */ +export interface IModalEditorNavigation { + + /** + * Total number of items in the navigation list. + */ + readonly total: number; + + /** + * Current 0-based index in the navigation list. + */ + readonly current: number; + + /** + * Navigate to the item at the given 0-based index. + */ + readonly navigate: (index: number) => void; } export interface ITextEditorSelection { diff --git a/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts b/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts index 6e847c65e2f35..0307cab0e03bd 100644 --- a/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts +++ b/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts @@ -421,11 +421,23 @@ export class ActivityBarCompositeBar extends PaneCompositeBar { getActivityBarContextMenuActions(): IAction[] { const activityBarPositionMenu = this.menuService.getMenuActions(MenuId.ActivityBarPositionMenu, this.contextKeyService, { shouldForwardArgs: true, renderShortTitle: true }); const positionActions = getContextMenuActions(activityBarPositionMenu).secondary; - const actions = [ - new SubmenuAction('workbench.action.panel.position', localize('activity bar position', "Activity Bar Position"), positionActions), - toAction({ id: ToggleSidebarPositionAction.ID, label: ToggleSidebarPositionAction.getLabel(this.layoutService), run: () => this.instantiationService.invokeFunction(accessor => new ToggleSidebarPositionAction().run(accessor)) }), + const actions: IAction[] = [ + new SubmenuAction('workbench.action.activityBar.position', localize('activity bar position', "Activity Bar Position"), positionActions), ]; + // Show size submenu only when activity bar is in default position + const activityBarPosition = this.configurationService.getValue(LayoutSettings.ACTIVITY_BAR_LOCATION); + if (activityBarPosition === ActivityBarPosition.DEFAULT) { + const isCompact = this.configurationService.getValue(LayoutSettings.ACTIVITY_BAR_COMPACT) ?? false; + const sizeActions = [ + toAction({ id: 'workbench.action.activityBar.size.default', label: localize('activityBarSizeDefault', "Default"), checked: !isCompact, run: () => this.configurationService.updateValue(LayoutSettings.ACTIVITY_BAR_COMPACT, false) }), + toAction({ id: 'workbench.action.activityBar.size.compact', label: localize('activityBarSizeCompact', "Compact"), checked: isCompact, run: () => this.configurationService.updateValue(LayoutSettings.ACTIVITY_BAR_COMPACT, true) }), + ]; + actions.push(new SubmenuAction('workbench.action.activityBar.size', localize('activity bar size', "Activity Bar Size"), sizeActions)); + } + + actions.push(toAction({ id: ToggleSidebarPositionAction.ID, label: ToggleSidebarPositionAction.getLabel(this.layoutService), run: () => this.instantiationService.invokeFunction(accessor => new ToggleSidebarPositionAction().run(accessor)) })); + if (this.part === Parts.SIDEBAR_PART) { actions.push(toAction({ id: ToggleSidebarVisibilityAction.ID, label: ToggleSidebarVisibilityAction.LABEL, run: () => this.instantiationService.invokeFunction(accessor => new ToggleSidebarVisibilityAction().run(accessor)) })); } diff --git a/src/vs/workbench/browser/parts/editor/editorCommands.ts b/src/vs/workbench/browser/parts/editor/editorCommands.ts index 70508b4a9b380..5fd906f1d8585 100644 --- a/src/vs/workbench/browser/parts/editor/editorCommands.ts +++ b/src/vs/workbench/browser/parts/editor/editorCommands.ts @@ -27,7 +27,7 @@ import { ITelemetryService } from '../../../../platform/telemetry/common/telemet import { ActiveGroupEditorsByMostRecentlyUsedQuickAccess } from './editorQuickAccess.js'; import { SideBySideEditor } from './sideBySideEditor.js'; import { TextDiffEditor } from './textDiffEditor.js'; -import { ActiveEditorCanSplitInGroupContext, ActiveEditorGroupEmptyContext, ActiveEditorGroupLockedContext, ActiveEditorStickyContext, EditorPartModalContext, EditorPartModalMaximizedContext, MultipleEditorGroupsContext, SideBySideEditorActiveContext, TextCompareEditorActiveContext } from '../../../common/contextkeys.js'; +import { ActiveEditorCanSplitInGroupContext, ActiveEditorGroupEmptyContext, ActiveEditorGroupLockedContext, ActiveEditorStickyContext, EditorPartModalContext, EditorPartModalMaximizedContext, EditorPartModalNavigationContext, MultipleEditorGroupsContext, SideBySideEditorActiveContext, TextCompareEditorActiveContext } from '../../../common/contextkeys.js'; import { CloseDirection, EditorInputCapabilities, EditorsOrder, IResourceDiffEditorInput, IUntitledTextResourceEditorInput, isEditorInputWithOptionsAndGroup } from '../../../common/editor.js'; import { EditorInput } from '../../../common/editor/editorInput.js'; import { SideBySideEditorInput } from '../../../common/editor/sideBySideEditorInput.js'; @@ -108,6 +108,8 @@ export const NEW_EMPTY_EDITOR_WINDOW_COMMAND_ID = 'workbench.action.newEmptyEdit export const CLOSE_MODAL_EDITOR_COMMAND_ID = 'workbench.action.closeModalEditor'; export const MOVE_MODAL_EDITOR_TO_MAIN_COMMAND_ID = 'workbench.action.moveModalEditorToMain'; export const TOGGLE_MODAL_EDITOR_MAXIMIZED_COMMAND_ID = 'workbench.action.toggleModalEditorMaximized'; +export const NAVIGATE_MODAL_EDITOR_PREVIOUS_COMMAND_ID = 'workbench.action.navigateModalEditorPrevious'; +export const NAVIGATE_MODAL_EDITOR_NEXT_COMMAND_ID = 'workbench.action.navigateModalEditorNext'; export const API_OPEN_EDITOR_COMMAND_ID = '_workbench.open'; export const API_OPEN_DIFF_EDITOR_COMMAND_ID = '_workbench.diff'; @@ -1500,6 +1502,64 @@ function registerModalEditorCommands(): void { } } }); + + registerAction2(class extends Action2 { + constructor() { + super({ + id: NAVIGATE_MODAL_EDITOR_PREVIOUS_COMMAND_ID, + title: localize2('navigateModalEditorPrevious', 'Navigate to Previous Item in Modal Editor'), + category: Categories.View, + precondition: ContextKeyExpr.and(EditorPartModalContext, EditorPartModalNavigationContext), + keybinding: { + primary: KeyMod.Alt | KeyCode.UpArrow, + weight: KeybindingWeight.WorkbenchContrib + 10, + when: ContextKeyExpr.and(EditorPartModalContext, EditorPartModalNavigationContext) + } + }); + } + run(accessor: ServicesAccessor): void { + const editorGroupsService = accessor.get(IEditorGroupsService); + + for (const part of editorGroupsService.parts) { + if (isModalEditorPart(part)) { + const nav = part.navigation; + if (nav && nav.current > 0) { + nav.navigate(nav.current - 1); + } + break; + } + } + } + }); + + registerAction2(class extends Action2 { + constructor() { + super({ + id: NAVIGATE_MODAL_EDITOR_NEXT_COMMAND_ID, + title: localize2('navigateModalEditorNext', 'Navigate to Next Item in Modal Editor'), + category: Categories.View, + precondition: ContextKeyExpr.and(EditorPartModalContext, EditorPartModalNavigationContext), + keybinding: { + primary: KeyMod.Alt | KeyCode.DownArrow, + weight: KeybindingWeight.WorkbenchContrib + 10, + when: ContextKeyExpr.and(EditorPartModalContext, EditorPartModalNavigationContext) + } + }); + } + run(accessor: ServicesAccessor): void { + const editorGroupsService = accessor.get(IEditorGroupsService); + + for (const part of editorGroupsService.parts) { + if (isModalEditorPart(part)) { + const nav = part.navigation; + if (nav && nav.current < nav.total - 1) { + nav.navigate(nav.current + 1); + } + break; + } + } + } + }); } function isModalEditorPart(obj: unknown): obj is IModalEditorPart { @@ -1510,6 +1570,8 @@ function isModalEditorPart(obj: unknown): obj is IModalEditorPart { && typeof part.onWillClose === 'function' && typeof part.toggleMaximized === 'function' && typeof part.maximized === 'boolean' + && typeof part.updateOptions === 'function' + && !!part.modalElement && part.windowId === mainWindow.vscodeWindowId; } diff --git a/src/vs/workbench/browser/parts/editor/editorParts.ts b/src/vs/workbench/browser/parts/editor/editorParts.ts index 8565198960330..c2ccf4a7316b0 100644 --- a/src/vs/workbench/browser/parts/editor/editorParts.ts +++ b/src/vs/workbench/browser/parts/editor/editorParts.ts @@ -28,6 +28,7 @@ import { IEditorService } from '../../../services/editor/common/editorService.js import { DeepPartial } from '../../../../base/common/types.js'; import { IStatusbarService } from '../../../services/statusbar/browser/statusbar.js'; import { mainWindow } from '../../../../base/browser/window.js'; +import { IModalEditorPartOptions } from '../../../../platform/editor/common/editor.js'; interface IEditorPartsUIState { readonly auxiliary: IAuxiliaryEditorPartState[]; @@ -157,14 +158,16 @@ export class EditorParts extends MultiWindowParts { + async createModalEditorPart(options?: IModalEditorPartOptions): Promise { // Reuse existing modal editor part if it exists if (this.modalEditorPart) { + this.modalEditorPart.updateOptions(options); + return this.modalEditorPart; } - const { part, instantiationService, disposables } = await this.instantiationService.createInstance(ModalEditorPart, this).create(); + const { part, instantiationService, disposables } = await this.instantiationService.createInstance(ModalEditorPart, this).create(options); // Keep instantiation service and reference to reuse this.modalEditorPart = part; diff --git a/src/vs/workbench/browser/parts/editor/media/modalEditorPart.css b/src/vs/workbench/browser/parts/editor/media/modalEditorPart.css index 201f1942042f4..b1cb10b4d6bd5 100644 --- a/src/vs/workbench/browser/parts/editor/media/modalEditorPart.css +++ b/src/vs/workbench/browser/parts/editor/media/modalEditorPart.css @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -/** Modal Editor Part: Modal Block */ +/** Modal Background Block */ .monaco-modal-editor-block { position: fixed; width: 100%; @@ -17,20 +17,16 @@ align-items: center; /* Never allow content to escape above the title bar */ overflow: hidden; -} - -.monaco-modal-editor-block.dimmed { background: rgba(0, 0, 0, 0.3); -} -/** Modal Editor Part: Shadow Container */ -.monaco-modal-editor-block .modal-editor-shadow { - box-shadow: 0 4px 32px var(--vscode-widget-shadow, rgba(0, 0, 0, 0.2)); - border-radius: 8px; - overflow: hidden; + .modal-editor-shadow { + box-shadow: 0 4px 32px var(--vscode-widget-shadow, rgba(0, 0, 0, 0.2)); + border-radius: 8px; + overflow: hidden; + } } -/** Modal Editor Part: Editor Container */ +/** Modal Editor Container */ .monaco-modal-editor-block .modal-editor-part { display: flex; flex-direction: column; @@ -40,13 +36,19 @@ border: 1px solid var(--vscode-editorWidget-border, var(--vscode-contrastBorder)); border-radius: 8px; overflow: hidden; -} -.monaco-modal-editor-block .modal-editor-part:focus { - outline: none; + &:focus { + outline: none; + } + + .content { + flex: 1; + position: relative; + overflow: hidden; + } } -/** Modal Editor Part: Header with title and close button */ +/** Modal Editor Header */ .monaco-modal-editor-block .modal-editor-header { display: grid; grid-template-columns: 1fr auto 1fr; @@ -57,36 +59,75 @@ color: var(--vscode-titleBar-activeForeground); background-color: var(--vscode-titleBar-activeBackground); border-bottom: 1px solid var(--vscode-titleBar-border, transparent); -} -.monaco-modal-editor-block .modal-editor-title { - grid-column: 1; - font-size: 12px; - font-weight: 500; - color: var(--vscode-titleBar-activeForeground); - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} + /* Modal Editor Title */ + .modal-editor-title { + grid-column: 1; + font-size: 12px; + font-weight: 500; + color: var(--vscode-titleBar-activeForeground); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } -.monaco-modal-editor-block .modal-editor-action-container { - grid-column: 3; - display: flex; - align-items: center; - justify-content: flex-end; -} + /* Modal Editor Navigation */ + .modal-editor-navigation { + grid-column: 2; + display: flex; + align-items: center; + height: 22px; + border-radius: 4px; + border: 1px solid var(--vscode-commandCenter-border, transparent); + overflow: hidden; + user-select: none; + -webkit-user-select: none; -.monaco-modal-editor-block .modal-editor-action-container .actions-container { - gap: 4px; -} + .modal-editor-nav-button.monaco-button { + display: flex; + align-items: center; + justify-content: center; + width: 22px; + height: 100%; + padding: 0; + border: none; + border-radius: 0; + background: none; + color: inherit; + opacity: 0.7; + } -.monaco-modal-editor-block .modal-editor-action-container .actions-container .codicon { - color: inherit; -} + .modal-editor-nav-button.monaco-button:hover:not(.disabled) { + opacity: 1; + background-color: var(--vscode-commandCenter-activeBackground); + } -/** Modal Editor Part: Ensure proper sizing */ -.monaco-modal-editor-block .modal-editor-part .content { - flex: 1; - position: relative; - overflow: hidden; + .modal-editor-nav-button.monaco-button.disabled { + opacity: 0.3; + } + + .modal-editor-nav-label { + font-size: 11px; + font-variant-numeric: tabular-nums; + opacity: 0.7; + white-space: nowrap; + padding: 0 6px; + } + } + + /* Modal Editor Actions */ + .modal-editor-action-container { + grid-column: 3; + display: flex; + align-items: center; + justify-content: flex-end; + + .actions-container { + gap: 4px; + + .codicon { + color: inherit; + } + } + } } diff --git a/src/vs/workbench/browser/parts/editor/modalEditorPart.ts b/src/vs/workbench/browser/parts/editor/modalEditorPart.ts index d42669e46088d..b58cb981051f4 100644 --- a/src/vs/workbench/browser/parts/editor/modalEditorPart.ts +++ b/src/vs/workbench/browser/parts/editor/modalEditorPart.ts @@ -4,8 +4,9 @@ *--------------------------------------------------------------------------------------------*/ import './media/modalEditorPart.css'; -import { $, addDisposableListener, append, EventHelper, EventType, isHTMLElement } from '../../../../base/browser/dom.js'; +import { $, addDisposableListener, append, EventHelper, EventType, hide, isHTMLElement, show } from '../../../../base/browser/dom.js'; import { StandardKeyboardEvent } from '../../../../base/browser/keyboardEvent.js'; +import { Button } from '../../../../base/browser/ui/button/button.js'; import { KeyCode } from '../../../../base/common/keyCodes.js'; import { Emitter, Event } from '../../../../base/common/event.js'; import { DisposableStore, MutableDisposable, toDisposable } from '../../../../base/common/lifecycle.js'; @@ -23,13 +24,15 @@ import { IEditorGroupView, IEditorPartsView } from './editor.js'; import { EditorPart } from './editorPart.js'; import { GroupDirection, GroupsOrder, IModalEditorPart, GroupActivationReason } from '../../../services/editor/common/editorGroupsService.js'; import { IEditorService } from '../../../services/editor/common/editorService.js'; -import { EditorPartModalContext, EditorPartModalMaximizedContext } from '../../../common/contextkeys.js'; +import { EditorPartModalContext, EditorPartModalMaximizedContext, EditorPartModalNavigationContext } from '../../../common/contextkeys.js'; import { Verbosity } from '../../../common/editor.js'; import { IHostService } from '../../../services/host/browser/host.js'; import { IWorkbenchLayoutService, Parts } from '../../../services/layout/browser/layoutService.js'; import { mainWindow } from '../../../../base/browser/window.js'; import { localize } from '../../../../nls.js'; -import { CLOSE_MODAL_EDITOR_COMMAND_ID, MOVE_MODAL_EDITOR_TO_MAIN_COMMAND_ID, TOGGLE_MODAL_EDITOR_MAXIMIZED_COMMAND_ID } from './editorCommands.js'; +import { Codicon } from '../../../../base/common/codicons.js'; +import { CLOSE_MODAL_EDITOR_COMMAND_ID, MOVE_MODAL_EDITOR_TO_MAIN_COMMAND_ID, NAVIGATE_MODAL_EDITOR_NEXT_COMMAND_ID, NAVIGATE_MODAL_EDITOR_PREVIOUS_COMMAND_ID, TOGGLE_MODAL_EDITOR_MAXIMIZED_COMMAND_ID } from './editorCommands.js'; +import { IModalEditorNavigation, IModalEditorPartOptions } from '../../../../platform/editor/common/editor.js'; const defaultModalEditorAllowableCommands = new Set([ 'workbench.action.quit', @@ -40,7 +43,9 @@ const defaultModalEditorAllowableCommands = new Set([ 'workbench.action.files.saveAll', CLOSE_MODAL_EDITOR_COMMAND_ID, MOVE_MODAL_EDITOR_TO_MAIN_COMMAND_ID, - TOGGLE_MODAL_EDITOR_MAXIMIZED_COMMAND_ID + TOGGLE_MODAL_EDITOR_MAXIMIZED_COMMAND_ID, + NAVIGATE_MODAL_EDITOR_PREVIOUS_COMMAND_ID, + NAVIGATE_MODAL_EDITOR_NEXT_COMMAND_ID, ]); export interface ICreateModalEditorPartResult { @@ -61,17 +66,50 @@ export class ModalEditorPart { ) { } - async create(): Promise { + async create(options?: IModalEditorPartOptions): Promise { const disposables = new DisposableStore(); - // Create modal container - const modalElement = $('.monaco-modal-editor-block.dimmed'); + // Modal container + const modalElement = $('.monaco-modal-editor-block'); this.layoutService.mainContainer.appendChild(modalElement); disposables.add(toDisposable(() => modalElement.remove())); + disposables.add(addDisposableListener(modalElement, EventType.MOUSE_DOWN, e => { + if (e.target === modalElement) { + EventHelper.stop(e, true); + + // Guide focus back into the modal when clicking outside modal + editorPartContainer.focus(); + } + })); + + disposables.add(addDisposableListener(modalElement, EventType.KEY_DOWN, e => { + const event = new StandardKeyboardEvent(e); + + // Close on Escape + if (event.equals(KeyCode.Escape)) { + EventHelper.stop(event, true); + + editorPart.close(); + } + + // Prevent unsupported commands + else { + const resolved = this.keybindingService.softDispatch(event, this.layoutService.mainContainer); + if (resolved.kind === ResultKind.KbFound && resolved.commandId) { + if ( + resolved.commandId.startsWith('workbench.') && + !defaultModalEditorAllowableCommands.has(resolved.commandId) + ) { + EventHelper.stop(event, true); + } + } + } + })); + const shadowElement = modalElement.appendChild($('.modal-editor-shadow')); - // Create editor part container + // Editor part container const titleId = 'modal-editor-title'; const editorPartContainer = $('.part.editor.modal-editor-part', { role: 'dialog', @@ -81,7 +119,7 @@ export class ModalEditorPart { }); shadowElement.appendChild(editorPartContainer); - // Create header with title and close button + // Header const headerElement = editorPartContainer.appendChild($('.modal-editor-header')); // Title element @@ -89,7 +127,35 @@ export class ModalEditorPart { titleElement.id = titleId; titleElement.textContent = ''; - // Action buttons + // Navigation widget + const navigationContainer = append(headerElement, $('div.modal-editor-navigation')); + hide(navigationContainer); + disposables.add(addDisposableListener(navigationContainer, EventType.DBLCLICK, e => EventHelper.stop(e, true))); + + const previousButton = disposables.add(new Button(navigationContainer, { title: localize('previousItem', "Previous") })); + previousButton.icon = Codicon.chevronLeft; + previousButton.element.classList.add('modal-editor-nav-button'); + disposables.add(previousButton.onDidClick(() => { + const navigation = editorPart.navigation; + if (navigation && navigation.current > 0) { + navigation.navigate(navigation.current - 1); + } + })); + + const navigationLabel = append(navigationContainer, $('span.modal-editor-nav-label')); + navigationLabel.setAttribute('aria-live', 'polite'); + + const nextButton = disposables.add(new Button(navigationContainer, { title: localize('nextItem', "Next") })); + nextButton.icon = Codicon.chevronRight; + nextButton.element.classList.add('modal-editor-nav-button'); + disposables.add(nextButton.onDidClick(() => { + const navigation = editorPart.navigation; + if (navigation && navigation.current < navigation.total - 1) { + navigation.navigate(navigation.current + 1); + } + })); + + // Toolbar const actionBarContainer = append(headerElement, $('div.modal-editor-action-container')); // Create the editor part @@ -98,10 +164,23 @@ export class ModalEditorPart { mainWindow.vscodeWindowId, this.editorPartsView, modalElement, + options, )); disposables.add(this.editorPartsView.registerPart(editorPart)); editorPart.create(editorPartContainer); + disposables.add(Event.once(editorPart.onWillClose)(() => disposables.dispose())); + disposables.add(Event.runAndSubscribe(editorPart.onDidChangeNavigation, ((navigation: IModalEditorNavigation | undefined) => { + if (navigation && navigation.total > 1) { + show(navigationContainer); + navigationLabel.textContent = localize('navigationCounter', "{0} of {1}", navigation.current + 1, navigation.total); + previousButton.enabled = navigation.current > 0; + nextButton.enabled = navigation.current < navigation.total - 1; + } else { + hide(navigationContainer); + } + }), editorPart.navigation)); + // Create scoped instantiation service const modalEditorService = this.editorService.createScoped(editorPart, disposables); const scopedInstantiationService = disposables.add(editorPart.scopedInstantiationService.createChild(new ServiceCollection( @@ -132,44 +211,6 @@ export class ModalEditorPart { editorPart.toggleMaximized(); })); - // Guide focus back into the modal when clicking outside modal - disposables.add(addDisposableListener(modalElement, EventType.MOUSE_DOWN, e => { - if (e.target === modalElement) { - EventHelper.stop(e, true); - - editorPartContainer.focus(); - } - })); - - // Block certain workbench commands from being dispatched while the modal is open - disposables.add(addDisposableListener(modalElement, EventType.KEY_DOWN, e => { - const event = new StandardKeyboardEvent(e); - - // Close on Escape - if (event.equals(KeyCode.Escape)) { - EventHelper.stop(event, true); - - editorPart.close(); - } - - // Prevent unsupported commands - else { - const resolved = this.keybindingService.softDispatch(event, this.layoutService.mainContainer); - if (resolved.kind === ResultKind.KbFound && resolved.commandId) { - if ( - resolved.commandId.startsWith('workbench.') && - !defaultModalEditorAllowableCommands.has(resolved.commandId) - ) { - EventHelper.stop(event, true); - } - } - } - })); - - // Handle close event from editor part - disposables.add(Event.once(editorPart.onWillClose)(() => { - disposables.dispose(); - })); // Layout the modal editor part const layoutModal = () => { @@ -231,9 +272,15 @@ class ModalEditorPartImpl extends EditorPart implements IModalEditorPart { private readonly _onDidChangeMaximized = this._register(new Emitter()); readonly onDidChangeMaximized = this._onDidChangeMaximized.event; + private readonly _onDidChangeNavigation = this._register(new Emitter()); + readonly onDidChangeNavigation = this._onDidChangeNavigation.event; + private _maximized = false; get maximized(): boolean { return this._maximized; } + private _navigation: IModalEditorNavigation | undefined; + get navigation(): IModalEditorNavigation | undefined { return this._navigation; } + private readonly optionsDisposable = this._register(new MutableDisposable()); private previousMainWindowActiveElement: Element | null = null; @@ -241,7 +288,8 @@ class ModalEditorPartImpl extends EditorPart implements IModalEditorPart { constructor( windowId: number, editorPartsView: IEditorPartsView, - private readonly modalElement: HTMLElement, + public readonly modalElement: HTMLElement, + options: IModalEditorPartOptions | undefined, @IInstantiationService instantiationService: IInstantiationService, @IThemeService themeService: IThemeService, @IConfigurationService configurationService: IConfigurationService, @@ -253,11 +301,9 @@ class ModalEditorPartImpl extends EditorPart implements IModalEditorPart { const id = ModalEditorPartImpl.COUNTER++; super(editorPartsView, `workbench.parts.modalEditor.${id}`, localize('modalEditorPart', "Modal Editor Area"), windowId, instantiationService, themeService, configurationService, storageService, layoutService, hostService, contextKeyService); - this.enforceModalPartOptions(); - } + this._navigation = options?.navigation; - getModalElement() { - return this.modalElement; + this.enforceModalPartOptions(); } override create(parent: HTMLElement, options?: object): void { @@ -270,6 +316,7 @@ class ModalEditorPartImpl extends EditorPart implements IModalEditorPart { const editorCount = this.groups.reduce((count, group) => count + group.count, 0); this.optionsDisposable.value = this.enforcePartOptions({ showTabs: editorCount > 1 ? 'multiple' : 'none', + enablePreview: true, closeEmptyGroups: true, tabActionCloseVisibility: editorCount > 1, editorActionsLocation: 'default', @@ -283,6 +330,12 @@ class ModalEditorPartImpl extends EditorPart implements IModalEditorPart { this.enforceModalPartOptions(); } + updateOptions(options?: IModalEditorPartOptions): void { + this._navigation = options?.navigation; + + this._onDidChangeNavigation.fire(options?.navigation); + } + toggleMaximized(): void { this._maximized = !this._maximized; @@ -297,6 +350,10 @@ class ModalEditorPartImpl extends EditorPart implements IModalEditorPart { isMaximizedContext.set(this._maximized); this._register(this.onDidChangeMaximized(maximized => isMaximizedContext.set(maximized))); + const hasNavigationContext = EditorPartModalNavigationContext.bindTo(this.scopedContextKeyService); + hasNavigationContext.set(!!this._navigation && this._navigation.total > 1); + this._register(this.onDidChangeNavigation(navigation => hasNavigationContext.set(!!navigation && navigation.total > 1))); + super.handleContextKeys(); } @@ -398,4 +455,10 @@ class ModalEditorPartImpl extends EditorPart implements IModalEditorPart { return result; } + + override dispose(): void { + this._navigation = undefined; // ensure to free the reference to the navigation closure + + super.dispose(); + } } diff --git a/src/vs/workbench/common/contextkeys.ts b/src/vs/workbench/common/contextkeys.ts index fad039e7689b1..50cc224cfc236 100644 --- a/src/vs/workbench/common/contextkeys.ts +++ b/src/vs/workbench/common/contextkeys.ts @@ -96,6 +96,7 @@ export const EditorPartMaximizedEditorGroupContext = new RawContextKey( export const EditorPartModalContext = new RawContextKey('editorPartModal', false, localize('editorPartModal', "Whether focus is in a modal editor part")); export const EditorPartModalMaximizedContext = new RawContextKey('editorPartModalMaximized', false, localize('editorPartModalMaximized', "Whether the modal editor part is maximized")); +export const EditorPartModalNavigationContext = new RawContextKey('editorPartModalNavigation', false, localize('editorPartModalNavigation', "Whether the modal editor part has navigation context")); // Editor Layout Context Keys export const EditorsVisibleContext = new RawContextKey('editorIsOpen', false, localize('editorIsOpen', "Whether an editor is open")); diff --git a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts index 459c99201fd27..e051ccfcc2bb6 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts @@ -657,7 +657,7 @@ configurationRegistry.registerConfiguration({ [ChatConfiguration.EditModeHidden]: { type: 'boolean', description: nls.localize('chat.editMode.hidden', "When enabled, hides the Edit mode from the chat mode picker."), - default: false, + default: true, tags: ['experimental'], experiment: { mode: 'auto' diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/modePickerActionItem.ts b/src/vs/workbench/contrib/chat/browser/widget/input/modePickerActionItem.ts index 363704772e72f..f6f9062405075 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/modePickerActionItem.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/modePickerActionItem.ts @@ -184,13 +184,21 @@ export class ModePickerActionItem extends ChatInputPickerActionViewItem { const target = mode.target.get(); return target === customAgentTarget || target === Target.Undefined; }); + const customModes = groupBy( + filteredCustomModes, + mode => isModeConsideredBuiltIn(mode, this._productService) ? 'builtin' : 'custom'); // Always include the default "Agent" option first const checked = currentMode.id === ChatMode.Agent.id; const defaultAction = { ...makeAction(ChatMode.Agent, ChatMode.Agent), checked }; - + defaultAction.category = builtInCategory; + const builtInActions = customModes.builtin?.map(mode => { + const action = makeActionFromCustomMode(mode, currentMode); + action.category = builtInCategory; + return action; + }) ?? []; // Add filtered custom modes - const customActions = filteredCustomModes.map(mode => makeActionFromCustomMode(mode, currentMode)); - return [defaultAction, ...customActions]; + const customActions = customModes.custom?.map(mode => makeActionFromCustomMode(mode, currentMode)) ?? []; + return [defaultAction, ...builtInActions, ...customActions]; } }; @@ -219,14 +227,19 @@ export class ModePickerActionItem extends ChatInputPickerActionViewItem { modes.custom, mode => isModeConsideredBuiltIn(mode, this._productService) ? 'builtin' : 'custom'); - const customBuiltinModeActions = customModes.builtin?.map(mode => { + const modeSupportsVSCode = (mode: IChatMode) => { + const target = mode.target.get(); + return target === Target.Undefined || target === Target.VSCode; + }; + + const customBuiltinModeActions = customModes.builtin?.filter(modeSupportsVSCode)?.map(mode => { const action = makeActionFromCustomMode(mode, currentMode); action.category = agentModeDisabledViaPolicy ? policyDisabledCategory : builtInCategory; return action; }) ?? []; customBuiltinModeActions.sort((a, b) => a.label.localeCompare(b.label)); - const customModeActions = customModes.custom?.map(mode => makeActionFromCustomMode(mode, currentMode)) ?? []; + const customModeActions = customModes.custom?.filter(modeSupportsVSCode)?.map(mode => makeActionFromCustomMode(mode, currentMode)) ?? []; customModeActions.sort((a, b) => a.label.localeCompare(b.label)); const orderedModes = coalesce([ diff --git a/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts b/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts index dd9ede29d6875..2842ef3b7d2bd 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts @@ -94,8 +94,8 @@ import { fromNow } from '../../../../base/common/date.js'; class NavBar extends Disposable { - private _onChange = this._register(new Emitter<{ id: string | null; focus: boolean }>()); - get onChange(): Event<{ id: string | null; focus: boolean }> { return this._onChange.event; } + private readonly _onChange = this._register(new Emitter<{ id: string | null; focus: boolean }>()); + readonly onChange = this._onChange.event; private _currentId: string | null = null; get currentId(): string | null { return this._currentId; } @@ -142,6 +142,11 @@ class NavBar extends Disposable { this._onChange.fire({ id, focus: !!focus }); this.actions.forEach(a => a.checked = a.id === id); } + + override dispose(): void { + this.clear(); + super.dispose(); + } } interface ILayoutParticipant { diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsViewer.ts b/src/vs/workbench/contrib/extensions/browser/extensionsViewer.ts index 282f5e496dde9..418cb12e5f66c 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsViewer.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsViewer.ts @@ -5,7 +5,7 @@ import * as dom from '../../../../base/browser/dom.js'; import { localize } from '../../../../nls.js'; -import { IDisposable, dispose, Disposable, DisposableStore, toDisposable, isDisposable } from '../../../../base/common/lifecycle.js'; +import { IDisposable, dispose, Disposable, DisposableStore, toDisposable, isDisposable, MutableDisposable } from '../../../../base/common/lifecycle.js'; import { Action, ActionRunner, IAction, Separator } from '../../../../base/common/actions.js'; import { IExtensionsWorkbenchService, IExtension, IExtensionsViewState } from '../common/extensions.js'; import { Event } from '../../../../base/common/event.js'; @@ -16,7 +16,8 @@ import { IContextKeyService } from '../../../../platform/contextkey/common/conte import { registerThemingParticipant, IColorTheme, ICssStyleCollector } from '../../../../platform/theme/common/themeService.js'; import { IAsyncDataSource, ITreeNode } from '../../../../base/browser/ui/tree/tree.js'; import { IListVirtualDelegate, IListRenderer, IListContextMenuEvent } from '../../../../base/browser/ui/list/list.js'; -import { CancellationToken } from '../../../../base/common/cancellation.js'; +import { CancellationToken, CancellationTokenSource } from '../../../../base/common/cancellation.js'; +import { IModalEditorPartOptions } from '../../../../platform/editor/common/editor.js'; import { isNonEmptyArray } from '../../../../base/common/arrays.js'; import { Delegate, Renderer } from './extensionsList.js'; import { listFocusForeground, listFocusBackground, foreground, editorBackground } from '../../../../platform/theme/common/colorRegistry.js'; @@ -35,6 +36,8 @@ import { INotificationService } from '../../../../platform/notification/common/n import { getLocationBasedViewColors } from '../../../browser/parts/views/viewPane.js'; import { DelayedPagedModel, IPagedModel } from '../../../../base/common/paging.js'; import { ExtensionIconWidget } from './extensionsWidgets.js'; +import { ILogService } from '../../../../platform/log/common/log.js'; +import { isCancellationError } from '../../../../base/common/errors.js'; function getAriaLabelForExtension(extension: IExtension | null): string { if (!extension) { @@ -51,6 +54,8 @@ export class ExtensionsList extends Disposable { readonly list: WorkbenchPagedList; private readonly contextMenuActionRunner = this._register(new ActionRunner()); + private readonly modalNavigationDisposable = this._register(new MutableDisposable()); + constructor( parent: HTMLElement, viewId: string, @@ -63,6 +68,7 @@ export class ExtensionsList extends Disposable { @IContextMenuService private readonly contextMenuService: IContextMenuService, @IContextKeyService private readonly contextKeyService: IContextKeyService, @IInstantiationService private readonly instantiationService: IInstantiationService, + @ILogService private readonly logService: ILogService ) { super(); this._register(this.contextMenuActionRunner.onDidRun(({ error }) => error && notificationService.error(error))); @@ -115,7 +121,17 @@ export class ExtensionsList extends Disposable { private openExtension(extension: IExtension, options: { sideByside?: boolean; preserveFocus?: boolean; pinned?: boolean }): void { extension = this.extensionsWorkbenchService.local.filter(e => areSameExtensions(e.identifier, extension.identifier))[0] || extension; - this.extensionsWorkbenchService.open(extension, options); + this.extensionsWorkbenchService.open(extension, { + ...options, + modal: options.sideByside ? undefined : buildModalNavigationForPagedList( + extension, + () => this.list.model, + (extA, extB) => areSameExtensions(extA.identifier, extB.identifier), + (ext, modal) => this.extensionsWorkbenchService.open(ext, { pinned: false, modal }), + this.modalNavigationDisposable, + this.logService + ), + }); } private async onContextMenu(e: IListContextMenuEvent): Promise { @@ -453,6 +469,80 @@ export async function getExtensions(extensions: string[], extensionsWorkbenchSer return result; } +/** + * Builds modal navigation options for navigating items in a paged list model. + */ +export function buildModalNavigationForPagedList( + openedItem: T, + getModel: () => IPagedModel | undefined, + isSame: (a: T, b: T) => boolean, + openItem: (item: T, modal: IModalEditorPartOptions) => void, + cancellationStore: MutableDisposable, + logService: ILogService +): IModalEditorPartOptions | undefined { + const model = getModel(); + if (!model) { + return undefined; + } + + const total = model.length; + if (total <= 1) { + return undefined; + } + + // Find the index of the opened item in the list + let current = -1; + for (let i = 0; i < total; i++) { + if (model.isResolved(i) && isSame(model.get(i), openedItem)) { + current = i; + break; + } + } + + if (current === -1) { + return undefined; + } + + const openAtIndex = (index: number, item: T) => { + const currentTotal = getModel()?.length ?? 0; + openItem(item, { navigation: { total: currentTotal, current: index, navigate } }); + }; + + let cts: CancellationTokenSource | undefined; + const navigate = (index: number) => { + cts?.cancel(); + cts = cancellationStore.value = new CancellationTokenSource(); + const token = cts.token; + + const currentModel = getModel(); + if (!currentModel || index < 0 || index >= currentModel.length) { + return; + } + + // Fast path: item already resolved + if (currentModel.isResolved(index)) { + openAtIndex(index, currentModel.get(index)); + } + + // Slow path: resolve the item first + else { + currentModel.resolve(index, token).then(item => { + if (token.isCancellationRequested) { + return; + } + + openAtIndex(index, item); + }, error => { + if (!isCancellationError(error)) { + logService.error(`Error while resolving item at index ${index} for modal navigation`, error); + } + }); + } + }; + + return { navigation: { total, current, navigate } }; +} + registerThemingParticipant((theme: IColorTheme, collector: ICssStyleCollector) => { const focusBackground = theme.getColor(listFocusBackground); if (focusBackground) { diff --git a/src/vs/workbench/contrib/mcp/browser/mcpServersView.ts b/src/vs/workbench/contrib/mcp/browser/mcpServersView.ts index 7f4dfea45e3d1..9e82f119e8830 100644 --- a/src/vs/workbench/contrib/mcp/browser/mcpServersView.ts +++ b/src/vs/workbench/contrib/mcp/browser/mcpServersView.ts @@ -9,7 +9,7 @@ import { ActionBar } from '../../../../base/browser/ui/actionbar/actionbar.js'; import { IListContextMenuEvent } from '../../../../base/browser/ui/list/list.js'; import { Emitter, Event } from '../../../../base/common/event.js'; import { createMarkdownCommandLink, MarkdownString } from '../../../../base/common/htmlContent.js'; -import { combinedDisposable, Disposable, DisposableStore, dispose, IDisposable, isDisposable } from '../../../../base/common/lifecycle.js'; +import { combinedDisposable, Disposable, DisposableStore, dispose, IDisposable, isDisposable, MutableDisposable } from '../../../../base/common/lifecycle.js'; import { DelayedPagedModel, IPagedModel, PagedModel, IterativePagedModel } from '../../../../base/common/paging.js'; import { localize, localize2 } from '../../../../nls.js'; import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; @@ -52,6 +52,8 @@ import { IMcpGalleryManifestService, McpGalleryManifestStatus } from '../../../. import { ProductQualityContext } from '../../../../platform/contextkey/common/contextkeys.js'; import { SeverityIcon } from '../../../../base/browser/ui/severityIcon/severityIcon.js'; import { IMarkdownRendererService } from '../../../../platform/markdown/browser/markdownRenderer.js'; +import { ILogService } from '../../../../platform/log/common/log.js'; +import { buildModalNavigationForPagedList } from '../../extensions/browser/extensionsViewer.js'; export interface McpServerListViewOptions { showWelcome?: boolean; @@ -81,6 +83,7 @@ export class McpServersListView extends AbstractExtensionsListView); this._register(Event.debounce(Event.filter(this.list.onDidOpen, e => e.element !== null), (_, event) => event, 75, true)(options => { - this.mcpWorkbenchService.open(options.element!, options.editorOptions); + this.mcpWorkbenchService.open(options.element!, { + ...options.editorOptions, + modal: options.sideBySide ? undefined : buildModalNavigationForPagedList( + options.element!, + () => this.list?.model, + (serverA, serverB) => serverA.id === serverB.id, + (server, modal) => this.mcpWorkbenchService.open(server, { pinned: false, modal }), + this.modalNavigationDisposable, + this.logService + ), + }); })); this._register(this.list.onContextMenu(e => this.onContextMenu(e), this)); diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/common/terminalSandboxService.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/common/terminalSandboxService.ts index c10004a8417f5..9b1da127a3529 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/common/terminalSandboxService.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/common/terminalSandboxService.ts @@ -160,7 +160,11 @@ export class TerminalSandboxService extends Disposable implements ITerminalSandb const allowedDomainsSet = new Set(networkSetting.allowedDomains ?? []); if (networkSetting.allowTrustedDomains) { for (const domain of this._trustedDomainService.trustedDomains) { - allowedDomainsSet.add(domain); + // Filter out sole wildcard '*' as sandbox runtime doesn't allow it + // Wildcards like '*.github.com' are OK + if (domain !== '*') { + allowedDomainsSet.add(domain); + } } } const allowedDomains = Array.from(allowedDomainsSet); diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/browser/terminalSandboxService.test.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/browser/terminalSandboxService.test.ts new file mode 100644 index 0000000000000..55901336b328d --- /dev/null +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/browser/terminalSandboxService.test.ts @@ -0,0 +1,257 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { strictEqual, ok } from 'assert'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/test/common/utils.js'; +import { TestInstantiationService } from '../../../../../../platform/instantiation/test/common/instantiationServiceMock.js'; +import { workbenchInstantiationService } from '../../../../../test/browser/workbenchTestServices.js'; +import { TerminalSandboxService } from '../../common/terminalSandboxService.js'; +import { IConfigurationService } from '../../../../../../platform/configuration/common/configuration.js'; +import { IFileService } from '../../../../../../platform/files/common/files.js'; +import { IEnvironmentService } from '../../../../../../platform/environment/common/environment.js'; +import { ILogService, NullLogService } from '../../../../../../platform/log/common/log.js'; +import { IRemoteAgentService } from '../../../../../services/remote/common/remoteAgentService.js'; +import { ITrustedDomainService } from '../../../../url/common/trustedDomainService.js'; +import { URI } from '../../../../../../base/common/uri.js'; +import { TerminalChatAgentToolsSettingId } from '../../common/terminalChatAgentToolsConfiguration.js'; +import { Event, Emitter } from '../../../../../../base/common/event.js'; +import { TestConfigurationService } from '../../../../../../platform/configuration/test/common/testConfigurationService.js'; +import { VSBuffer } from '../../../../../../base/common/buffer.js'; +import { OperatingSystem } from '../../../../../../base/common/platform.js'; +import { IRemoteAgentEnvironment } from '../../../../../../platform/remote/common/remoteAgentEnvironment.js'; + +suite('TerminalSandboxService - allowTrustedDomains', () => { + const store = ensureNoDisposablesAreLeakedInTestSuite(); + + let instantiationService: TestInstantiationService; + let configurationService: TestConfigurationService; + let trustedDomainService: MockTrustedDomainService; + let fileService: MockFileService; + let createdFiles: Map; + + class MockTrustedDomainService implements ITrustedDomainService { + _serviceBrand: undefined; + private _onDidChangeTrustedDomains = new Emitter(); + readonly onDidChangeTrustedDomains: Event = this._onDidChangeTrustedDomains.event; + trustedDomains: string[] = []; + isValid(_resource: URI): boolean { + return true; + } + } + + class MockFileService { + async createFile(uri: URI, content: VSBuffer): Promise { + const contentString = content.toString(); + createdFiles.set(uri.path, contentString); + return {}; + } + } + + class MockRemoteAgentService { + async getEnvironment(): Promise { + // Return a Linux environment to ensure tests pass on Windows + // (sandbox is not supported on Windows) + return { + os: OperatingSystem.Linux, + tmpDir: URI.file('/tmp'), + appRoot: URI.file('/app'), + pid: 1234, + connectionToken: 'test-token', + settingsPath: URI.file('/settings'), + mcpResource: URI.file('/mcp'), + logsPath: URI.file('/logs'), + extensionHostLogsPath: URI.file('/ext-logs'), + globalStorageHome: URI.file('/global'), + workspaceStorageHome: URI.file('/workspace'), + localHistoryHome: URI.file('/history'), + userHome: URI.file('/home/user'), + arch: 'x64', + marks: [], + useHostProxy: false, + profiles: { + all: [], + home: URI.file('/profiles') + }, + isUnsupportedGlibc: false + }; + } + } + + setup(() => { + createdFiles = new Map(); + instantiationService = workbenchInstantiationService({}, store); + configurationService = new TestConfigurationService(); + trustedDomainService = new MockTrustedDomainService(); + fileService = new MockFileService(); + + // Setup default configuration + configurationService.setUserConfiguration(TerminalChatAgentToolsSettingId.TerminalSandboxEnabled, true); + configurationService.setUserConfiguration(TerminalChatAgentToolsSettingId.TerminalSandboxNetwork, { + allowedDomains: [], + deniedDomains: [], + allowTrustedDomains: false + }); + + instantiationService.stub(IConfigurationService, configurationService); + instantiationService.stub(IFileService, fileService); + instantiationService.stub(IEnvironmentService, { + _serviceBrand: undefined, + tmpDir: URI.file('/tmp'), + execPath: '/usr/bin/node' + }); + instantiationService.stub(ILogService, new NullLogService()); + instantiationService.stub(IRemoteAgentService, new MockRemoteAgentService()); + instantiationService.stub(ITrustedDomainService, trustedDomainService); + }); + + test('should filter out sole wildcard (*) from trusted domains', async () => { + // Setup: Enable allowTrustedDomains and add * to trusted domains + configurationService.setUserConfiguration(TerminalChatAgentToolsSettingId.TerminalSandboxNetwork, { + allowedDomains: [], + deniedDomains: [], + allowTrustedDomains: true + }); + trustedDomainService.trustedDomains = ['*']; + + const sandboxService = store.add(instantiationService.createInstance(TerminalSandboxService)); + const configPath = await sandboxService.getSandboxConfigPath(); + + ok(configPath, 'Config path should be defined'); + const configContent = createdFiles.get(configPath); + ok(configContent, 'Config file should be created'); + + const config = JSON.parse(configContent); + strictEqual(config.network.allowedDomains.length, 0, 'Sole wildcard * should be filtered out'); + }); + + test('should allow wildcards with domains like *.github.com', async () => { + // Setup: Enable allowTrustedDomains and add *.github.com + configurationService.setUserConfiguration(TerminalChatAgentToolsSettingId.TerminalSandboxNetwork, { + allowedDomains: [], + deniedDomains: [], + allowTrustedDomains: true + }); + trustedDomainService.trustedDomains = ['*.github.com']; + + const sandboxService = store.add(instantiationService.createInstance(TerminalSandboxService)); + const configPath = await sandboxService.getSandboxConfigPath(); + + ok(configPath, 'Config path should be defined'); + const configContent = createdFiles.get(configPath); + ok(configContent, 'Config file should be created'); + + const config = JSON.parse(configContent); + strictEqual(config.network.allowedDomains.length, 1, 'Wildcard domain should be included'); + strictEqual(config.network.allowedDomains[0], '*.github.com', 'Wildcard domain should match'); + }); + + test('should combine trusted domains with configured allowedDomains, filtering out *', async () => { + // Setup: Enable allowTrustedDomains with multiple domains including * + configurationService.setUserConfiguration(TerminalChatAgentToolsSettingId.TerminalSandboxNetwork, { + allowedDomains: ['example.com'], + deniedDomains: [], + allowTrustedDomains: true + }); + trustedDomainService.trustedDomains = ['*', '*.github.com', 'microsoft.com']; + + const sandboxService = store.add(instantiationService.createInstance(TerminalSandboxService)); + const configPath = await sandboxService.getSandboxConfigPath(); + + ok(configPath, 'Config path should be defined'); + const configContent = createdFiles.get(configPath); + ok(configContent, 'Config file should be created'); + + const config = JSON.parse(configContent); + strictEqual(config.network.allowedDomains.length, 3, 'Should have 3 domains (excluding *)'); + ok(config.network.allowedDomains.includes('example.com'), 'Should include configured domain'); + ok(config.network.allowedDomains.includes('*.github.com'), 'Should include wildcard domain'); + ok(config.network.allowedDomains.includes('microsoft.com'), 'Should include microsoft.com'); + ok(!config.network.allowedDomains.includes('*'), 'Should not include sole wildcard'); + }); + + test('should not include trusted domains when allowTrustedDomains is false', async () => { + // Setup: Disable allowTrustedDomains + configurationService.setUserConfiguration(TerminalChatAgentToolsSettingId.TerminalSandboxNetwork, { + allowedDomains: ['example.com'], + deniedDomains: [], + allowTrustedDomains: false + }); + trustedDomainService.trustedDomains = ['*', '*.github.com']; + + const sandboxService = store.add(instantiationService.createInstance(TerminalSandboxService)); + const configPath = await sandboxService.getSandboxConfigPath(); + + ok(configPath, 'Config path should be defined'); + const configContent = createdFiles.get(configPath); + ok(configContent, 'Config file should be created'); + + const config = JSON.parse(configContent); + strictEqual(config.network.allowedDomains.length, 1, 'Should only have configured domain'); + strictEqual(config.network.allowedDomains[0], 'example.com', 'Should only include example.com'); + }); + + test('should deduplicate domains when combining sources', async () => { + // Setup: Same domain in both sources + configurationService.setUserConfiguration(TerminalChatAgentToolsSettingId.TerminalSandboxNetwork, { + allowedDomains: ['github.com', '*.github.com'], + deniedDomains: [], + allowTrustedDomains: true + }); + trustedDomainService.trustedDomains = ['*.github.com', 'github.com']; + + const sandboxService = store.add(instantiationService.createInstance(TerminalSandboxService)); + const configPath = await sandboxService.getSandboxConfigPath(); + + ok(configPath, 'Config path should be defined'); + const configContent = createdFiles.get(configPath); + ok(configContent, 'Config file should be created'); + + const config = JSON.parse(configContent); + strictEqual(config.network.allowedDomains.length, 2, 'Should have 2 unique domains'); + ok(config.network.allowedDomains.includes('github.com'), 'Should include github.com'); + ok(config.network.allowedDomains.includes('*.github.com'), 'Should include *.github.com'); + }); + + test('should handle empty trusted domains list', async () => { + // Setup: Empty trusted domains + configurationService.setUserConfiguration(TerminalChatAgentToolsSettingId.TerminalSandboxNetwork, { + allowedDomains: ['example.com'], + deniedDomains: [], + allowTrustedDomains: true + }); + trustedDomainService.trustedDomains = []; + + const sandboxService = store.add(instantiationService.createInstance(TerminalSandboxService)); + const configPath = await sandboxService.getSandboxConfigPath(); + + ok(configPath, 'Config path should be defined'); + const configContent = createdFiles.get(configPath); + ok(configContent, 'Config file should be created'); + + const config = JSON.parse(configContent); + strictEqual(config.network.allowedDomains.length, 1, 'Should have only configured domain'); + strictEqual(config.network.allowedDomains[0], 'example.com', 'Should only include example.com'); + }); + + test('should handle only * in trusted domains', async () => { + // Setup: Only * in trusted domains (edge case) + configurationService.setUserConfiguration(TerminalChatAgentToolsSettingId.TerminalSandboxNetwork, { + allowedDomains: [], + deniedDomains: [], + allowTrustedDomains: true + }); + trustedDomainService.trustedDomains = ['*']; + + const sandboxService = store.add(instantiationService.createInstance(TerminalSandboxService)); + const configPath = await sandboxService.getSandboxConfigPath(); + + ok(configPath, 'Config path should be defined'); + const configContent = createdFiles.get(configPath); + ok(configContent, 'Config file should be created'); + + const config = JSON.parse(configContent); + strictEqual(config.network.allowedDomains.length, 0, 'Should have no domains (* filtered out)'); + }); +}); diff --git a/src/vs/workbench/contrib/webview/browser/overlayWebview.ts b/src/vs/workbench/contrib/webview/browser/overlayWebview.ts index b6e0bc187fc52..00fc64dbb82f6 100644 --- a/src/vs/workbench/contrib/webview/browser/overlayWebview.ts +++ b/src/vs/workbench/contrib/webview/browser/overlayWebview.ts @@ -110,7 +110,7 @@ export class OverlayWebview extends Disposable implements IOverlayWebview { // Webviews cannot be reparented in the dom as it will destroy their contents. // Mount them to a high level node to avoid this depending on the active container. - const modalEditorContainer = this._editorGroupsService.activeModalEditorPart?.getModalElement(); + const modalEditorContainer = this._editorGroupsService.activeModalEditorPart?.modalElement; let root: HTMLElement; if (isHTMLElement(modalEditorContainer)) { root = modalEditorContainer; diff --git a/src/vs/workbench/contrib/webviewPanel/browser/webviewEditor.ts b/src/vs/workbench/contrib/webviewPanel/browser/webviewEditor.ts index 62c9a4bb88c81..b6bdf6eef7393 100644 --- a/src/vs/workbench/contrib/webviewPanel/browser/webviewEditor.ts +++ b/src/vs/workbench/contrib/webviewPanel/browser/webviewEditor.ts @@ -195,7 +195,7 @@ export class WebviewEditor extends EditorPane { return; } - const modalEditorContainer = this._editorGroupsService.activeModalEditorPart?.getModalElement(); + const modalEditorContainer = this._editorGroupsService.activeModalEditorPart?.modalElement; let clippingContainer: HTMLElement | undefined; if (isHTMLElement(modalEditorContainer)) { clippingContainer = modalEditorContainer; diff --git a/src/vs/workbench/services/editor/common/editorGroupFinder.ts b/src/vs/workbench/services/editor/common/editorGroupFinder.ts index 825061a3663c8..f46747726b722 100644 --- a/src/vs/workbench/services/editor/common/editorGroupFinder.ts +++ b/src/vs/workbench/services/editor/common/editorGroupFinder.ts @@ -119,16 +119,13 @@ function doFindGroup(input: EditorInputWithOptions | IUntypedEditorInput, prefer // Group: Aux Window else if (preferredGroup === AUX_WINDOW_GROUP) { - group = editorGroupService.createAuxiliaryEditorPart({ - bounds: options?.auxiliary?.bounds, - compact: options?.auxiliary?.compact, - alwaysOnTop: options?.auxiliary?.alwaysOnTop - }).then(group => group.activeGroup); + group = editorGroupService.createAuxiliaryEditorPart(options?.auxiliary) + .then(group => group.activeGroup); } // Group: Modal (gated behind a setting) else if (preferredGroup === MODAL_GROUP && configurationService.getValue('workbench.editor.allowOpenInModalEditor')) { - group = editorGroupService.createModalEditorPart() + group = editorGroupService.createModalEditorPart(options?.modal) .then(part => part.activeGroup); } diff --git a/src/vs/workbench/services/editor/common/editorGroupsService.ts b/src/vs/workbench/services/editor/common/editorGroupsService.ts index d87d8724ebd32..9fcff97208a66 100644 --- a/src/vs/workbench/services/editor/common/editorGroupsService.ts +++ b/src/vs/workbench/services/editor/common/editorGroupsService.ts @@ -7,7 +7,7 @@ import { Event } from '../../../../base/common/event.js'; import { IInstantiationService, createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; import { IEditorPane, GroupIdentifier, EditorInputWithOptions, CloseDirection, IEditorPartOptions, IEditorPartOptionsChangeEvent, EditorsOrder, IVisibleEditorPane, IEditorCloseEvent, IUntypedEditorInput, isEditorInput, IEditorWillMoveEvent, IMatchEditorOptions, IActiveEditorChangeEvent, IFindEditorOptions, IToolbarActions } from '../../../common/editor.js'; import { EditorInput } from '../../../common/editor/editorInput.js'; -import { IEditorOptions } from '../../../../platform/editor/common/editor.js'; +import { IEditorOptions, IModalEditorNavigation, IModalEditorPartOptions } from '../../../../platform/editor/common/editor.js'; import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; import { IDimension } from '../../../../editor/common/core/2d/dimension.js'; import { DisposableStore, IDisposable } from '../../../../base/common/lifecycle.js'; @@ -524,9 +524,9 @@ export interface IAuxiliaryEditorPart extends IEditorPart { export interface IModalEditorPart extends IEditorPart { /** - * Fired when this modal editor part is about to close. + * Modal container of the editor part. */ - readonly onWillClose: Event; + readonly modalElement: unknown /* HTMLElement */; /** * Whether the modal editor part is currently maximized. @@ -544,9 +544,19 @@ export interface IModalEditorPart extends IEditorPart { toggleMaximized(): void; /** - * Modal container of the editor part. + * The current navigation context, if any. + */ + readonly navigation: IModalEditorNavigation | undefined; + + /** + * Update options for the modal editor part. + */ + updateOptions(options?: IModalEditorPartOptions): void; + + /** + * Fired when this modal editor part is about to close. */ - getModalElement(): unknown /* HTMLElement */; + readonly onWillClose: Event; /** * Close this modal editor part after moving all @@ -631,7 +641,7 @@ export interface IEditorGroupsService extends IEditorGroupsContainer { * If a modal part already exists, it will be returned * instead of creating a new one. */ - createModalEditorPart(): Promise; + createModalEditorPart(options?: IModalEditorPartOptions): Promise; /** * The currently active modal editor part, if any. diff --git a/src/vs/workbench/test/browser/parts/editor/modalEditorNavigation.test.ts b/src/vs/workbench/test/browser/parts/editor/modalEditorNavigation.test.ts new file mode 100644 index 0000000000000..fa1d60c8537d0 --- /dev/null +++ b/src/vs/workbench/test/browser/parts/editor/modalEditorNavigation.test.ts @@ -0,0 +1,149 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import assert from 'assert'; +import { Emitter } from '../../../../../base/common/event.js'; +import { DisposableStore } from '../../../../../base/common/lifecycle.js'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js'; +import { IModalEditorNavigation, IModalEditorPartOptions } from '../../../../../platform/editor/common/editor.js'; + +/** + * Simple test harness that mimics the ModalEditorPartImpl navigation behavior + * without requiring the full editor part infrastructure. + */ +class TestModalEditorNavigationHost { + + private readonly _onDidChangeNavigation = new Emitter(); + readonly onDidChangeNavigation = this._onDidChangeNavigation.event; + + private _navigation: IModalEditorNavigation | undefined; + get navigation(): IModalEditorNavigation | undefined { return this._navigation; } + + updateOptions(options: IModalEditorPartOptions): void { + this._navigation = options.navigation; + this._onDidChangeNavigation.fire(options.navigation); + } + + dispose(): void { + this._onDidChangeNavigation.dispose(); + } +} + +suite('Modal Editor Navigation', () => { + + const disposables = new DisposableStore(); + + teardown(() => disposables.clear()); + + ensureNoDisposablesAreLeakedInTestSuite(); + + test('updateOptions sets navigation and fires event', () => { + const host = new TestModalEditorNavigationHost(); + disposables.add({ dispose: () => host.dispose() }); + + const events: (IModalEditorNavigation | undefined)[] = []; + disposables.add(host.onDidChangeNavigation(ctx => events.push(ctx))); + + const nav: IModalEditorNavigation = { + total: 10, + current: 3, + navigate: () => { } + }; + + host.updateOptions({ navigation: nav }); + + assert.strictEqual(host.navigation, nav); + assert.deepStrictEqual(events, [nav]); + }); + + test('updateOptions with undefined navigation clears navigation', () => { + const host = new TestModalEditorNavigationHost(); + disposables.add({ dispose: () => host.dispose() }); + + const events: (IModalEditorNavigation | undefined)[] = []; + disposables.add(host.onDidChangeNavigation(ctx => events.push(ctx))); + + const nav: IModalEditorNavigation = { + total: 5, + current: 0, + navigate: () => { } + }; + + host.updateOptions({ navigation: nav }); + host.updateOptions({ navigation: undefined }); + + assert.strictEqual(host.navigation, undefined); + assert.deepStrictEqual(events, [nav, undefined]); + }); + + test('navigate callback updates context', () => { + const host = new TestModalEditorNavigationHost(); + disposables.add({ dispose: () => host.dispose() }); + + const navigatedIndices: number[] = []; + + const navigate = (index: number) => { + navigatedIndices.push(index); + // Simulates what real navigation does: update the context with new index + host.updateOptions({ navigation: { total: 10, current: index, navigate } }); + }; + + host.updateOptions({ navigation: { total: 10, current: 0, navigate } }); + + // Navigate forward + host.navigation!.navigate(1); + assert.strictEqual(host.navigation!.current, 1); + + host.navigation!.navigate(5); + assert.strictEqual(host.navigation!.current, 5); + + assert.deepStrictEqual(navigatedIndices, [1, 5]); + }); + + test('navigation boundary conditions', () => { + const host = new TestModalEditorNavigationHost(); + disposables.add({ dispose: () => host.dispose() }); + + const navigate = (index: number) => { + if (index >= 0 && index < 3) { + host.updateOptions({ navigation: { total: 3, current: index, navigate } }); + } + }; + + host.updateOptions({ navigation: { total: 3, current: 0, navigate } }); + + // At first item + assert.strictEqual(host.navigation!.current, 0); + assert.ok(host.navigation!.current <= 0); // previous disabled + + // Navigate to last + host.navigation!.navigate(2); + assert.strictEqual(host.navigation!.current, 2); + assert.ok(host.navigation!.current >= host.navigation!.total - 1); // next disabled + + // Navigate back to middle + host.navigation!.navigate(1); + assert.strictEqual(host.navigation!.current, 1); + }); + + test('navigation context fires multiple events', () => { + const host = new TestModalEditorNavigationHost(); + disposables.add({ dispose: () => host.dispose() }); + + let eventCount = 0; + disposables.add(host.onDidChangeNavigation(() => eventCount++)); + + const navigate = (index: number) => { + host.updateOptions({ navigation: { total: 5, current: index, navigate } }); + }; + + host.updateOptions({ navigation: { total: 5, current: 0, navigate } }); + host.navigation!.navigate(1); + host.navigation!.navigate(2); + host.updateOptions({ navigation: undefined }); + + assert.strictEqual(eventCount, 4); // initial + 2 navigates + clear + }); +});