diff --git a/.yarnrc b/.yarnrc index 8675f7ab83c..dc10cd8fae6 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 4fa5946a867..82db58aa7c8 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 35feee9b876..72d5349d425 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 654a4445848..3c804f13816 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 ffbd6ae9edd..a965cfcdb9f 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", @@ -789,8 +790,6 @@ "--vscode-hover-maxWidth", "--vscode-hover-sourceWhiteSpace", "--vscode-hover-whiteSpace", - "--vscode-inline-chat-cropped", - "--vscode-inline-chat-expanded", "--vscode-inline-chat-quick-voice-height", "--vscode-inline-chat-quick-voice-width", "--vscode-editor-dictation-widget-height", @@ -827,4 +826,4 @@ "--zoom-factor", "--test-bar-width" ] -} \ No newline at end of file +} diff --git a/cgmanifest.json b/cgmanifest.json index 4b4d49573e4..e0a02fcc97d 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 ee7117330be..2ee4f73c919 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 8815e2df40c..534c1556763 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 5167b1eb95e..3aae16f6baf 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 { +class GitIncomingChangesFileDecorationProvider implements FileDecorationProvider { -// private readonly _onDidChangeDecorations = new EventEmitter(); -// readonly onDidChangeFileDecorations: Event = this._onDidChangeDecorations.event; + private readonly _onDidChangeDecorations = new EventEmitter(); + readonly onDidChangeFileDecorations: Event = this._onDidChangeDecorations.event; -// private decorations = new Map(); -// private readonly disposables: Disposable[] = []; + 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); -// } + 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()])); + 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))); -// } + 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 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; + private async getIncomingChanges(): Promise { + try { + const historyProvider = this.repository.historyProvider; + const currentHistoryItemGroup = historyProvider.currentHistoryItemGroup; -// if (!currentHistoryItemGroup?.base) { -// return []; -// } + if (!currentHistoryItemGroup?.base) { + return []; + } -// const ancestor = await historyProvider.resolveHistoryItemGroupCommonAncestor(currentHistoryItemGroup.id, currentHistoryItemGroup.base.id); -// if (!ancestor) { -// 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 []; -// } -// } + 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()); -// } + provideFileDecoration(uri: Uri): FileDecoration | undefined { + return this.decorations.get(uri.toString()); + } -// dispose(): void { -// dispose(this.disposables); -// } -// } + 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 3ba3dfc006a..f60b7233499 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 3641ffb3a36..7498a2b2202 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 71aa17bd5cc..6c9b1f20294 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 0729c4c5077..af2cf22724f 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 f7c87fbf9fa..f78f494d727 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 ce81dcb4c9e..ceb081403dc 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 79d66e32dda..d57ebf80834 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 dc5948314d9..9f5d76f5ac3 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 e9323fc9c43..b0b56a3fdd9 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 6fb0262e132..1a07b475074 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 f9142db514f..2afb762ff12 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 a3af2b99fe4..2dcfab99b56 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 e891be054dd..377df190971 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 1af0f2be779..b69b9a96e3f 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-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.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-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.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-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.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-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.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-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.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/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.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/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 bcd7160cadc..b068bf655bb 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-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.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-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.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-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.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-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.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-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.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/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.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/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.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/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 82459f97178..ff113c9baa9 100644 --- a/src/vs/base/browser/dom.ts +++ b/src/vs/base/browser/dom.ts @@ -921,13 +921,6 @@ export function getActiveWindow(): CodeWindow { return (document.defaultView?.window ?? mainWindow) as CodeWindow; } -export function focusWindow(element: Node): void { - const window = getWindow(element); - if (!window.document.hasFocus()) { - window.focus(); - } -} - const globalStylesheets = new Map>(); export function isGlobalStylesheet(node: Node): boolean { diff --git a/src/vs/base/browser/ui/hover/updatableHoverWidget.ts b/src/vs/base/browser/ui/hover/updatableHoverWidget.ts index c3c505cef39..d36ebab0d95 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 e4c89dd3aff..0a06ece485f 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 0cde23be0e8..6919e4934a2 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 a350213cc76..95886fa6017 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 c5466e20b45..8548fa7ce4d 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 067f09fd0bd..56623913d7d 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 b91367f1fc2..aea83578ee0 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 3c0de38db43..81faf87e87b 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 7499fa8e0bc..417d75045a3 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 57f11ed3e4e..4d157bf5788 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 8b9790bce21..cae43742902 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 8466fbf9f99..5ade938e7c2 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 c094df337af..3391b58aa2e 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 d9ee1ecd2c6..5323c6c6340 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 d2689b002d7..0468f46d5d5 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; + 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(cue: Sound, allowManyInParallel?: boolean): Promise; - playSignalLoop(cue: AccessibilitySignal, milliseconds: number): IDisposable; + 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 00000000000..3bfbced34a3 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 d0cedb8d962..f7c4a89d735 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 8833103e59c..1bec23931b3 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 09a2b1dddcd..30c4f1c51eb 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 c4403134b93..b5f0317af55 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 4fcf403d999..fddaf329af0 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 4ac28d17b5f..1174b510b51 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 bb2828a935f..3d7c9420e50 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 1767e8931cb..7c40a4af114 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 3dd6ec94d02..0a60a0b4556 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 acde1e461d1..86160c9e26a 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; + const buttonTrigger = async (button: IQuickInputButton, item: T | IPickerQuickAccessSeparator) => { + if (typeof item.trigger !== 'function') { + return; + } - if (token.isCancellationRequested) { - return; - } + 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; - 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; + 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 73c3a23a305..2bfd12c91cb 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 d5a23ae66e8..10529b5b903 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 4ccce2c7120..ef907680e5c 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 66defd005d1..087f5858b48 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/terminal.ts b/src/vs/platform/terminal/common/terminal.ts index 8a51aaa3ae1..1e0dfb3581f 100644 --- a/src/vs/platform/terminal/common/terminal.ts +++ b/src/vs/platform/terminal/common/terminal.ts @@ -122,6 +122,7 @@ export const enum TerminalSettingId { StickyScrollEnabled = 'terminal.integrated.stickyScroll.enabled', StickyScrollMaxLineCount = 'terminal.integrated.stickyScroll.maxLineCount', MouseWheelZoom = 'terminal.integrated.mouseWheelZoom', + ExperimentalInlineChat = 'workbench.action.terminal.experimentalInlineChat', // Debug settings that are hidden from user diff --git a/src/vs/platform/terminal/common/terminalEnvironment.ts b/src/vs/platform/terminal/common/terminalEnvironment.ts index 38e8fa2c669..d2925819862 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 4cc8994bd01..87c40596916 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; } @@ -63,7 +64,7 @@ export type Disabled = { type: StateType.Disabled; reason: DisablementReason }; export type Idle = { type: StateType.Idle; updateType: UpdateType; error?: string }; export type CheckingForUpdates = { type: StateType.CheckingForUpdates; explicit: boolean }; export type AvailableForDownload = { type: StateType.AvailableForDownload; update: IUpdate }; -export type Downloading = { type: StateType.Downloading; update: IUpdate }; +export type Downloading = { type: StateType.Downloading }; export type Downloaded = { type: StateType.Downloaded; update: IUpdate }; export type Updating = { type: StateType.Updating; update: IUpdate }; export type Ready = { type: StateType.Ready; update: IUpdate }; @@ -76,7 +77,7 @@ export const State = { Idle: (updateType: UpdateType, error?: string) => ({ type: StateType.Idle, updateType, error }) as Idle, CheckingForUpdates: (explicit: boolean) => ({ type: StateType.CheckingForUpdates, explicit } as CheckingForUpdates), AvailableForDownload: (update: IUpdate) => ({ type: StateType.AvailableForDownload, update } as AvailableForDownload), - Downloading: (update: IUpdate) => ({ type: StateType.Downloading, update } as Downloading), + Downloading: { type: StateType.Downloading } as Downloading, Downloaded: (update: IUpdate) => ({ type: StateType.Downloaded, update } as Downloaded), Updating: (update: IUpdate) => ({ type: StateType.Updating, update } as Updating), Ready: (update: IUpdate) => ({ type: StateType.Ready, update } as Ready), diff --git a/src/vs/platform/update/electron-main/updateService.darwin.ts b/src/vs/platform/update/electron-main/updateService.darwin.ts index 329488abb51..27a2fcbeadf 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'); } + @memoize private get onRawUpdateDownloaded(): Event { return Event.fromNodeEventEmitter(electron.autoUpdater, 'update-downloaded', (_, releaseNotes, version, timestamp) => ({ version, productVersion: version, timestamp })); } constructor( @ILifecycleMainService lifecycleMainService: ILifecycleMainService, @@ -96,12 +96,12 @@ export class DarwinUpdateService extends AbstractUpdateService implements IRelau electron.autoUpdater.checkForUpdates(); } - private onUpdateAvailable(update: IUpdate): void { + private onUpdateAvailable(): void { if (this.state.type !== StateType.CheckingForUpdates) { return; } - this.setState(State.Downloading(update)); + this.setState(State.Downloading); } private onUpdateDownloaded(update: IUpdate): void { diff --git a/src/vs/platform/update/electron-main/updateService.win32.ts b/src/vs/platform/update/electron-main/updateService.win32.ts index ff8bbb0e559..4c49a758185 100644 --- a/src/vs/platform/update/electron-main/updateService.win32.ts +++ b/src/vs/platform/update/electron-main/updateService.win32.ts @@ -135,7 +135,7 @@ export class Win32UpdateService extends AbstractUpdateService implements IRelaun return Promise.resolve(null); } - this.setState(State.Downloading(update)); + this.setState(State.Downloading); return this.cleanup(update.version).then(() => { return this.getUpdatePackagePath(update.version).then(updatePackagePath => { @@ -153,15 +153,13 @@ export class Win32UpdateService extends AbstractUpdateService implements IRelaun .then(() => updatePackagePath); }); }).then(packagePath => { - const fastUpdatesEnabled = this.configurationService.getValue('update.enableWindowsBackgroundUpdates'); - this.availableUpdate = { packagePath }; + this.setState(State.Downloaded(update)); + const fastUpdatesEnabled = this.configurationService.getValue('update.enableWindowsBackgroundUpdates'); if (fastUpdatesEnabled) { if (this.productService.target === 'user') { this.doApplyUpdate(); - } else { - this.setState(State.Downloaded(update)); } } else { this.setState(State.Ready(update)); @@ -209,7 +207,7 @@ export class Win32UpdateService extends AbstractUpdateService implements IRelaun } protected override async doApplyUpdate(): Promise { - if (this.state.type !== StateType.Downloaded && this.state.type !== StateType.Downloading) { + if (this.state.type !== StateType.Downloaded) { return Promise.resolve(undefined); } @@ -273,14 +271,13 @@ export class Win32UpdateService extends AbstractUpdateService implements IRelaun const fastUpdatesEnabled = this.configurationService.getValue('update.enableWindowsBackgroundUpdates'); const update: IUpdate = { version: 'unknown', productVersion: 'unknown' }; - this.setState(State.Downloading(update)); + this.setState(State.Downloading); this.availableUpdate = { packagePath }; + this.setState(State.Downloaded(update)); if (fastUpdatesEnabled) { if (this.productService.target === 'user') { this.doApplyUpdate(); - } else { - this.setState(State.Downloaded(update)); } } else { this.setState(State.Ready(update)); diff --git a/src/vs/platform/utilityProcess/electron-main/utilityProcess.ts b/src/vs/platform/utilityProcess/electron-main/utilityProcess.ts index 6bd1d52b986..8386e94dab2 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 807bb601136..e46474de06c 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 caec44f7f3a..657d3e8238a 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 22c82811bb4..b3ebdd940c3 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 c7155e716a9..1a4d657cd94 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 c143be65078..445e29220e7 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 3178df6d093..94641115abb 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 80d13f11362..b7ecbfdb9a2 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 a4713d12239..9a886a6713a 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 da678cb6efb..9ac80463738 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 dda01854f21..fb881d05c4f 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 d125a8b8f79..036d64b14d2 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 8166c1e7d33..7a4c69dc8f3 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 { - const agent = this._agents.get(handle); - if (!agent) { - // this is OK, the agent might have disposed while the request was in flight - return []; - } - - 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 { + async $provideFollowups(request: IChatAgentRequest, handle: number, result: IChatAgentResult, context: { history: IChatAgentHistoryEntryDto[] }, token: CancellationToken): Promise { const agent = this._agents.get(handle); if (!agent) { return Promise.resolve([]); } + const convertedHistory = await this.prepareHistoryTurns(agent.id, context); + 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 d6bc4f793aa..1c532bcc8ee 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 2cc7a200edc..8f74a0a4b69 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 f2a201e9e06..998e624a195 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 68cbe330492..13077072ff3 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 2e1ff171d1f..8660835e2d6 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 f38194be406..995d4f8a17b 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 5e4e8ed1510..dd77886bbf0 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 59eba8e11ff..d56a31212ed 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 7d0b333bece..d5e1a7f3391 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 797a1713dd8..9e397b31a21 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 7ae271c65cc..48278fe16b9 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. @@ -197,7 +193,10 @@ export abstract class MultiWindowParts extends Compo registerPart(part: T): IDisposable { this._parts.add(part); - return this._register(toDisposable(() => this.unregisterPart(part))); + return this._register(toDisposable(() => { + this.unregisterPart(part); + part = undefined!; // helps to avoid a memory leak with closures where part is captured + })); } protected unregisterPart(part: T): void { diff --git a/src/vs/workbench/browser/parts/banner/bannerPart.ts b/src/vs/workbench/browser/parts/banner/bannerPart.ts index 3725e594c97..91c8e9902bb 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 bcd00491191..f4314ae0dbb 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 cba829c7be5..52a01908818 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 ddcce4134c0..66940e29b77 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 faddd4e7b06..69bfb90f5d4 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 0f26fa1aa20..7a73fe46fc5 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 1094f315842..28e059443e7 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 a42032f3c16..7a7691b3850 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/editorStatus.ts b/src/vs/workbench/browser/parts/editor/editorStatus.ts index d34a58122cd..acf60d208b5 100644 --- a/src/vs/workbench/browser/parts/editor/editorStatus.ts +++ b/src/vs/workbench/browser/parts/editor/editorStatus.ts @@ -366,7 +366,7 @@ class EditorStatus extends Disposable { } private registerCommands(): void { - CommandsRegistry.registerCommand({ id: `changeEditorIndentation${this.targetWindowId}`, handler: () => this.showIndentationPicker() }); + this._register(CommandsRegistry.registerCommand({ id: `changeEditorIndentation${this.targetWindowId}`, handler: () => this.showIndentationPicker() })); } private async showIndentationPicker(): Promise { diff --git a/src/vs/workbench/browser/parts/editor/editorWithViewState.ts b/src/vs/workbench/browser/parts/editor/editorWithViewState.ts index f01bedd8f59..e31756007e9 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 1a6b5ca985e..24d85415eb8 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 becfd13cac2..c3d7a0cce9d 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 21429e37334..3c298c26b1e 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 563f12a1be7..f1b5b3a91a8 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 7a16fa667b8..0a3b885e01d 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 3490432e312..8301e27c643 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/panel/media/panelpart.css b/src/vs/workbench/browser/parts/panel/media/panelpart.css index 4ccb53b0c80..7b17465b2d9 100644 --- a/src/vs/workbench/browser/parts/panel/media/panelpart.css +++ b/src/vs/workbench/browser/parts/panel/media/panelpart.css @@ -35,9 +35,9 @@ border-right-width: 0; /* no border when main editor area is hiden */ } -.monaco-workbench .part.panel > .content .monaco-editor, -.monaco-workbench .part.panel > .content .monaco-editor .margin, -.monaco-workbench .part.panel > .content .monaco-editor .monaco-editor-background { +.monaco-workbench .part.panel > .content .monaco-editor:not(.ignore-panel-bg), +.monaco-workbench .part.panel > .content .monaco-editor:not(.ignore-panel-bg) .margin, +.monaco-workbench .part.panel > .content .monaco-editor:not(.ignore-panel-bg) .monaco-editor-background { background-color: var(--vscode-panel-background); } diff --git a/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts b/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts index 664a333bb16..06548d13508 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 548f4d36caf..110d04fba11 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 3ec39eeafc2..e025f5c4d6c 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 15c4a1aae00..130fd60fe85 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 84e11baf381..6ef0b18cdb1 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 17354e5f403..121f3528fe4 100644 --- a/src/vs/workbench/browser/window.ts +++ b/src/vs/workbench/browser/window.ts @@ -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,7 +40,8 @@ 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(); @@ -52,26 +54,49 @@ export abstract class BaseWindow extends Disposable { //#region focus handling in multi-window applications protected enableWindowFocusOnElementFocus(targetWindow: CodeWindow): void { - const originalFocus = HTMLElement.prototype.focus; + const originalFocus = targetWindow.HTMLElement.prototype.focus; + const that = this; 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 + that.onElementFocus(getWindow(this)); // Pass to original focus() method originalFocus.apply(this, [options]); }; } + private onElementFocus(targetWindow: CodeWindow): void { + const activeWindow = getActiveWindow(); + if (activeWindow !== targetWindow && activeWindow.document.hasFocus()) { + + // Call original focus() + targetWindow.focus(); + + // In Electron, `window.focus()` fails to bring the window + // to the front if multiple windows exist in the same process + // group (floating windows). As such, we ask the host service + // to focus the window which can take care of bringin the + // window to the front. + // + // To minimise disruption by bringing windows to the front + // by accident, we only do this if the window is not already + // focused and the active window is not the target window + // but has focus. This is an indication that multiple windows + // are opened in the same process group while the target window + // is not focused. + + if ( + !this.environmentService.extensionTestsLocationURI && + !targetWindow.document.hasFocus() + ) { + this.hostService.focus(targetWindow); + } + } + } + //#endregion //#region timeout handling in multi-window applications @@ -186,12 +211,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 +313,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 6c25dc9d977..f8dd0115412 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 c27b43d382c..6cb1ce9b115 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 fb097ebecaa..faa741438ed 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 33a1379d3bd..e3b3bb1ef7a 100644 --- a/src/vs/workbench/contrib/accessibility/browser/accessibilityConfiguration.ts +++ b/src/vs/workbench/contrib/accessibility/browser/accessibilityConfiguration.ts @@ -45,6 +45,7 @@ export const enum AccessibilityVerbositySettingId { DiffEditor = 'accessibility.verbosity.diffEditor', Chat = 'accessibility.verbosity.panelChat', InlineChat = 'accessibility.verbosity.inlineChat', + TerminalChat = 'accessibility.verbosity.terminalChat', InlineCompletions = 'accessibility.verbosity.inlineCompletions', KeybindingsEditor = 'accessibility.verbosity.keybindingsEditor', Notebook = 'accessibility.verbosity.notebook', @@ -57,6 +58,7 @@ export const enum AccessibilityVerbositySettingId { export const enum AccessibleViewProviderId { Terminal = 'terminal', + TerminalChat = 'terminal-chat', TerminalHelp = 'terminal-help', DiffEditor = 'diffEditor', Chat = 'panelChat', @@ -544,6 +546,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 87abfd5cb8d..e4df2fcc74e 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 329cabe4bf3..ec79963a27f 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 00000000000..6559535304c --- /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 00000000000..87afd379e24 --- /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 00000000000..90649d2f358 --- /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 ba85dee93b2..6f4e6cce624 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 0d34b49aff8..000c78ced65 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 b6c66797247..73a0ebd76a9 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 8498292395a..3e70653b2f0 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 4651778b165..d184cb7525c 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 93b651276e5..f06f0a9e5a4 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 20f6f5d621f..793c8f1032a 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 b1ef64dae9d..46854de6a5e 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}`); } - this._agents.set(agent.id, { agent }); - this._onDidChangeAgents.fire(agent); + + const data = this.getRegisteredAgent(name); + if (!data) { + throw new Error(`Unknown agent: ${name}`); + } + + 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 2c43c2af703..ca022f35aa0 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 c8e47cb8b16..a2c05d68ca0 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 4f7654fd56a..e6bb4e56bf9 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 aa05f52b6d8..ca2057b6342 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 5da8d9b747b..c7b1a1eec48 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 e28b1e14e28..ac232bc4dba 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 fc1328ca803..10a1bf4787c 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 edf5b4ae445..763773d71c2 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 9c92a44354a..5f7208cdd79 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/electron-sandbox/actions/media/voiceChatActions.css b/src/vs/workbench/contrib/chat/electron-sandbox/actions/media/voiceChatActions.css index 900330afe18..3411d749fce 100644 --- a/src/vs/workbench/contrib/chat/electron-sandbox/actions/media/voiceChatActions.css +++ b/src/vs/workbench/contrib/chat/electron-sandbox/actions/media/voiceChatActions.css @@ -7,7 +7,8 @@ * Replace with "microphone" icon. */ .monaco-workbench .interactive-input-part .monaco-action-bar .action-label.codicon-loading.codicon-modifier-spin:not(.disabled)::before, -.monaco-workbench .inline-chat .monaco-action-bar .action-label.codicon-loading.codicon-modifier-spin:not(.disabled)::before { +.monaco-workbench .inline-chat .monaco-action-bar .action-label.codicon-loading.codicon-modifier-spin:not(.disabled)::before, +.monaco-workbench .terminal-inline-chat .monaco-action-bar .action-label.codicon-loading.codicon-modifier-spin:not(.disabled)::before { content: "\ec1c"; font-family: 'codicon'; } @@ -16,7 +17,8 @@ * Clear animation styles when reduced motion is enabled. */ .monaco-workbench.reduce-motion .interactive-input-part .monaco-action-bar .action-label.codicon-loading.codicon-modifier-spin:not(.disabled), -.monaco-workbench.reduce-motion .inline-chat .monaco-action-bar .action-label.codicon-loading.codicon-modifier-spin:not(.disabled) { +.monaco-workbench.reduce-motion .inline-chat .monaco-action-bar .action-label.codicon-loading.codicon-modifier-spin:not(.disabled), +.monaco-workbench.reduce-motion .terminal-inline-chat .monaco-action-bar .action-label.codicon-loading.codicon-modifier-spin:not(.disabled) { animation: none; } @@ -24,7 +26,8 @@ * Replace with "stop" icon when reduced motion is enabled. */ .monaco-workbench.reduce-motion .interactive-input-part .monaco-action-bar .action-label.codicon-loading.codicon-modifier-spin:not(.disabled)::before, -.monaco-workbench.reduce-motion .inline-chat .monaco-action-bar .action-label.codicon-loading.codicon-modifier-spin:not(.disabled)::before { +.monaco-workbench.reduce-motion .inline-chat .monaco-action-bar .action-label.codicon-loading.codicon-modifier-spin:not(.disabled)::before, +.monaco-workbench.reduce-motion .terminal-inline-chat .monaco-action-bar .action-label.codicon-loading.codicon-modifier-spin:not(.disabled)::before { content: "\ead7"; font-family: 'codicon'; } diff --git a/src/vs/workbench/contrib/chat/electron-sandbox/actions/voiceChatActions.ts b/src/vs/workbench/contrib/chat/electron-sandbox/actions/voiceChatActions.ts index b0f09ffdf75..47909f02faa 100644 --- a/src/vs/workbench/contrib/chat/electron-sandbox/actions/voiceChatActions.ts +++ b/src/vs/workbench/contrib/chat/electron-sandbox/actions/voiceChatActions.ts @@ -51,20 +51,23 @@ import { IVoiceChatService } from 'vs/workbench/contrib/chat/common/voiceChat'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { ThemeIcon } from 'vs/base/common/themables'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; +import { ITerminalService } from 'vs/workbench/contrib/terminal/browser/terminal'; import { ProgressLocation } from 'vs/platform/progress/common/progress'; +import { TerminalChatController } from 'vs/workbench/contrib/terminal/browser/terminalContribExports'; const CONTEXT_VOICE_CHAT_GETTING_READY = new RawContextKey('voiceChatGettingReady', false, { type: 'boolean', description: localize('voiceChatGettingReady', "True when getting ready for receiving voice input from the microphone for voice chat.") }); const CONTEXT_VOICE_CHAT_IN_PROGRESS = new RawContextKey('voiceChatInProgress', false, { type: 'boolean', description: localize('voiceChatInProgress', "True when voice recording from microphone is in progress for voice chat.") }); const CONTEXT_QUICK_VOICE_CHAT_IN_PROGRESS = new RawContextKey('quickVoiceChatInProgress', false, { type: 'boolean', description: localize('quickVoiceChatInProgress', "True when voice recording from microphone is in progress for quick chat.") }); const CONTEXT_INLINE_VOICE_CHAT_IN_PROGRESS = new RawContextKey('inlineVoiceChatInProgress', false, { type: 'boolean', description: localize('inlineVoiceChatInProgress', "True when voice recording from microphone is in progress for inline chat.") }); +const CONTEXT_TERMINAL_VOICE_CHAT_IN_PROGRESS = new RawContextKey('terminalVoiceChatInProgress', false, { type: 'boolean', description: localize('terminalVoiceChatInProgress', "True when voice recording from microphone is in progress for terminal chat.") }); const CONTEXT_VOICE_CHAT_IN_VIEW_IN_PROGRESS = new RawContextKey('voiceChatInViewInProgress', false, { type: 'boolean', description: localize('voiceChatInViewInProgress', "True when voice recording from microphone is in progress in the chat view.") }); const CONTEXT_VOICE_CHAT_IN_EDITOR_IN_PROGRESS = new RawContextKey('voiceChatInEditorInProgress', false, { type: 'boolean', description: localize('voiceChatInEditorInProgress', "True when voice recording from microphone is in progress in the chat editor.") }); const CanVoiceChat = ContextKeyExpr.and(CONTEXT_PROVIDER_EXISTS, HasSpeechProvider); const FocusInChatInput = assertIsDefined(ContextKeyExpr.or(CTX_INLINE_CHAT_FOCUSED, CONTEXT_IN_CHAT_INPUT)); -type VoiceChatSessionContext = 'inline' | 'quick' | 'view' | 'editor'; +type VoiceChatSessionContext = 'inline' | 'terminal' | 'quick' | 'view' | 'editor'; interface IVoiceChatSessionController { @@ -88,14 +91,16 @@ class VoiceChatSessionControllerFactory { static create(accessor: ServicesAccessor, context: 'quick'): Promise; static create(accessor: ServicesAccessor, context: 'view'): Promise; static create(accessor: ServicesAccessor, context: 'focused'): Promise; - static create(accessor: ServicesAccessor, context: 'inline' | 'quick' | 'view' | 'focused'): Promise; - static async create(accessor: ServicesAccessor, context: 'inline' | 'quick' | 'view' | 'focused'): Promise { + static create(accessor: ServicesAccessor, context: 'terminal'): Promise; + static create(accessor: ServicesAccessor, context: 'inline' | 'terminal' | 'quick' | 'view' | 'focused'): Promise; + static async create(accessor: ServicesAccessor, context: 'inline' | 'terminal' | 'quick' | 'view' | 'focused'): Promise { const chatWidgetService = accessor.get(IChatWidgetService); const viewsService = accessor.get(IViewsService); const chatContributionService = accessor.get(IChatContributionService); const quickChatService = accessor.get(IQuickChatService); const layoutService = accessor.get(IWorkbenchLayoutService); const editorService = accessor.get(IEditorService); + const terminalService = accessor.get(ITerminalService); // Currently Focused Context if (context === 'focused') { @@ -130,6 +135,15 @@ class VoiceChatSessionControllerFactory { return VoiceChatSessionControllerFactory.doCreateForInlineChat(inlineChat); } } + + // Try with the terminal chat + const activeInstance = terminalService.activeInstance; + if (activeInstance) { + const terminalChat = TerminalChatController.activeChatWidget || TerminalChatController.get(activeInstance); + if (terminalChat?.hasFocus()) { + return VoiceChatSessionControllerFactory.doCreateForTerminalChat(terminalChat); + } + } } // View Chat @@ -151,6 +165,17 @@ class VoiceChatSessionControllerFactory { } } + // Terminal Chat + if (context === 'terminal') { + const activeInstance = terminalService.activeInstance; + if (activeInstance) { + const terminalChat = TerminalChatController.activeChatWidget || TerminalChatController.get(activeInstance); + if (terminalChat) { + return VoiceChatSessionControllerFactory.doCreateForTerminalChat(terminalChat); + } + } + } + // Quick Chat if (context === 'quick') { quickChatService.open(); @@ -231,6 +256,20 @@ class VoiceChatSessionControllerFactory { clearInputPlaceholder: () => inlineChat.resetPlaceholder() }; } + + private static doCreateForTerminalChat(terminalChat: TerminalChatController): IVoiceChatSessionController { + return { + context: 'terminal', + onDidAcceptInput: terminalChat.onDidAcceptInput, + onDidCancelInput: terminalChat.onDidCancelInput, + focusInput: () => terminalChat.focus(), + acceptInput: () => terminalChat.acceptInput(), + updateInput: text => terminalChat.updateInput(text, false), + getInput: () => terminalChat.getInput(), + setInputPlaceholder: text => terminalChat.setPlaceholder(text), + clearInputPlaceholder: () => terminalChat.resetPlaceholder() + }; + } } interface IVoiceChatSession { @@ -262,6 +301,7 @@ class VoiceChatSessions { private quickVoiceChatInProgressKey = CONTEXT_QUICK_VOICE_CHAT_IN_PROGRESS.bindTo(this.contextKeyService); private inlineVoiceChatInProgressKey = CONTEXT_INLINE_VOICE_CHAT_IN_PROGRESS.bindTo(this.contextKeyService); + private terminalVoiceChatInProgressKey = CONTEXT_TERMINAL_VOICE_CHAT_IN_PROGRESS.bindTo(this.contextKeyService); private voiceChatInViewInProgressKey = CONTEXT_VOICE_CHAT_IN_VIEW_IN_PROGRESS.bindTo(this.contextKeyService); private voiceChatInEditorInProgressKey = CONTEXT_VOICE_CHAT_IN_EDITOR_IN_PROGRESS.bindTo(this.contextKeyService); @@ -352,6 +392,9 @@ class VoiceChatSessions { case 'inline': this.inlineVoiceChatInProgressKey.set(true); break; + case 'terminal': + this.terminalVoiceChatInProgressKey.set(true); + break; case 'quick': this.quickVoiceChatInProgressKey.set(true); break; @@ -394,6 +437,7 @@ class VoiceChatSessions { this.quickVoiceChatInProgressKey.set(false); this.inlineVoiceChatInProgressKey.set(false); + this.terminalVoiceChatInProgressKey.set(false); this.voiceChatInViewInProgressKey.set(false); this.voiceChatInEditorInProgressKey.set(false); } @@ -565,7 +609,8 @@ export class StartVoiceChatAction extends Action2 { CONTEXT_VOICE_CHAT_IN_VIEW_IN_PROGRESS.negate(), CONTEXT_QUICK_VOICE_CHAT_IN_PROGRESS.negate(), CONTEXT_VOICE_CHAT_IN_EDITOR_IN_PROGRESS.negate(), - CONTEXT_INLINE_VOICE_CHAT_IN_PROGRESS.negate() + CONTEXT_INLINE_VOICE_CHAT_IN_PROGRESS.negate(), + CONTEXT_TERMINAL_VOICE_CHAT_IN_PROGRESS.negate() ), primary: KeyMod.CtrlCmd | KeyCode.KeyI }, @@ -581,6 +626,12 @@ export class StartVoiceChatAction extends Action2 { when: ContextKeyExpr.and(HasSpeechProvider, CONTEXT_INLINE_VOICE_CHAT_IN_PROGRESS.negate()), group: 'main', order: -1 + }, + { + id: MenuId.for('terminalChatInput'), + when: ContextKeyExpr.and(HasSpeechProvider, CONTEXT_TERMINAL_VOICE_CHAT_IN_PROGRESS.negate()), + group: 'main', + order: -1 }] }); } @@ -627,6 +678,11 @@ export class InstallVoiceChatAction extends Action2 { when: HasSpeechProvider.negate(), group: 'main', order: -1 + }, { + id: MenuId.for('terminalChatInput'), + when: HasSpeechProvider.negate(), + group: 'main', + order: -1 }] }); } @@ -650,7 +706,7 @@ class BaseStopListeningAction extends Action2 { constructor( desc: { id: string; icon?: ThemeIcon; f1?: boolean }, - private readonly target: 'inline' | 'quick' | 'view' | 'editor' | undefined, + private readonly target: 'inline' | 'terminal' | 'quick' | 'view' | 'editor' | undefined, context: RawContextKey, menu: MenuId | undefined, group: 'navigation' | 'main' = 'navigation' @@ -723,6 +779,15 @@ export class StopListeningInInlineChatAction extends BaseStopListeningAction { } } +export class StopListeningInTerminalChatAction extends BaseStopListeningAction { + + static readonly ID = 'workbench.action.chat.stopListeningInTerminalChat'; + + constructor() { + super({ id: StopListeningInTerminalChatAction.ID, icon: spinningLoading }, 'terminal', CONTEXT_TERMINAL_VOICE_CHAT_IN_PROGRESS, MenuId.for('terminalChatInput'), 'main'); + } +} + export class StopListeningAndSubmitAction extends Action2 { static readonly ID = 'workbench.action.chat.stopListeningAndSubmit'; @@ -761,8 +826,29 @@ registerThemingParticipant((theme, collector) => { // Show a "microphone" icon when recording is in progress that glows via outline. collector.addRule(` .monaco-workbench:not(.reduce-motion) .interactive-input-part .monaco-action-bar .action-label.codicon-loading.codicon-modifier-spin:not(.disabled), - .monaco-workbench:not(.reduce-motion) .inline-chat .monaco-action-bar .action-label.codicon-loading.codicon-modifier-spin:not(.disabled) { + .monaco-workbench:not(.reduce-motion) .inline-chat .monaco-action-bar .action-label.codicon-loading.codicon-modifier-spin:not(.disabled), + .monaco-workbench:not(.reduce-motion) .terminal-inline-chat .monaco-action-bar .action-label.codicon-loading.codicon-modifier-spin:not(.disabled) { + color: ${activeRecordingColor}; + outline: 1px solid ${activeRecordingColor}; + outline-offset: -1px; + animation: pulseAnimation 1s infinite; border-radius: 50%; + } + + .monaco-workbench:not(.reduce-motion) .interactive-input-part .monaco-action-bar .action-label.codicon-loading.codicon-modifier-spin:not(.disabled)::before, + .monaco-workbench:not(.reduce-motion) .inline-chat .monaco-action-bar .action-label.codicon-loading.codicon-modifier-spin:not(.disabled)::before, + .monaco-workbench:not(.reduce-motion) .terminal-inline-chat .monaco-action-bar .action-label.codicon-loading.codicon-modifier-spin:not(.disabled)::before { + position: absolute; + outline: 1px solid ${activeRecordingColor}; + outline-offset: 2px; + border-radius: 50%; + width: 16px; + height: 16px; + } + + .monaco-workbench:not(.reduce-motion) .interactive-input-part .monaco-action-bar .action-label.codicon-loading.codicon-modifier-spin:not(.disabled)::after, + .monaco-workbench:not(.reduce-motion) .inline-chat .monaco-action-bar .action-label.codicon-loading.codicon-modifier-spin:not(.disabled), + .monaco-workbench:not(.reduce-motion) .terminal-inline-chat .monaco-action-bar .action-label.codicon-loading.codicon-modifier-spin:not(.disabled)::after { outline: 2px solid ${activeRecordingColor}; outline-offset: -1px; animation: pulseAnimation 1500ms cubic-bezier(0.75, 0, 0.25, 1) infinite; @@ -980,7 +1066,7 @@ class KeywordActivationStatusEntry extends Disposable { ) { super(); - CommandsRegistry.registerCommand(KeywordActivationStatusEntry.STATUS_COMMAND, () => this.commandService.executeCommand('workbench.action.openSettings', KEYWORD_ACTIVIATION_SETTING_ID)); + this._register(CommandsRegistry.registerCommand(KeywordActivationStatusEntry.STATUS_COMMAND, () => this.commandService.executeCommand('workbench.action.openSettings', KEYWORD_ACTIVIATION_SETTING_ID))); this.registerListeners(); this.updateStatusEntry(); diff --git a/src/vs/workbench/contrib/chat/electron-sandbox/chat.contribution.ts b/src/vs/workbench/contrib/chat/electron-sandbox/chat.contribution.ts index 08b145350e1..eb473ee8091 100644 --- a/src/vs/workbench/contrib/chat/electron-sandbox/chat.contribution.ts +++ b/src/vs/workbench/contrib/chat/electron-sandbox/chat.contribution.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { InlineVoiceChatAction, QuickVoiceChatAction, StartVoiceChatAction, StopListeningInInlineChatAction, StopListeningInQuickChatAction, StopListeningInChatEditorAction, StopListeningInChatViewAction, VoiceChatInChatViewAction, StopListeningAction, StopListeningAndSubmitAction, KeywordActivationContribution, InstallVoiceChatAction, HoldToVoiceChatInChatViewAction } from 'vs/workbench/contrib/chat/electron-sandbox/actions/voiceChatActions'; +import { InlineVoiceChatAction, QuickVoiceChatAction, StartVoiceChatAction, StopListeningInInlineChatAction, StopListeningInQuickChatAction, StopListeningInChatEditorAction, StopListeningInChatViewAction, VoiceChatInChatViewAction, StopListeningAction, StopListeningAndSubmitAction, KeywordActivationContribution, InstallVoiceChatAction, StopListeningInTerminalChatAction, HoldToVoiceChatInChatViewAction } from 'vs/workbench/contrib/chat/electron-sandbox/actions/voiceChatActions'; import { registerAction2 } from 'vs/platform/actions/common/actions'; import { WorkbenchPhase, registerWorkbenchContribution2 } from 'vs/workbench/common/contributions'; @@ -22,5 +22,6 @@ registerAction2(StopListeningInChatViewAction); registerAction2(StopListeningInChatEditorAction); registerAction2(StopListeningInQuickChatAction); registerAction2(StopListeningInInlineChatAction); +registerAction2(StopListeningInTerminalChatAction); registerWorkbenchContribution2(KeywordActivationContribution.ID, KeywordActivationContribution, WorkbenchPhase.AfterRestored); 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 4a241114279..ae1818689d0 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 becd9bf6f31..190c3555054 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 50c67ea58d0..65d00490273 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 345e8c874de..6585ff1e0e5 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 406e20cfe55..fc2a622c9ac 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 31fd0b94e96..6f9eaa531cf 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 85bc82a3136..fee5731f3e3 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 d58da8b3744..cbd61e5811d 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 0939983222f..75c5fa71f40 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 3a6b248a792..f231c781190 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 ba397535bff..e82c0f84940 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 3a2b6abc998..eb6bee1c3fa 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 00000000000..e1adddb2dec --- /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 35c17753392..d961bbb74c5 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 c85e0056d12..dba9fb7a613 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 902b335ba4f..6aeddc30e35 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/codeEditor/browser/emptyTextEditorHint/emptyTextEditorHint.ts b/src/vs/workbench/contrib/codeEditor/browser/emptyTextEditorHint/emptyTextEditorHint.ts index 10e517dc4cb..ab46e943663 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/emptyTextEditorHint/emptyTextEditorHint.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/emptyTextEditorHint/emptyTextEditorHint.ts @@ -226,7 +226,7 @@ class EmptyTextEditorHintContentWidget implements IContentWidget { id: 'inlineChat.hintAction', from: 'hint' }); - void this.commandService.executeCommand(inlineChatId, { from: 'hint' }); + this.commandService.executeCommand(inlineChatId, { from: 'hint' }); }; const hintHandler: IContentActionHandler = { @@ -254,7 +254,7 @@ class EmptyTextEditorHintContentWidget implements IContentWidget { const hintPart = $('a', undefined, fragment); hintPart.style.fontStyle = 'italic'; hintPart.style.cursor = 'pointer'; - hintPart.onclick = handleClick; + this.toDispose.add(dom.addDisposableListener(hintPart, dom.EventType.CLICK, handleClick)); return hintPart; } else { const hintPart = $('span', undefined, fragment); @@ -272,7 +272,7 @@ class EmptyTextEditorHintContentWidget implements IContentWidget { if (this.options.clickable) { label.element.style.cursor = 'pointer'; - label.element.onclick = handleClick; + this.toDispose.add(dom.addDisposableListener(label.element, dom.EventType.CLICK, handleClick)); } hintElement.appendChild(after); diff --git a/src/vs/workbench/contrib/comments/browser/commentNode.ts b/src/vs/workbench/contrib/comments/browser/commentNode.ts index 58e376f9561..95ef19e971e 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 e6fd43f4b91..7a0f4d21531 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 1c36e83c55c..710d0d1d783 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 13b72018035..ddfb63625fd 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 b9d319bad85..db948476aef 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 3bb555a1a53..f2f839ff912 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 b1a9a4a0789..12376d1a83f 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 5478398dfb6..84e946ecf70 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 861135c6f6a..0d56d661268 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 e092df64537..92f139a3721 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 cce34ecbbe6..56582689c12 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 86d0e94b826..eefbb082ad6 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 8098d1ce57b..b12e9b726ff 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 b00a4fd466a..50eacfd65e2 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 4b0959a97a8..7221f390771 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 b85e544f9bb..61599c36ce9 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 617f46d449f..464a4794def 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 6ba3511f0ee..f612c22c3e3 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 1c278f159d3..7c7fbac3a95 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 8f4ca1d9d6a..34c163c0b47 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 1ff80d2256d..2f2833049ba 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 window', 'Reload Window') + : 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 38c0e666af1..43c231d7c45 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 adcd04b081f..8e64f3bacaa 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 9245e2b8b4f..01bd5f4b89b 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 c92d36d3ecf..903dcac8f25 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.Downloaded) { + 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,31 @@ 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.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 +1732,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 +2003,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 0158101cc31..b523d97d6b2 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 11dc035fb5b..14cb766b00d 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 1e9e11baa19..d0bb47b7529 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 0762678249d..3ed7d4a1b94 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 4a06d92c0e8..d37a7df8775 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 39d5869cd3c..701a823db7d 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 ad722bacc27..1b6b279b308 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 c0f1c912060..7c01103959b 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/browser/views/openEditorsView.ts b/src/vs/workbench/contrib/files/browser/views/openEditorsView.ts index c6fd9a02c0d..ed7dcd5a06a 100644 --- a/src/vs/workbench/contrib/files/browser/views/openEditorsView.ts +++ b/src/vs/workbench/contrib/files/browser/views/openEditorsView.ts @@ -26,7 +26,7 @@ import { IListVirtualDelegate, IListRenderer, IListContextMenuEvent, IListDragAn import { ResourceLabels, IResourceLabel } from 'vs/workbench/browser/labels'; import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import { IDisposable, dispose } from 'vs/base/common/lifecycle'; +import { DisposableMap, IDisposable, dispose } from 'vs/base/common/lifecycle'; import { MenuId, Action2, registerAction2, MenuRegistry } from 'vs/platform/actions/common/actions'; import { OpenEditorsDirtyEditorContext, OpenEditorsGroupContext, OpenEditorsReadonlyEditorContext, SAVE_ALL_LABEL, SAVE_ALL_COMMAND_ID, NEW_UNTITLED_FILE_COMMAND_ID } from 'vs/workbench/contrib/files/browser/fileConstants'; import { ResourceContextKey, MultipleEditorGroupsContext } from 'vs/workbench/common/contextkeys'; @@ -118,7 +118,7 @@ export class OpenEditorsView extends ViewPane { this.listRefreshScheduler?.schedule(this.structuralRefreshDelay); }; - const groupDisposables = new Map(); + const groupDisposables = this._register(new DisposableMap()); const addGroupListener = (group: IEditorGroup) => { const groupModelChangeListener = group.onDidModelChange(e => { if (this.listRefreshScheduler?.isScheduled()) { @@ -156,7 +156,6 @@ export class OpenEditorsView extends ViewPane { } }); groupDisposables.set(group.id, groupModelChangeListener); - this._register(groupDisposables.get(group.id)!); }; this.editorGroupService.groups.forEach(g => addGroupListener(g)); @@ -167,7 +166,7 @@ export class OpenEditorsView extends ViewPane { this._register(this.editorGroupService.onDidMoveGroup(() => updateWholeList())); this._register(this.editorGroupService.onDidChangeActiveGroup(() => this.focusActiveEditor())); this._register(this.editorGroupService.onDidRemoveGroup(group => { - dispose(groupDisposables.get(group.id)); + groupDisposables.deleteAndDispose(group.id); updateWholeList(); })); } 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 e1ecd72a649..f104ba762eb 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/inlineChat.contribution.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChat.contribution.ts index ea70b146a8c..c6a996a178c 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChat.contribution.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChat.contribution.ts @@ -51,8 +51,6 @@ registerAction2(InlineChatActions.FocusInlineChat); registerAction2(InlineChatActions.PreviousFromHistory); registerAction2(InlineChatActions.NextFromHistory); registerAction2(InlineChatActions.ViewInChatAction); -registerAction2(InlineChatActions.ExpandMessageAction); -registerAction2(InlineChatActions.ContractMessageAction); registerAction2(InlineChatActions.ToggleDiffForChange); registerAction2(InlineChatActions.FeebackHelpfulCommand); diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChat.css b/src/vs/workbench/contrib/inlineChat/browser/inlineChat.css index 7d299bbc1e5..70ff20e6432 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChat.css +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChat.css @@ -3,15 +3,15 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -.monaco-editor .zone-widget.inline-chat-widget { +.monaco-workbench .zone-widget.inline-chat-widget { z-index: 3; } -.monaco-editor .zone-widget-container.inside-selection { +.monaco-workbench .zone-widget-container.inside-selection { background-color: var(--vscode-inlineChat-regionHighlight); } -.monaco-editor .inline-chat { +.monaco-workbench .inline-chat { color: inherit; padding: 6px; margin-top: 6px; @@ -23,11 +23,11 @@ /* body */ -.monaco-editor .inline-chat .body { +.monaco-workbench .inline-chat .body { display: flex; } -.monaco-editor .inline-chat .body .content { +.monaco-workbench .inline-chat .body .content { display: flex; box-sizing: border-box; outline: 1px solid var(--vscode-inlineChatInput-border); @@ -35,11 +35,11 @@ border-radius: 2px; } -.monaco-editor .inline-chat .body .content.synthetic-focus { +.monaco-workbench .inline-chat .body .content.synthetic-focus { outline: 1px solid var(--vscode-inlineChatInput-focusBorder); } -.monaco-editor .inline-chat .body .content .input { +.monaco-workbench .inline-chat .body .content .input { display: flex; align-items: center; justify-content: space-between; @@ -48,11 +48,11 @@ cursor: text; } -.monaco-editor .inline-chat .body .content .input .monaco-editor-background { +.monaco-workbench .inline-chat .body .content .input .monaco-editor-background { background-color: var(--vscode-inlineChatInput-background); } -.monaco-editor .inline-chat .body .content .input .editor-placeholder { +.monaco-workbench .inline-chat .body .content .input .editor-placeholder { position: absolute; z-index: 1; color: var(--vscode-inlineChatInput-placeholderForeground); @@ -61,14 +61,15 @@ text-overflow: ellipsis; } -.monaco-editor .inline-chat .body .content .input .editor-placeholder.hidden { +.monaco-workbench .inline-chat .body .content .input .editor-placeholder.hidden { display: none; } -.monaco-editor .inline-chat .body .content .input .editor-container { +.monaco-workbench .inline-chat .body .content .input .editor-container { vertical-align: middle; } -.monaco-editor .inline-chat .body .toolbar { + +.monaco-workbench .inline-chat .body .toolbar { display: flex; flex-direction: column; align-self: stretch; @@ -78,47 +79,47 @@ background: var(--vscode-inlineChatInput-background); } -.monaco-editor .inline-chat .body .toolbar .actions-container { +.monaco-workbench .inline-chat .body .toolbar .actions-container { display: flex; flex-direction: row; gap: 4px; } -.monaco-editor .inline-chat .body > .widget-toolbar { +.monaco-workbench .inline-chat .body > .widget-toolbar { padding-left: 4px; } /* progress bit */ -.monaco-editor .inline-chat .progress { +.monaco-workbench .inline-chat .progress { position: relative; width: calc(100% - 18px); left: 19px; } /* UGLY - fighting against workbench styles */ -.monaco-workbench .part.editor > .content .monaco-editor .inline-chat .progress .monaco-progress-container { +.monaco-workbench .part.editor > .content .inline-chat .progress .monaco-progress-container { top: 0; } /* status */ -.monaco-editor .inline-chat .status { +.monaco-workbench .inline-chat .status { margin-top: 4px; display: flex; justify-content: space-between; align-items: center; } -.monaco-editor .inline-chat .status.actions { +.monaco-workbench .inline-chat .status.actions { margin-top: 4px; } -.monaco-editor .inline-chat .status .actions.hidden { +.monaco-workbench .inline-chat .status .actions.hidden { display: none; } -.monaco-editor .inline-chat .status .label { +.monaco-workbench .inline-chat .status .label { overflow: hidden; color: var(--vscode-descriptionForeground); font-size: 11px; @@ -126,203 +127,179 @@ display: inline-flex; } -.monaco-editor .inline-chat .status .label.hidden { +.monaco-workbench .inline-chat .status .label.hidden { display: none; } -.monaco-editor .inline-chat .status .label.info { +.monaco-workbench .inline-chat .status .label.info { margin-right: auto; padding-left: 2px; } -.monaco-editor .inline-chat .status .label.info > .codicon { +.monaco-workbench .inline-chat .status .label.info > .codicon { padding: 0 5px; font-size: 12px; line-height: 18px; } -.monaco-editor .inline-chat .status .label.status { +.monaco-workbench .inline-chat .status .label.status { padding-left: 10px; padding-right: 4px; margin-left: auto; } -.monaco-editor .inline-chat .status .label .slash-command-pill CODE { +.monaco-workbench .inline-chat .status .label .slash-command-pill CODE { border-radius: 3px; padding: 0 1px; background-color: var(--vscode-chat-slashCommandBackground); color: var(--vscode-chat-slashCommandForeground); } -.monaco-editor .inline-chat .detectedIntent { +.monaco-workbench .inline-chat .detectedIntent { color: var(--vscode-descriptionForeground); padding: 5px 0px 5px 5px; } -.monaco-editor .inline-chat .detectedIntent.hidden { +.monaco-workbench .inline-chat .detectedIntent.hidden { display: none; } -.monaco-editor .inline-chat .detectedIntent .slash-command-pill CODE { +.monaco-workbench .inline-chat .detectedIntent .slash-command-pill CODE { border-radius: 3px; padding: 0 1px; background-color: var(--vscode-chat-slashCommandBackground); color: var(--vscode-chat-slashCommandForeground); } -.monaco-editor .inline-chat .detectedIntent .slash-command-pill a { +.monaco-workbench .inline-chat .detectedIntent .slash-command-pill a { color: var(--vscode-textLink-foreground); cursor: pointer; } -/* .monaco-editor .inline-chat .markdownMessage .message * { - margin: unset; -} -.monaco-editor .inline-chat .markdownMessage .message code { - font-family: var(--monaco-monospace-font); - font-size: 12px; - color: var(--vscode-textPreformat-foreground); - background-color: var(--vscode-textPreformat-background); - padding: 1px 3px; - border-radius: 4px; -} */ - - -.monaco-editor .inline-chat .chatMessage .chatMessageContent .value { - -webkit-line-clamp: initial; - -webkit-box-orient: vertical; +.monaco-workbench .inline-chat .chatMessage .chatMessageContent .value { overflow: hidden; - display: -webkit-box; -webkit-user-select: text; user-select: text; } -.monaco-editor .inline-chat .chatMessage .chatMessageContent[state="cropped"] .value { - -webkit-line-clamp: var(--vscode-inline-chat-cropped, 3); -} - -.monaco-editor .inline-chat .chatMessage .chatMessageContent[state="expanded"] .value { - -webkit-line-clamp: var(--vscode-inline-chat-expanded, 10); -} - -.monaco-editor .inline-chat .followUps { +.monaco-workbench .inline-chat .followUps { padding: 5px 5px; } -.monaco-editor .inline-chat .followUps .interactive-session-followups .monaco-button { +.monaco-workbench .inline-chat .followUps .interactive-session-followups .monaco-button { display: block; color: var(--vscode-textLink-foreground); font-size: 12px; } -.monaco-editor .inline-chat .followUps.hidden { +.monaco-workbench .inline-chat .followUps.hidden { display: none; } -.monaco-editor .inline-chat .chatMessage { +.monaco-workbench .inline-chat .chatMessage { padding: 8px 3px; } -.monaco-editor .inline-chat .chatMessage .chatMessageContent { +.monaco-workbench .inline-chat .chatMessage .chatMessageContent { padding: 2px 2px; } -.monaco-editor .inline-chat .chatMessage.hidden { +.monaco-workbench .inline-chat .chatMessage.hidden { display: none; } -.monaco-editor .inline-chat .status .label A { +.monaco-workbench .inline-chat .status .label A { color: var(--vscode-textLink-foreground); cursor: pointer; } -.monaco-editor .inline-chat .status .label.error { +.monaco-workbench .inline-chat .status .label.error { color: var(--vscode-errorForeground); } -.monaco-editor .inline-chat .status .label.warn { +.monaco-workbench .inline-chat .status .label.warn { color: var(--vscode-editorWarning-foreground); } -.monaco-editor .inline-chat .status .actions { +.monaco-workbench .inline-chat .status .actions { display: flex; } -.monaco-editor .inline-chat .status .actions > .monaco-button, -.monaco-editor .inline-chat .status .actions > .monaco-button-dropdown { +.monaco-workbench .inline-chat .status .actions > .monaco-button, +.monaco-workbench .inline-chat .status .actions > .monaco-button-dropdown { margin-right: 6px; } -.monaco-editor .inline-chat .status .actions > .monaco-button-dropdown > .monaco-dropdown-button { +.monaco-workbench .inline-chat .status .actions > .monaco-button-dropdown > .monaco-dropdown-button { display: flex; align-items: center; padding: 0 4px; } -.monaco-editor .inline-chat .status .actions > .monaco-button.codicon { +.monaco-workbench .inline-chat .status .actions > .monaco-button.codicon { display: flex; } -.monaco-editor .inline-chat .status .actions > .monaco-button.codicon::before { +.monaco-workbench .inline-chat .status .actions > .monaco-button.codicon::before { align-self: center; } -.monaco-editor .inline-chat .status .actions .monaco-text-button { +.monaco-workbench .inline-chat .status .actions .monaco-text-button { padding: 2px 4px; white-space: nowrap; } -.monaco-editor .inline-chat .status .monaco-toolbar .action-item { +.monaco-workbench .inline-chat .status .monaco-toolbar .action-item { padding: 0 2px; } /* TODO@jrieken not needed? */ -.monaco-editor .inline-chat .status .monaco-toolbar .action-label.checked { +.monaco-workbench .inline-chat .status .monaco-toolbar .action-label.checked { color: var(--vscode-inputOption-activeForeground); background-color: var(--vscode-inputOption-activeBackground); outline: 1px solid var(--vscode-inputOption-activeBorder); } -.monaco-editor .inline-chat .status .monaco-toolbar .action-item.button-item .action-label:is(:hover, :focus) { +.monaco-workbench .inline-chat .status .monaco-toolbar .action-item.button-item .action-label:is(:hover, :focus) { background-color: var(--vscode-button-hoverBackground); } /* preview */ -.monaco-editor .inline-chat .preview { +.monaco-workbench .inline-chat .preview { display: none; } -.monaco-editor .inline-chat .previewDiff, -.monaco-editor .inline-chat .previewCreate { +.monaco-workbench .inline-chat .previewDiff, +.monaco-workbench .inline-chat .previewCreate { display: inherit; border: 1px solid var(--vscode-inlineChat-border); border-radius: 2px; margin: 6px 0px; } -.monaco-editor .inline-chat .previewCreateTitle { +.monaco-workbench .inline-chat .previewCreateTitle { padding-top: 6px; } -.monaco-editor .inline-chat .diff-review.hidden, -.monaco-editor .inline-chat .previewDiff.hidden, -.monaco-editor .inline-chat .previewCreate.hidden, -.monaco-editor .inline-chat .previewCreateTitle.hidden { +.monaco-workbench .inline-chat .diff-review.hidden, +.monaco-workbench .inline-chat .previewDiff.hidden, +.monaco-workbench .inline-chat .previewCreate.hidden, +.monaco-workbench .inline-chat .previewCreateTitle.hidden { display: none; } -.monaco-editor .inline-chat-toolbar { +.monaco-workbench .inline-chat-toolbar { display: flex; } -.monaco-editor .inline-chat-toolbar > .monaco-button{ +.monaco-workbench .inline-chat-toolbar > .monaco-button { margin-right: 6px; } -.monaco-editor .inline-chat-toolbar .action-label.checked { +.monaco-workbench .inline-chat-toolbar .action-label.checked { color: var(--vscode-inputOption-activeForeground); background-color: var(--vscode-inputOption-activeBackground); outline: 1px solid var(--vscode-inputOption-activeBorder); @@ -330,65 +307,65 @@ /* decoration styles */ -.monaco-editor .inline-chat-inserted-range { +.monaco-workbench .inline-chat-inserted-range { background-color: var(--vscode-inlineChatDiff-inserted); } -.monaco-editor .inline-chat-inserted-range-linehighlight { +.monaco-workbench .inline-chat-inserted-range-linehighlight { background-color: var(--vscode-diffEditor-insertedLineBackground); } -.monaco-editor .inline-chat-original-zone2 { +.monaco-workbench .inline-chat-original-zone2 { background-color: var(--vscode-diffEditor-removedLineBackground); opacity: 0.8; } -.monaco-editor .inline-chat-lines-inserted-range { +.monaco-workbench .inline-chat-lines-inserted-range { background-color: var(--vscode-diffEditor-insertedTextBackground); } -.monaco-editor .inline-chat-block-selection { +.monaco-workbench .inline-chat-block-selection { background-color: var(--vscode-inlineChat-regionHighlight); } -.monaco-editor .inline-chat-slash-command { +.monaco-workbench .inline-chat-slash-command { opacity: 0; } -.monaco-editor .inline-chat-slash-command-detail { +.monaco-workbench .inline-chat-slash-command-detail { opacity: 0.5; } /* diff zone */ -.monaco-editor .inline-chat-diff-widget .monaco-diff-editor .monaco-editor-background, -.monaco-editor .inline-chat-diff-widget .monaco-diff-editor .monaco-editor .margin-view-overlays { +.monaco-workbench .inline-chat-diff-widget .monaco-diff-editor .monaco-editor-background, +.monaco-workbench .inline-chat-diff-widget .monaco-diff-editor .monaco-workbench .margin-view-overlays { background-color: var(--vscode-inlineChat-regionHighlight); } /* create zone */ -.monaco-editor .inline-chat-newfile-widget { +.monaco-workbench .inline-chat-newfile-widget { background-color: var(--vscode-inlineChat-regionHighlight); } -.monaco-editor .inline-chat-newfile-widget .title { +.monaco-workbench .inline-chat-newfile-widget .title { display: flex; align-items: center; justify-content: space-between; } -.monaco-editor .inline-chat-newfile-widget .title .detail { +.monaco-workbench .inline-chat-newfile-widget .title .detail { margin-left: 4px; } -.monaco-editor .inline-chat-newfile-widget .buttonbar-widget { +.monaco-workbench .inline-chat-newfile-widget .buttonbar-widget { display: flex; margin-left: auto; margin-right: 8px; } -.monaco-editor .inline-chat-newfile-widget .buttonbar-widget > .monaco-button { +.monaco-workbench .inline-chat-newfile-widget .buttonbar-widget > .monaco-button { display: inline-flex; white-space: nowrap; margin-left: 4px; @@ -396,22 +373,22 @@ /* gutter decoration */ -.monaco-editor .glyph-margin-widgets .cgmr.codicon-inline-chat-opaque, -.monaco-editor .glyph-margin-widgets .cgmr.codicon-inline-chat-transparent { +.monaco-workbench .glyph-margin-widgets .cgmr.codicon-inline-chat-opaque, +.monaco-workbench .glyph-margin-widgets .cgmr.codicon-inline-chat-transparent { display: block; cursor: pointer; transition: opacity .2s ease-in-out; } -.monaco-editor .glyph-margin-widgets .cgmr.codicon-inline-chat-opaque { +.monaco-workbench .glyph-margin-widgets .cgmr.codicon-inline-chat-opaque { opacity: 0.5; } -.monaco-editor .glyph-margin-widgets .cgmr.codicon-inline-chat-transparent { +.monaco-workbench .glyph-margin-widgets .cgmr.codicon-inline-chat-transparent { opacity: 0; } -.monaco-editor .glyph-margin-widgets .cgmr.codicon-inline-chat-opaque:hover, -.monaco-editor .glyph-margin-widgets .cgmr.codicon-inline-chat-transparent:hover { +.monaco-workbench .glyph-margin-widgets .cgmr.codicon-inline-chat-opaque:hover, +.monaco-workbench .glyph-margin-widgets .cgmr.codicon-inline-chat-transparent:hover { opacity: 1; } diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts index 75ca16a1f24..95e43b9d9d1 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts @@ -5,13 +5,13 @@ import { Codicon } from 'vs/base/common/codicons'; import { KeyChord, KeyCode, KeyMod } from 'vs/base/common/keyCodes'; -import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; +import { ICodeEditor, isCodeEditor, isDiffEditor } from 'vs/editor/browser/editorBrowser'; import { EditorAction2 } from 'vs/editor/browser/editorExtensions'; import { EmbeddedDiffEditorWidget } from 'vs/editor/browser/widget/diffEditor/embeddedDiffEditorWidget'; import { EmbeddedCodeEditorWidget } from 'vs/editor/browser/widget/codeEditor/embeddedCodeEditorWidget'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; import { InlineChatController, InlineChatRunOptions } from 'vs/workbench/contrib/inlineChat/browser/inlineChatController'; -import { CTX_INLINE_CHAT_FOCUSED, CTX_INLINE_CHAT_HAS_ACTIVE_REQUEST, CTX_INLINE_CHAT_HAS_PROVIDER, CTX_INLINE_CHAT_INNER_CURSOR_FIRST, CTX_INLINE_CHAT_INNER_CURSOR_LAST, CTX_INLINE_CHAT_EMPTY, CTX_INLINE_CHAT_OUTER_CURSOR_POSITION, CTX_INLINE_CHAT_VISIBLE, MENU_INLINE_CHAT_INPUT, MENU_INLINE_CHAT_WIDGET_DISCARD, MENU_INLINE_CHAT_WIDGET_STATUS, CTX_INLINE_CHAT_LAST_FEEDBACK, CTX_INLINE_CHAT_EDIT_MODE, EditMode, MENU_INLINE_CHAT_WIDGET_MARKDOWN_MESSAGE, CTX_INLINE_CHAT_MESSAGE_CROP_STATE, CTX_INLINE_CHAT_DOCUMENT_CHANGED, CTX_INLINE_CHAT_DID_EDIT, CTX_INLINE_CHAT_HAS_STASHED_SESSION, MENU_INLINE_CHAT_WIDGET_FEEDBACK, ACTION_ACCEPT_CHANGES, ACTION_REGENERATE_RESPONSE, CTX_INLINE_CHAT_RESPONSE_TYPES, InlineChatResponseTypes, ACTION_VIEW_IN_CHAT, CTX_INLINE_CHAT_USER_DID_EDIT, CTX_INLINE_CHAT_INNER_CURSOR_START, CTX_INLINE_CHAT_INNER_CURSOR_END, CTX_INLINE_CHAT_RESPONSE_FOCUSED, CTX_INLINE_CHAT_SUPPORT_ISSUE_REPORTING, InlineChatResponseFeedbackKind, CTX_INLINE_CHAT_CHANGE_SHOWS_DIFF, CTX_INLINE_CHAT_CHANGE_HAS_DIFF, MENU_INLINE_CHAT_WIDGET } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; +import { CTX_INLINE_CHAT_FOCUSED, CTX_INLINE_CHAT_HAS_ACTIVE_REQUEST, CTX_INLINE_CHAT_HAS_PROVIDER, CTX_INLINE_CHAT_INNER_CURSOR_FIRST, CTX_INLINE_CHAT_INNER_CURSOR_LAST, CTX_INLINE_CHAT_EMPTY, CTX_INLINE_CHAT_OUTER_CURSOR_POSITION, CTX_INLINE_CHAT_VISIBLE, MENU_INLINE_CHAT_INPUT, MENU_INLINE_CHAT_WIDGET_DISCARD, MENU_INLINE_CHAT_WIDGET_STATUS, CTX_INLINE_CHAT_LAST_FEEDBACK, CTX_INLINE_CHAT_EDIT_MODE, EditMode, CTX_INLINE_CHAT_DOCUMENT_CHANGED, CTX_INLINE_CHAT_DID_EDIT, CTX_INLINE_CHAT_HAS_STASHED_SESSION, MENU_INLINE_CHAT_WIDGET_FEEDBACK, ACTION_ACCEPT_CHANGES, ACTION_REGENERATE_RESPONSE, CTX_INLINE_CHAT_RESPONSE_TYPES, InlineChatResponseTypes, ACTION_VIEW_IN_CHAT, CTX_INLINE_CHAT_USER_DID_EDIT, CTX_INLINE_CHAT_INNER_CURSOR_START, CTX_INLINE_CHAT_INNER_CURSOR_END, CTX_INLINE_CHAT_RESPONSE_FOCUSED, CTX_INLINE_CHAT_SUPPORT_ISSUE_REPORTING, InlineChatResponseFeedbackKind, CTX_INLINE_CHAT_CHANGE_SHOWS_DIFF, CTX_INLINE_CHAT_CHANGE_HAS_DIFF, MENU_INLINE_CHAT_WIDGET } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; import { localize, localize2 } from 'vs/nls'; import { Action2, IAction2Options, MenuRegistry } from 'vs/platform/actions/common/actions'; import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; @@ -31,11 +31,12 @@ import { CommandsRegistry } from 'vs/platform/commands/common/commands'; import { AccessibilityHelpAction } from 'vs/workbench/contrib/accessibility/browser/accessibleViewActions'; import { registerIcon } from 'vs/platform/theme/common/iconRegistry'; import { IPreferencesService } from 'vs/workbench/services/preferences/common/preferences'; +import { ILogService } from 'vs/platform/log/common/log'; CommandsRegistry.registerCommandAlias('interactiveEditor.start', 'inlineChat.start'); CommandsRegistry.registerCommandAlias('interactive.acceptChanges', ACTION_ACCEPT_CHANGES); -export const LOCALIZED_START_INLINE_CHAT_STRING = localize2('run', 'Start Inline Chat'); +export const LOCALIZED_START_INLINE_CHAT_STRING = localize2('run', 'Start in Editor'); export const START_INLINE_CHAT = registerIcon('start-inline-chat', Codicon.sparkle, localize('startInlineChat', 'Icon which spawns the inline chat from the editor toolbar.')); // some gymnastics to enable hold for speech without moving the StartSessionAction into the electron-layer @@ -129,10 +130,28 @@ export abstract class AbstractInlineChatAction extends EditorAction2 { } override runEditorCommand(accessor: ServicesAccessor, editor: ICodeEditor, ..._args: any[]) { + const editorService = accessor.get(IEditorService); + const logService = accessor.get(ILogService); + + let ctrl = InlineChatController.get(editor); + if (!ctrl) { + const { activeTextEditorControl } = editorService; + if (isCodeEditor(activeTextEditorControl)) { + editor = activeTextEditorControl; + } else if (isDiffEditor(activeTextEditorControl)) { + editor = activeTextEditorControl.getModifiedEditor(); + } + ctrl = InlineChatController.get(editor); + } + + if (!ctrl) { + logService.warn('[IE] NO controller found for action', this.desc.id, editor.getModel()?.uri); + return; + } + if (editor instanceof EmbeddedCodeEditorWidget) { editor = editor.getParentEditor(); } - const ctrl = InlineChatController.get(editor); if (!ctrl) { for (const diffEditor of accessor.get(ICodeEditorService).listDiffEditors()) { if (diffEditor.getOriginalEditor() === editor || diffEditor.getModifiedEditor() === editor) { @@ -737,46 +756,6 @@ export class ViewInChatAction extends AbstractInlineChatAction { } } -export class ExpandMessageAction extends AbstractInlineChatAction { - constructor() { - super({ - id: 'inlineChat.expandMessageAction', - title: localize('expandMessage', 'Show More'), - icon: Codicon.chevronDown, - precondition: CTX_INLINE_CHAT_VISIBLE, - menu: { - id: MENU_INLINE_CHAT_WIDGET_MARKDOWN_MESSAGE, - when: ContextKeyExpr.and(ContextKeyExpr.or(CTX_INLINE_CHAT_RESPONSE_TYPES.isEqualTo(InlineChatResponseTypes.OnlyMessages), CTX_INLINE_CHAT_RESPONSE_TYPES.isEqualTo(InlineChatResponseTypes.Mixed)), CTX_INLINE_CHAT_MESSAGE_CROP_STATE.isEqualTo('cropped')), - group: '2_expandOrContract', - order: 1 - } - }); - } - override runInlineChatCommand(_accessor: ServicesAccessor, ctrl: InlineChatController, _editor: ICodeEditor, ..._args: any[]): void { - ctrl.updateExpansionState(true); - } -} - -export class ContractMessageAction extends AbstractInlineChatAction { - constructor() { - super({ - id: 'inlineChat.contractMessageAction', - title: localize('contractMessage', 'Show Less'), - icon: Codicon.chevronUp, - precondition: CTX_INLINE_CHAT_VISIBLE, - menu: { - id: MENU_INLINE_CHAT_WIDGET_MARKDOWN_MESSAGE, - when: ContextKeyExpr.and(ContextKeyExpr.or(CTX_INLINE_CHAT_RESPONSE_TYPES.isEqualTo(InlineChatResponseTypes.OnlyMessages), CTX_INLINE_CHAT_RESPONSE_TYPES.isEqualTo(InlineChatResponseTypes.Mixed)), CTX_INLINE_CHAT_MESSAGE_CROP_STATE.isEqualTo('expanded')), - group: '2_expandOrContract', - order: 1 - } - }); - } - override runInlineChatCommand(_accessor: ServicesAccessor, ctrl: InlineChatController, _editor: ICodeEditor, ..._args: any[]): void { - ctrl.updateExpansionState(false); - } -} - export class InlineAccessibilityHelpContribution extends Disposable { constructor() { super(); diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts index 775b62fee68..e2feff8c52f 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts @@ -39,10 +39,11 @@ import { IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; import { chatAgentLeader, chatSubcommandLeader } from 'vs/workbench/contrib/chat/common/chatParserTypes'; import { IChatService } from 'vs/workbench/contrib/chat/common/chatService'; import { IInlineChatSavingService } from './inlineChatSavingService'; -import { EmptyResponse, ErrorResponse, ExpansionState, ReplyResponse, Session, SessionExchange, SessionPrompt } from 'vs/workbench/contrib/inlineChat/browser/inlineChatSession'; +import { EmptyResponse, ErrorResponse, ReplyResponse, Session, SessionExchange, SessionPrompt } from 'vs/workbench/contrib/inlineChat/browser/inlineChatSession'; import { IInlineChatSessionService } from './inlineChatSessionService'; import { EditModeStrategy, IEditObserver, LiveStrategy, PreviewStrategy, ProgressingEditsOptions } from 'vs/workbench/contrib/inlineChat/browser/inlineChatStrategies'; -import { IInlineChatMessageAppender, InlineChatZoneWidget } from 'vs/workbench/contrib/inlineChat/browser/inlineChatWidget'; +import { IInlineChatMessageAppender } from 'vs/workbench/contrib/inlineChat/browser/inlineChatWidget'; +import { InlineChatZoneWidget } from './inlineChatZoneWidget'; import { CTX_INLINE_CHAT_DID_EDIT, CTX_INLINE_CHAT_HAS_ACTIVE_REQUEST, CTX_INLINE_CHAT_LAST_FEEDBACK, CTX_INLINE_CHAT_RESPONSE_TYPES, CTX_INLINE_CHAT_SUPPORT_ISSUE_REPORTING, CTX_INLINE_CHAT_USER_DID_EDIT, EditMode, IInlineChatProgressItem, IInlineChatRequest, IInlineChatResponse, INLINE_CHAT_ID, InlineChatConfigKeys, InlineChatResponseFeedbackKind, InlineChatResponseTypes } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { StashedSession } from './inlineChatSession'; @@ -379,7 +380,6 @@ export class InlineChatController implements IEditorContribution { this._zone.value.widget.updateSlashCommands(this._session.session.slashCommands ?? []); this._updatePlaceholder(); this._zone.value.widget.updateInfo(this._session.session.message ?? localize('welcome.1', "AI-generated code may be incorrect")); - this._zone.value.widget.preferredExpansionState = this._session.lastExpansionState; this._zone.value.widget.value = this._session.session.input ?? this._session.lastInput?.value ?? this._zone.value.widget.value; if (this._session.session.input) { this._zone.value.widget.selectAll(); @@ -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; @@ -795,8 +795,6 @@ export class InlineChatController implements IEditorContribution { const message = { message: response.mdContent, providerId: this._session.provider.debugName, requestId: response.requestId }; this._zone.value.widget.updateChatMessage(message); - //this._zone.value.widget.updateMarkdownMessage(response.mdContent); - this._session.lastExpansionState = this._zone.value.widget.expansionState; this._zone.value.widget.updateToolbar(true); newPosition = await this._strategy.renderChanges(response); @@ -1088,14 +1086,6 @@ export class InlineChatController implements IEditorContribution { } } - updateExpansionState(expand: boolean) { - if (this._session) { - const expansionState = expand ? ExpansionState.EXPANDED : ExpansionState.CROPPED; - this._zone.value.widget.updateChatMessageExpansionState(expansionState); - this._session.lastExpansionState = expansionState; - } - } - toggleDiff() { this._strategy?.toggleDiff?.(); } diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatSession.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatSession.ts index a3e3865631d..109875de35f 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatSession.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatSession.ts @@ -67,11 +67,6 @@ export type TelemetryDataClassification = { responseTypes: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Comma separated list of response types like edits, message, mixed' }; }; -export enum ExpansionState { - EXPANDED = 'expanded', - CROPPED = 'cropped', - NOT_CROPPED = 'not_cropped' -} export class SessionWholeRange { @@ -142,7 +137,6 @@ export class SessionWholeRange { export class Session { private _lastInput: SessionPrompt | undefined; - private _lastExpansionState: ExpansionState | undefined; private _isUnstashed: boolean = false; private readonly _exchange: SessionExchange[] = []; private readonly _startTime = new Date(); @@ -204,14 +198,6 @@ export class Session { this._isUnstashed = true; } - get lastExpansionState(): ExpansionState | undefined { - return this._lastExpansionState; - } - - set lastExpansionState(state: ExpansionState) { - this._lastExpansionState = state; - } - get textModelNSnapshotAltVersion(): number | undefined { return this._textModelNSnapshotAltVersion; } diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatStrategies.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatStrategies.ts index 30aa3f46b10..500d978c389 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatStrategies.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatStrategies.ts @@ -30,7 +30,7 @@ import { SaveReason } from 'vs/workbench/common/editor'; import { countWords } from 'vs/workbench/contrib/chat/common/chatWordCounter'; import { InlineChatFileCreatePreviewWidget } from 'vs/workbench/contrib/inlineChat/browser/inlineChatFileCreationWidget'; import { HunkInformation, ReplyResponse, Session } from 'vs/workbench/contrib/inlineChat/browser/inlineChatSession'; -import { InlineChatZoneWidget } from 'vs/workbench/contrib/inlineChat/browser/inlineChatWidget'; +import { InlineChatZoneWidget } from './inlineChatZoneWidget'; import { CTX_INLINE_CHAT_CHANGE_HAS_DIFF, CTX_INLINE_CHAT_CHANGE_SHOWS_DIFF, CTX_INLINE_CHAT_DOCUMENT_CHANGED, InlineChatConfigKeys, overviewRulerInlineChatDiffInserted } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; import { HunkState } from './inlineChatSession'; import { assertType } from 'vs/base/common/types'; diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts index 5af8f2acc53..2b9b26cf68f 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts @@ -13,20 +13,19 @@ import { IMarkdownString, MarkdownString } from 'vs/base/common/htmlContent'; import { Lazy } from 'vs/base/common/lazy'; import { DisposableStore, MutableDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { ISettableObservable, constObservable, derived, observableValue } from 'vs/base/common/observable'; -import { assertType } from 'vs/base/common/types'; import { URI } from 'vs/base/common/uri'; import 'vs/css!./inlineChat'; import { IEditorConstructionOptions } from 'vs/editor/browser/config/editorConfiguration'; import { IActiveCodeEditor, ICodeEditor, IDiffEditorConstructionOptions } from 'vs/editor/browser/editorBrowser'; import { EditorExtensionsRegistry } from 'vs/editor/browser/editorExtensions'; -import { ICodeEditorWidgetOptions } from 'vs/editor/browser/widget/codeEditor/codeEditorWidget'; +import { CodeEditorWidget, ICodeEditorWidgetOptions } from 'vs/editor/browser/widget/codeEditor/codeEditorWidget'; import { AccessibleDiffViewer, IAccessibleDiffViewerModel } from 'vs/editor/browser/widget/diffEditor/components/accessibleDiffViewer'; import { EmbeddedDiffEditorWidget } from 'vs/editor/browser/widget/diffEditor/embeddedDiffEditorWidget'; import { EmbeddedCodeEditorWidget } from 'vs/editor/browser/widget/codeEditor/embeddedCodeEditorWidget'; -import { EditorLayoutInfo, EditorOption, IComputedEditorOptions } from 'vs/editor/common/config/editorOptions'; +import { EditorOption, IComputedEditorOptions } from 'vs/editor/common/config/editorOptions'; import { LineRange } from 'vs/editor/common/core/lineRange'; import { Position } from 'vs/editor/common/core/position'; -import { IRange, Range } from 'vs/editor/common/core/range'; +import { Range } from 'vs/editor/common/core/range'; import { DetailedLineRangeMapping, RangeMapping } from 'vs/editor/common/diff/rangeMapping'; import { ICodeEditorViewState, ScrollType } from 'vs/editor/common/editorCommon'; import { LanguageSelector } from 'vs/editor/common/languageSelector'; @@ -37,7 +36,6 @@ import { IModelService } from 'vs/editor/common/services/model'; import { ITextModelService } from 'vs/editor/common/services/resolverService'; import { SnippetController2 } from 'vs/editor/contrib/snippet/browser/snippetController2'; import { SuggestController } from 'vs/editor/contrib/suggest/browser/suggestController'; -import { ZoneWidget } from 'vs/editor/contrib/zoneWidget/browser/zoneWidget'; import { localize } from 'vs/nls'; import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility'; import { IWorkbenchButtonBarOptions, MenuWorkbenchButtonBar } from 'vs/platform/actions/browser/buttonbar'; @@ -63,11 +61,12 @@ import { IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; import { ChatModel, ChatResponseModel } from 'vs/workbench/contrib/chat/common/chatModel'; import { ChatResponseViewModel } from 'vs/workbench/contrib/chat/common/chatViewModel'; import { CodeBlockModelCollection } from 'vs/workbench/contrib/chat/common/codeBlockModelCollection'; -import { ExpansionState, HunkData, HunkInformation, Session } from 'vs/workbench/contrib/inlineChat/browser/inlineChatSession'; +import { HunkData, HunkInformation, Session } from 'vs/workbench/contrib/inlineChat/browser/inlineChatSession'; import { asRange, invertLineRange } from 'vs/workbench/contrib/inlineChat/browser/utils'; -import { ACTION_ACCEPT_CHANGES, ACTION_REGENERATE_RESPONSE, ACTION_VIEW_IN_CHAT, CTX_INLINE_CHAT_EMPTY, CTX_INLINE_CHAT_FOCUSED, CTX_INLINE_CHAT_INNER_CURSOR_END, CTX_INLINE_CHAT_INNER_CURSOR_FIRST, CTX_INLINE_CHAT_INNER_CURSOR_LAST, CTX_INLINE_CHAT_INNER_CURSOR_START, CTX_INLINE_CHAT_MESSAGE_CROP_STATE, CTX_INLINE_CHAT_OUTER_CURSOR_POSITION, CTX_INLINE_CHAT_RESPONSE_FOCUSED, CTX_INLINE_CHAT_VISIBLE, IInlineChatFollowup, IInlineChatSlashCommand, MENU_INLINE_CHAT_INPUT, MENU_INLINE_CHAT_WIDGET, MENU_INLINE_CHAT_WIDGET_FEEDBACK, MENU_INLINE_CHAT_WIDGET_MARKDOWN_MESSAGE, MENU_INLINE_CHAT_WIDGET_STATUS } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; +import { CTX_INLINE_CHAT_EMPTY, CTX_INLINE_CHAT_FOCUSED, CTX_INLINE_CHAT_INNER_CURSOR_END, CTX_INLINE_CHAT_INNER_CURSOR_FIRST, CTX_INLINE_CHAT_INNER_CURSOR_LAST, CTX_INLINE_CHAT_INNER_CURSOR_START, CTX_INLINE_CHAT_RESPONSE_FOCUSED, IInlineChatFollowup, IInlineChatSlashCommand } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; import { IUntitledTextEditorModel } from 'vs/workbench/services/untitled/common/untitledTextEditorModel'; import { createInstantHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegateFactory'; +import { DomScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableElement'; const defaultAriaLabel = localize('aria-label', "Inline Chat Input"); @@ -137,9 +136,25 @@ export interface InlineChatWidgetViewState { } export interface IInlineChatWidgetConstructionOptions { - menuId: MenuId; + /** + * The telemetry source for all commands of this widget + */ + telemetrySource: string; + /** + * The menu that is inside the input editor, use for send, dictation + */ + inputMenuId: MenuId; + /** + * The menu that next to the input editor, use for close, config etc + */ widgetMenuId: MenuId; - statusMenuId: MenuId; + /** + * The menu that rendered as button bar, use for accept, discard etc + */ + statusMenuId: MenuId | { menu: MenuId; options: IWorkbenchButtonBarOptions }; + /** + * The men that rendered in the lower right corner, use for feedback + */ feedbackMenuId: MenuId; } @@ -177,10 +192,7 @@ export class InlineChatWidget { h('div.previewDiff.hidden@previewDiff'), h('div.previewCreateTitle.show-file-icons@previewCreateTitle'), h('div.previewCreate.hidden@previewCreate'), - h('div.chatMessage.hidden@chatMessage', [ - h('div.chatMessageContent@chatMessageContent'), - h('div.messageActions@messageActions') - ]), + h('div.chatMessage.hidden@chatMessage'), h('div.followUps.hidden@followUps'), h('div.accessibleViewer@accessibleViewer'), h('div.status@status', [ @@ -192,13 +204,15 @@ export class InlineChatWidget { ] ); + private readonly _chatMessageContents: HTMLDivElement; + private readonly _chatMessageScrollable: DomScrollableElement; + private readonly _store = new DisposableStore(); private readonly _slashCommands = this._store.add(new DisposableStore()); private readonly _inputEditor: IActiveCodeEditor; private readonly _inputModel: ITextModel; private readonly _ctxInputEmpty: IContextKey; - private readonly _ctxMessageCropState: IContextKey<'cropped' | 'not_cropped' | 'expanded'>; private readonly _ctxInnerCursorFirst: IContextKey; private readonly _ctxInnerCursorLast: IContextKey; private readonly _ctxInnerCursorStart: IContextKey; @@ -229,8 +243,6 @@ export class InlineChatWidget { private _lastDim: Dimension | undefined; private _isLayouting: boolean = false; - private _preferredExpansionState: ExpansionState | undefined; - private _expansionState: ExpansionState = ExpansionState.NOT_CROPPED; private _slashCommandDetails: { command: string; detail: string }[] = []; private _slashCommandContentWidget: SlashCommandContentWidget; @@ -244,8 +256,8 @@ export class InlineChatWidget { private readonly _codeBlockModelCollection: CodeBlockModelCollection; constructor( - private readonly parentEditor: ICodeEditor, - _options: IInlineChatWidgetConstructionOptions, + private readonly _parentEditor: ICodeEditor, + options: IInlineChatWidgetConstructionOptions, @IModelService private readonly _modelService: IModelService, @IContextKeyService private readonly _contextKeyService: IContextKeyService, @ILanguageFeaturesService private readonly _languageFeaturesService: ILanguageFeaturesService, @@ -268,20 +280,13 @@ export class InlineChatWidget { ]) }; - this._inputEditor = this._instantiationService.createInstance(EmbeddedCodeEditorWidget, this._elements.editor, _inputEditorOptions, codeEditorWidgetOptions, this.parentEditor); + this._inputEditor = this._instantiationService.createInstance(CodeEditorWidget, this._elements.editor, _inputEditorOptions, codeEditorWidgetOptions); this._updateAriaLabel(); this._store.add(this._inputEditor); this._store.add(this._inputEditor.onDidChangeModelContent(() => this._onDidChangeInput.fire(this))); this._store.add(this._inputEditor.onDidLayoutChange(() => this._onDidChangeHeight.fire())); this._store.add(this._inputEditor.onDidContentSizeChange(() => this._onDidChangeHeight.fire())); - this._store.add(addDisposableListener(this._elements.chatMessageContent, 'focus', () => this._ctxResponseFocused.set(true))); - this._store.add(addDisposableListener(this._elements.chatMessageContent, 'blur', () => this._ctxResponseFocused.reset())); - this._store.add(this._configurationService.onDidChangeConfiguration(e => { - if (e.affectsConfiguration(AccessibilityVerbositySettingId.InlineChat)) { - this._updateAriaLabel(); - } - })); const uri = URI.from({ scheme: 'vscode', authority: 'inline-chat', path: `/inline-chat/model${InlineChatWidget._modelPool++}.txt` }); this._inputModel = this._store.add(this._modelService.getModel(uri) ?? this._modelService.createModel('', null, uri)); @@ -289,10 +294,24 @@ export class InlineChatWidget { this._editorOptions = this._store.add(_instantiationService.createInstance(ChatEditorOptions, undefined, editorForeground, inputBackground, editorBackground)); + this._chatMessageContents = document.createElement('div'); + this._chatMessageContents.className = 'chatMessageContent'; + this._chatMessageContents.tabIndex = 0; + this._chatMessageContents.ariaLabel = this._accessibleViewService.getOpenAriaHint(AccessibilityVerbositySettingId.InlineChat); + this._chatMessageScrollable = new DomScrollableElement(this._chatMessageContents, { alwaysConsumeMouseWheel: true }); + this._store.add(this._chatMessageScrollable); + this._elements.chatMessage.appendChild(this._chatMessageScrollable.getDomNode()); + this._store.add(addDisposableListener(this._chatMessageContents, 'focus', () => this._ctxResponseFocused.set(true))); + this._store.add(addDisposableListener(this._chatMessageContents, 'blur', () => this._ctxResponseFocused.reset())); + + this._store.add(this._configurationService.onDidChangeConfiguration(e => { + if (e.affectsConfiguration(AccessibilityVerbositySettingId.InlineChat)) { + this._updateAriaLabel(); + } + })); // --- context keys - this._ctxMessageCropState = CTX_INLINE_CHAT_MESSAGE_CROP_STATE.bindTo(this._contextKeyService); this._ctxInputEmpty = CTX_INLINE_CHAT_EMPTY.bindTo(this._contextKeyService); this._ctxInnerCursorFirst = CTX_INLINE_CHAT_INNER_CURSOR_FIRST.bindTo(this._contextKeyService); @@ -381,8 +400,8 @@ export class InlineChatWidget { // toolbars - this._store.add(this._instantiationService.createInstance(MenuWorkbenchToolBar, this._elements.editorToolbar, _options.menuId, { - telemetrySource: 'interactiveEditorWidget-toolbar', + this._store.add(this._instantiationService.createInstance(MenuWorkbenchToolBar, this._elements.editorToolbar, options.inputMenuId, { + telemetrySource: options.telemetrySource, toolbarOptions: { primaryGroup: 'main' }, hiddenItemStrategy: HiddenItemStrategy.Ignore, // keep it lean when hiding items and avoid a "..." overflow menu hoverDelegate @@ -392,25 +411,17 @@ export class InlineChatWidget { this._store.add(this._progressBar); - this._store.add(this._instantiationService.createInstance(MenuWorkbenchToolBar, this._elements.widgetToolbar, _options.widgetMenuId, { - telemetrySource: 'interactiveEditorWidget-toolbar', + this._store.add(this._instantiationService.createInstance(MenuWorkbenchToolBar, this._elements.widgetToolbar, options.widgetMenuId, { + telemetrySource: options.telemetrySource, toolbarOptions: { primaryGroup: 'main' }, hoverDelegate })); - const workbenchMenubarOptions: IWorkbenchButtonBarOptions = { - telemetrySource: 'interactiveEditorWidget-toolbar', - buttonConfigProvider: action => { - if (action.id === ACTION_REGENERATE_RESPONSE) { - return { showIcon: true, showLabel: false, isSecondary: true }; - } else if (action.id === ACTION_VIEW_IN_CHAT || action.id === ACTION_ACCEPT_CHANGES) { - return { isSecondary: false }; - } else { - return { isSecondary: true }; - } - } - }; - const statusButtonBar = this._instantiationService.createInstance(MenuWorkbenchButtonBar, this._elements.statusToolbar, _options.statusMenuId, workbenchMenubarOptions); + + const statusMenuId = options.statusMenuId instanceof MenuId ? options.statusMenuId : options.statusMenuId.menu; + const statusMenuOptions = options.statusMenuId instanceof MenuId ? undefined : options.statusMenuId.options; + + const statusButtonBar = this._instantiationService.createInstance(MenuWorkbenchButtonBar, this._elements.statusToolbar, statusMenuId, statusMenuOptions); this._store.add(statusButtonBar.onDidChange(() => this._onDidChangeHeight.fire())); this._store.add(statusButtonBar); @@ -423,7 +434,7 @@ export class InlineChatWidget { } }; - const feedbackToolbar = this._instantiationService.createInstance(MenuWorkbenchToolBar, this._elements.feedbackToolbar, _options.feedbackMenuId, { ...workbenchToolbarOptions, hiddenItemStrategy: HiddenItemStrategy.Ignore }); + const feedbackToolbar = this._instantiationService.createInstance(MenuWorkbenchToolBar, this._elements.feedbackToolbar, options.feedbackMenuId, { ...workbenchToolbarOptions, hiddenItemStrategy: HiddenItemStrategy.Ignore }); this._store.add(feedbackToolbar.onDidChangeMenuItems(() => this._onDidChangeHeight.fire())); this._store.add(feedbackToolbar); @@ -432,24 +443,19 @@ export class InlineChatWidget { useInlineViewWhenSpaceIsLimited: false, ..._previewEditorEditorOptions, onlyShowAccessibleDiffViewer: this._accessibilityService.isScreenReaderOptimized(), - }, { modifiedEditor: codeEditorWidgetOptions, originalEditor: codeEditorWidgetOptions }, parentEditor))); + }, { modifiedEditor: codeEditorWidgetOptions, originalEditor: codeEditorWidgetOptions }, _parentEditor))); this._previewCreateTitle = this._store.add(_instantiationService.createInstance(ResourceLabel, this._elements.previewCreateTitle, { supportIcons: true })); - this._previewCreateEditor = new Lazy(() => this._store.add(_instantiationService.createInstance(EmbeddedCodeEditorWidget, this._elements.previewCreate, _previewEditorEditorOptions, codeEditorWidgetOptions, parentEditor))); + this._previewCreateEditor = new Lazy(() => this._store.add(_instantiationService.createInstance(EmbeddedCodeEditorWidget, this._elements.previewCreate, _previewEditorEditorOptions, codeEditorWidgetOptions, _parentEditor))); - this._elements.chatMessageContent.tabIndex = 0; - this._elements.chatMessageContent.ariaLabel = this._accessibleViewService.getOpenAriaHint(AccessibilityVerbositySettingId.InlineChat); this._elements.followUps.tabIndex = 0; this._elements.followUps.ariaLabel = this._accessibleViewService.getOpenAriaHint(AccessibilityVerbositySettingId.InlineChat); this._elements.statusLabel.tabIndex = 0; - const markdownMessageToolbar = this._instantiationService.createInstance(MenuWorkbenchToolBar, this._elements.messageActions, MENU_INLINE_CHAT_WIDGET_MARKDOWN_MESSAGE, workbenchToolbarOptions); - this._store.add(markdownMessageToolbar.onDidChangeMenuItems(() => this._onDidChangeHeight.fire())); - this._store.add(markdownMessageToolbar); this._store.add(this._configurationService.onDidChangeConfiguration(e => { if (e.affectsConfiguration(AccessibilityVerbositySettingId.InlineChat)) { - this._elements.chatMessageContent.ariaLabel = this._accessibleViewService.getOpenAriaHint(AccessibilityVerbositySettingId.InlineChat); + this._chatMessageContents.ariaLabel = this._accessibleViewService.getOpenAriaHint(AccessibilityVerbositySettingId.InlineChat); this._elements.followUps.ariaLabel = this._accessibleViewService.getOpenAriaHint(AccessibilityVerbositySettingId.InlineChat); } })); @@ -475,7 +481,6 @@ export class InlineChatWidget { dispose(): void { this._store.dispose(); this._ctxInputEmpty.reset(); - this._ctxMessageCropState.reset(); } get domNode(): HTMLElement { @@ -488,6 +493,9 @@ export class InlineChatWidget { if (this._accessibleViewer.value) { this._accessibleViewer.value.width = _dim.width - 12; } + this._chatMessageContents.style.width = `${_dim.width - 10}px`; + this._chatMessageContents.style.maxHeight = `270px`; + const widgetToolbarWidth = getTotalWidth(this._elements.widgetToolbar); const editorToolbarWidth = getTotalWidth(this._elements.editorToolbar) + 8 /* L/R-padding */; const innerEditorWidth = _dim.width - editorToolbarWidth - widgetToolbarWidth; @@ -510,12 +518,6 @@ export class InlineChatWidget { this._elements.previewCreate.style.height = `${previewCreateDim.height}px`; } - - const lineHeight = this.parentEditor.getOption(EditorOption.lineHeight); - const editorHeight = this.parentEditor.getLayoutInfo().height; - const editorHeightInLines = Math.floor(editorHeight / lineHeight); - this._elements.root.style.setProperty('--vscode-inline-chat-cropped', String(Math.floor(editorHeightInLines / 5))); - this._elements.root.style.setProperty('--vscode-inline-chat-expanded', String(Math.floor(editorHeightInLines / 3))); this._onDidChangeLayout.fire(); } } finally { @@ -524,16 +526,17 @@ export class InlineChatWidget { } getHeight(): number { - const base = getTotalHeight(this._elements.progress) + getTotalHeight(this._elements.status); const editorHeight = this._inputEditor.getContentHeight() + 12 /* padding and border */; + const progressHeight = getTotalHeight(this._elements.progress); const detectedIntentHeight = getTotalHeight(this._elements.detectedIntent); + const chatResponseHeight = getTotalHeight(this._chatMessageContents) + 16 /*padding*/; const followUpsHeight = getTotalHeight(this._elements.followUps); - const chatResponseHeight = getTotalHeight(this._elements.chatMessage); const previewDiffHeight = this._previewDiffEditor.hasValue && this._previewDiffEditor.value.getModel() ? 12 + Math.min(300, Math.max(0, this._previewDiffEditor.value.getContentHeight())) : 0; const previewCreateTitleHeight = getTotalHeight(this._elements.previewCreateTitle); const previewCreateHeight = this._previewCreateEditor.hasValue && this._previewCreateEditor.value.getModel() ? 18 + Math.min(300, Math.max(0, this._previewCreateEditor.value.getContentHeight())) : 0; const accessibleViewHeight = this._accessibleViewer.value?.height ?? 0; - return base + editorHeight + detectedIntentHeight + followUpsHeight + chatResponseHeight + previewDiffHeight + previewCreateTitleHeight + previewCreateHeight + accessibleViewHeight + 18 /* padding */ + 8 /*shadow*/; + const statusHeight = getTotalHeight(this._elements.status); + return progressHeight + editorHeight + detectedIntentHeight + followUpsHeight + chatResponseHeight + previewDiffHeight + previewCreateTitleHeight + previewCreateHeight + accessibleViewHeight + statusHeight + 18 /* padding */ + 8 /*shadow*/; } updateProgress(show: boolean) { @@ -589,14 +592,6 @@ export class InlineChatWidget { this._onDidChangeHeight.fire(); } - get expansionState(): ExpansionState { - return this._expansionState; - } - - set preferredExpansionState(expansionState: ExpansionState | undefined) { - this._preferredExpansionState = expansionState; - } - get responseContent(): string | undefined { return this._chatMessage?.value; } @@ -604,42 +599,31 @@ export class InlineChatWidget { updateChatMessage(message: IInlineChatMessage, isIncomplete: true): IInlineChatMessageAppender; updateChatMessage(message: IInlineChatMessage | undefined): void; updateChatMessage(message: IInlineChatMessage | undefined, isIncomplete?: boolean): IInlineChatMessageAppender | undefined { - let expansionState: ExpansionState; + this._chatMessageDisposables.clear(); this._chatMessage = message ? new MarkdownString(message.message.value) : undefined; const hasMessage = message?.message.value; this._elements.chatMessage.classList.toggle('hidden', !hasMessage); - reset(this._elements.chatMessageContent); + reset(this._chatMessageContents); let resultingAppender: IInlineChatMessageAppender | undefined; - if (!hasMessage) { - this._ctxMessageCropState.reset(); - expansionState = ExpansionState.NOT_CROPPED; - } else { - const sessionModel = this._chatMessageDisposables.add(new ChatModel(message.providerId, undefined, this._logService, this._chatAgentService)); + if (hasMessage) { + 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 }; const chatRendererDelegate: IChatRendererDelegate = { getListLength() { return 1; } }; const renderer = this._chatMessageDisposables.add(this._instantiationService.createInstance(ChatListItemRenderer, this._editorOptions, renderOptions, chatRendererDelegate, this._codeBlockModelCollection, undefined)); - renderer.layout(this._elements.chatMessageContent.clientWidth - 4); // 2 for the padding used for the tab index border + renderer.layout(this._chatMessageContents.clientWidth - 4); // 2 for the padding used for the tab index border this._chatMessageDisposables.add(this._onDidChangeLayout.event(() => { - renderer.layout(this._elements.chatMessageContent.clientWidth - 4); + renderer.layout(this._chatMessageContents.clientWidth - 4); + this._chatMessageScrollable.scanDomNode(); })); - const template = renderer.renderTemplate(this._elements.chatMessageContent); + const template = renderer.renderTemplate(this._chatMessageContents); this._chatMessageDisposables.add(template.elementDisposables); this._chatMessageDisposables.add(template.templateDisposables); renderer.renderChatTreeItem(viewModel, 0, template); this._chatMessageDisposables.add(renderer.onDidChangeItemHeight(() => this._onDidChangeHeight.fire())); - if (this._preferredExpansionState) { - expansionState = this._preferredExpansionState; - this._preferredExpansionState = undefined; - } else { - this._updateLineClamp(ExpansionState.CROPPED); - expansionState = template.value.scrollHeight > template.value.clientHeight ? ExpansionState.CROPPED : ExpansionState.NOT_CROPPED; - } - this._ctxMessageCropState.set(expansionState); - this._updateLineClamp(expansionState); resultingAppender = isIncomplete ? { cancel: () => responseModel.cancel(), complete: () => responseModel.complete(), @@ -649,7 +633,6 @@ export class InlineChatWidget { } } : undefined; } - this._expansionState = expansionState; this._onDidChangeHeight.fire(); return resultingAppender; } @@ -667,21 +650,6 @@ export class InlineChatWidget { this._onDidChangeHeight.fire(); } - updateChatMessageExpansionState(expansionState: ExpansionState) { - this._ctxMessageCropState.set(expansionState); - const heightBefore = this._elements.chatMessageContent.scrollHeight; - this._updateLineClamp(expansionState); - const heightAfter = this._elements.chatMessageContent.scrollHeight; - if (heightBefore === heightAfter) { - this._ctxMessageCropState.set(ExpansionState.NOT_CROPPED); - } - this._onDidChangeHeight.fire(); - } - - private _updateLineClamp(expansionState: ExpansionState) { - this._elements.chatMessageContent.setAttribute('state', expansionState); - } - updateSlashCommandUsed(command: string): void { const details = this._slashCommandDetails.find(candidate => candidate.command === command); if (!details) { @@ -948,7 +916,7 @@ export class InlineChatWidget { this._elements.accessibleViewer, session, hunkData, - new AccessibleHunk(this.parentEditor, session, hunkData) + new AccessibleHunk(this._parentEditor, session, hunkData) ); this._onDidChangeHeight.fire(); @@ -956,160 +924,6 @@ export class InlineChatWidget { } } -export class InlineChatZoneWidget extends ZoneWidget { - - readonly widget: InlineChatWidget; - - private readonly _ctxVisible: IContextKey; - private readonly _ctxCursorPosition: IContextKey<'above' | 'below' | ''>; - private _dimension?: Dimension; - private _indentationWidth: number | undefined; - - constructor( - editor: ICodeEditor, - @IInstantiationService private readonly _instaService: IInstantiationService, - @IContextKeyService contextKeyService: IContextKeyService, - ) { - super(editor, { showFrame: false, showArrow: false, isAccessible: true, className: 'inline-chat-widget', keepEditorSelection: true, showInHiddenAreas: true, ordinal: 10000 }); - - this._ctxVisible = CTX_INLINE_CHAT_VISIBLE.bindTo(contextKeyService); - this._ctxCursorPosition = CTX_INLINE_CHAT_OUTER_CURSOR_POSITION.bindTo(contextKeyService); - - this._disposables.add(toDisposable(() => { - this._ctxVisible.reset(); - this._ctxCursorPosition.reset(); - })); - - this.widget = this._instaService.createInstance(InlineChatWidget, this.editor, { - menuId: MENU_INLINE_CHAT_INPUT, - widgetMenuId: MENU_INLINE_CHAT_WIDGET, - statusMenuId: MENU_INLINE_CHAT_WIDGET_STATUS, - feedbackMenuId: MENU_INLINE_CHAT_WIDGET_FEEDBACK - }); - this._disposables.add(this.widget.onDidChangeHeight(() => this._relayout())); - this._disposables.add(this.widget); - this.create(); - - - this._disposables.add(addDisposableListener(this.domNode, 'click', e => { - if (!this.widget.hasFocus()) { - this.widget.focus(); - } - }, true)); - - // todo@jrieken listen ONLY when showing - const updateCursorIsAboveContextKey = () => { - if (!this.position || !this.editor.hasModel()) { - this._ctxCursorPosition.reset(); - } else if (this.position.lineNumber === this.editor.getPosition().lineNumber) { - this._ctxCursorPosition.set('above'); - } else if (this.position.lineNumber + 1 === this.editor.getPosition().lineNumber) { - this._ctxCursorPosition.set('below'); - } else { - this._ctxCursorPosition.reset(); - } - }; - this._disposables.add(this.editor.onDidChangeCursorPosition(e => updateCursorIsAboveContextKey())); - this._disposables.add(this.editor.onDidFocusEditorText(e => updateCursorIsAboveContextKey())); - updateCursorIsAboveContextKey(); - } - - protected override _fillContainer(container: HTMLElement): void { - container.appendChild(this.widget.domNode); - } - - - protected override _doLayout(heightInPixel: number): void { - - const maxWidth = !this.widget.showsAnyPreview() ? 640 : Number.MAX_SAFE_INTEGER; - const width = Math.min(maxWidth, this._availableSpaceGivenIndentation(this._indentationWidth)); - this._dimension = new Dimension(width, heightInPixel); - this.widget.domNode.style.width = `${width}px`; - this.widget.layout(this._dimension); - } - - private _availableSpaceGivenIndentation(indentationWidth: number | undefined): number { - const info = this.editor.getLayoutInfo(); - return info.contentWidth - (info.glyphMarginWidth + info.decorationsWidth + (indentationWidth ?? 0)); - } - - private _computeHeightInLines(): number { - const lineHeight = this.editor.getOption(EditorOption.lineHeight); - return this.widget.getHeight() / lineHeight; - } - - protected override _relayout() { - if (this._dimension) { - this._doLayout(this._dimension.height); - } - super._relayout(this._computeHeightInLines()); - } - - override show(position: Position): void { - super.show(position, this._computeHeightInLines()); - this.widget.focus(); - this._ctxVisible.set(true); - } - - protected override _getWidth(info: EditorLayoutInfo): number { - return info.width - info.minimap.minimapWidth; - } - - updateBackgroundColor(newPosition: Position, wholeRange: IRange) { - assertType(this.container); - const widgetLineNumber = newPosition.lineNumber; - this.container.classList.toggle('inside-selection', widgetLineNumber > wholeRange.startLineNumber && widgetLineNumber < wholeRange.endLineNumber); - } - - private _calculateIndentationWidth(position: Position): number { - const viewModel = this.editor._getViewModel(); - if (!viewModel) { - return 0; - } - const visibleRange = viewModel.getCompletelyVisibleViewRange(); - const startLineVisibleRange = visibleRange.startLineNumber; - const positionLine = position.lineNumber; - let indentationLineNumber: number | undefined; - let indentationLevel: number | undefined; - for (let lineNumber = positionLine; lineNumber >= startLineVisibleRange; lineNumber--) { - const currentIndentationLevel = viewModel.getLineFirstNonWhitespaceColumn(lineNumber); - if (currentIndentationLevel !== 0) { - indentationLineNumber = lineNumber; - indentationLevel = currentIndentationLevel; - break; - } - } - return this.editor.getOffsetForColumn(indentationLineNumber ?? positionLine, indentationLevel ?? viewModel.getLineFirstNonWhitespaceColumn(positionLine)); - } - - setContainerMargins(): void { - assertType(this.container); - - const info = this.editor.getLayoutInfo(); - const marginWithoutIndentation = info.glyphMarginWidth + info.decorationsWidth + info.lineNumbersWidth; - this.container.style.marginLeft = `${marginWithoutIndentation}px`; - } - - setWidgetMargins(position: Position): void { - const indentationWidth = this._calculateIndentationWidth(position); - if (this._indentationWidth === indentationWidth) { - return; - } - this._indentationWidth = this._availableSpaceGivenIndentation(indentationWidth) > 400 ? indentationWidth : 0; - this.widget.domNode.style.marginLeft = `${this._indentationWidth}px`; - this.widget.domNode.style.marginRight = `${this.editor.getLayoutInfo().minimap.minimapWidth}px`; - } - - override hide(): void { - this.container!.classList.remove('inside-selection'); - this._ctxVisible.reset(); - this._ctxCursorPosition.reset(); - this.widget.reset(); - super.hide(); - aria.status(localize('inlineChatClosed', 'Closed inline chat widget')); - } -} - class HunkAccessibleDiffViewer extends AccessibleDiffViewer { readonly height: number; diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatZoneWidget.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatZoneWidget.ts new file mode 100644 index 00000000000..a6cdc9e5a29 --- /dev/null +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatZoneWidget.ts @@ -0,0 +1,193 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import { Dimension, addDisposableListener } from 'vs/base/browser/dom'; +import * as aria from 'vs/base/browser/ui/aria/aria'; +import { toDisposable } from 'vs/base/common/lifecycle'; +import { assertType } from 'vs/base/common/types'; +import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; +import { EditorLayoutInfo, EditorOption } from 'vs/editor/common/config/editorOptions'; +import { Position } from 'vs/editor/common/core/position'; +import { IRange } from 'vs/editor/common/core/range'; +import { ZoneWidget } from 'vs/editor/contrib/zoneWidget/browser/zoneWidget'; +import { localize } from 'vs/nls'; +import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { ACTION_ACCEPT_CHANGES, ACTION_REGENERATE_RESPONSE, ACTION_VIEW_IN_CHAT, CTX_INLINE_CHAT_OUTER_CURSOR_POSITION, CTX_INLINE_CHAT_VISIBLE, MENU_INLINE_CHAT_INPUT, MENU_INLINE_CHAT_WIDGET, MENU_INLINE_CHAT_WIDGET_FEEDBACK, MENU_INLINE_CHAT_WIDGET_STATUS } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; +import { InlineChatWidget } from './inlineChatWidget'; + + +export class InlineChatZoneWidget extends ZoneWidget { + + readonly widget: InlineChatWidget; + + private readonly _ctxVisible: IContextKey; + private readonly _ctxCursorPosition: IContextKey<'above' | 'below' | ''>; + private _dimension?: Dimension; + private _indentationWidth: number | undefined; + + constructor( + editor: ICodeEditor, + @IInstantiationService private readonly _instaService: IInstantiationService, + @IContextKeyService contextKeyService: IContextKeyService + ) { + super(editor, { showFrame: false, showArrow: false, isAccessible: true, className: 'inline-chat-widget', keepEditorSelection: true, showInHiddenAreas: true, ordinal: 10000 }); + + this._ctxVisible = CTX_INLINE_CHAT_VISIBLE.bindTo(contextKeyService); + this._ctxCursorPosition = CTX_INLINE_CHAT_OUTER_CURSOR_POSITION.bindTo(contextKeyService); + + this._disposables.add(toDisposable(() => { + this._ctxVisible.reset(); + this._ctxCursorPosition.reset(); + })); + + this.widget = this._instaService.createInstance(InlineChatWidget, this.editor, { + telemetrySource: 'interactiveEditorWidget-toolbar', + inputMenuId: MENU_INLINE_CHAT_INPUT, + widgetMenuId: MENU_INLINE_CHAT_WIDGET, + feedbackMenuId: MENU_INLINE_CHAT_WIDGET_FEEDBACK, + statusMenuId: { + menu: MENU_INLINE_CHAT_WIDGET_STATUS, + options: { + buttonConfigProvider: action => { + if (action.id === ACTION_REGENERATE_RESPONSE) { + return { showIcon: true, showLabel: false, isSecondary: true }; + } else if (action.id === ACTION_VIEW_IN_CHAT || action.id === ACTION_ACCEPT_CHANGES) { + return { isSecondary: false }; + } else { + return { isSecondary: true }; + } + } + } + } + }); + this._disposables.add(this.widget.onDidChangeHeight(() => this._relayout())); + this._disposables.add(this.widget); + this.create(); + + + this._disposables.add(addDisposableListener(this.domNode, 'click', e => { + if (!this.widget.hasFocus()) { + this.widget.focus(); + } + }, true)); + + // todo@jrieken listen ONLY when showing + const updateCursorIsAboveContextKey = () => { + if (!this.position || !this.editor.hasModel()) { + this._ctxCursorPosition.reset(); + } else if (this.position.lineNumber === this.editor.getPosition().lineNumber) { + this._ctxCursorPosition.set('above'); + } else if (this.position.lineNumber + 1 === this.editor.getPosition().lineNumber) { + this._ctxCursorPosition.set('below'); + } else { + this._ctxCursorPosition.reset(); + } + }; + this._disposables.add(this.editor.onDidChangeCursorPosition(e => updateCursorIsAboveContextKey())); + this._disposables.add(this.editor.onDidFocusEditorText(e => updateCursorIsAboveContextKey())); + updateCursorIsAboveContextKey(); + } + + protected override _fillContainer(container: HTMLElement): void { + container.appendChild(this.widget.domNode); + } + + + protected override _doLayout(heightInPixel: number): void { + + const maxWidth = !this.widget.showsAnyPreview() ? 640 : Number.MAX_SAFE_INTEGER; + const width = Math.min(maxWidth, this._availableSpaceGivenIndentation(this._indentationWidth)); + this._dimension = new Dimension(width, heightInPixel); + this.widget.domNode.style.width = `${width}px`; + this.widget.layout(this._dimension); + } + + private _availableSpaceGivenIndentation(indentationWidth: number | undefined): number { + const info = this.editor.getLayoutInfo(); + return info.contentWidth - (info.glyphMarginWidth + info.decorationsWidth + (indentationWidth ?? 0)); + } + + private _computeHeightInLines(): number { + const lineHeight = this.editor.getOption(EditorOption.lineHeight); + return this.widget.getHeight() / lineHeight; + } + + protected override _onWidth(_widthInPixel: number): void { + if (this._dimension) { + this._doLayout(this._dimension.height); + } + } + + protected override _relayout() { + if (this._dimension) { + this._doLayout(this._dimension.height); + } + super._relayout(this._computeHeightInLines()); + } + + override show(position: Position): void { + super.show(position, this._computeHeightInLines()); + this.widget.focus(); + this._ctxVisible.set(true); + } + + protected override _getWidth(info: EditorLayoutInfo): number { + return info.width - info.minimap.minimapWidth; + } + + updateBackgroundColor(newPosition: Position, wholeRange: IRange) { + assertType(this.container); + const widgetLineNumber = newPosition.lineNumber; + this.container.classList.toggle('inside-selection', widgetLineNumber > wholeRange.startLineNumber && widgetLineNumber < wholeRange.endLineNumber); + } + + private _calculateIndentationWidth(position: Position): number { + const viewModel = this.editor._getViewModel(); + if (!viewModel) { + return 0; + } + const visibleRange = viewModel.getCompletelyVisibleViewRange(); + const startLineVisibleRange = visibleRange.startLineNumber; + const positionLine = position.lineNumber; + let indentationLineNumber: number | undefined; + let indentationLevel: number | undefined; + for (let lineNumber = positionLine; lineNumber >= startLineVisibleRange; lineNumber--) { + const currentIndentationLevel = viewModel.getLineFirstNonWhitespaceColumn(lineNumber); + if (currentIndentationLevel !== 0) { + indentationLineNumber = lineNumber; + indentationLevel = currentIndentationLevel; + break; + } + } + return this.editor.getOffsetForColumn(indentationLineNumber ?? positionLine, indentationLevel ?? viewModel.getLineFirstNonWhitespaceColumn(positionLine)); + } + + setContainerMargins(): void { + assertType(this.container); + + const info = this.editor.getLayoutInfo(); + const marginWithoutIndentation = info.glyphMarginWidth + info.decorationsWidth + info.lineNumbersWidth; + this.container.style.marginLeft = `${marginWithoutIndentation}px`; + } + + setWidgetMargins(position: Position): void { + const indentationWidth = this._calculateIndentationWidth(position); + if (this._indentationWidth === indentationWidth) { + return; + } + this._indentationWidth = this._availableSpaceGivenIndentation(indentationWidth) > 400 ? indentationWidth : 0; + this.widget.domNode.style.marginLeft = `${this._indentationWidth}px`; + this.widget.domNode.style.marginRight = `${this.editor.getLayoutInfo().minimap.minimapWidth}px`; + } + + override hide(): void { + this.container!.classList.remove('inside-selection'); + this._ctxVisible.reset(); + this._ctxCursorPosition.reset(); + this.widget.reset(); + super.hide(); + aria.status(localize('inlineChatClosed', 'Closed inline chat widget')); + } +} diff --git a/src/vs/workbench/contrib/inlineChat/common/inlineChat.ts b/src/vs/workbench/contrib/inlineChat/common/inlineChat.ts index 10e6097adea..d44c8e11d1e 100644 --- a/src/vs/workbench/contrib/inlineChat/common/inlineChat.ts +++ b/src/vs/workbench/contrib/inlineChat/common/inlineChat.ts @@ -156,7 +156,6 @@ export const CTX_INLINE_CHAT_INNER_CURSOR_FIRST = new RawContextKey('in export const CTX_INLINE_CHAT_INNER_CURSOR_LAST = new RawContextKey('inlineChatInnerCursorLast', false, localize('inlineChatInnerCursorLast', "Whether the cursor of the iteractive editor input is on the last line")); export const CTX_INLINE_CHAT_INNER_CURSOR_START = new RawContextKey('inlineChatInnerCursorStart', false, localize('inlineChatInnerCursorStart', "Whether the cursor of the iteractive editor input is on the start of the input")); export const CTX_INLINE_CHAT_INNER_CURSOR_END = new RawContextKey('inlineChatInnerCursorEnd', false, localize('inlineChatInnerCursorEnd', "Whether the cursor of the iteractive editor input is on the end of the input")); -export const CTX_INLINE_CHAT_MESSAGE_CROP_STATE = new RawContextKey<'cropped' | 'not_cropped' | 'expanded'>('inlineChatMarkdownMessageCropState', 'not_cropped', localize('inlineChatMarkdownMessageCropState', "Whether the interactive editor message is cropped, not cropped or expanded")); export const CTX_INLINE_CHAT_OUTER_CURSOR_POSITION = new RawContextKey<'above' | 'below' | ''>('inlineChatOuterCursorPosition', '', localize('inlineChatOuterCursorPosition', "Whether the cursor of the outer editor is above or below the interactive editor input")); export const CTX_INLINE_CHAT_HAS_ACTIVE_REQUEST = new RawContextKey('inlineChatHasActiveRequest', false, localize('inlineChatHasActiveRequest', "Whether interactive editor has an active request")); export const CTX_INLINE_CHAT_HAS_STASHED_SESSION = new RawContextKey('inlineChatHasStashedSession', false, localize('inlineChatHasStashedSession', "Whether interactive editor has kept a session for quick restore")); @@ -181,7 +180,6 @@ export const ACTION_VIEW_IN_CHAT = 'inlineChat.viewInChat'; export const MENU_INLINE_CHAT_INPUT = MenuId.for('inlineChatInput'); export const MENU_INLINE_CHAT_WIDGET = MenuId.for('inlineChatWidget'); -export const MENU_INLINE_CHAT_WIDGET_MARKDOWN_MESSAGE = MenuId.for('inlineChatWidget.markdownMessage'); export const MENU_INLINE_CHAT_WIDGET_STATUS = MenuId.for('inlineChatWidget.status'); export const MENU_INLINE_CHAT_WIDGET_FEEDBACK = MenuId.for('inlineChatWidget.feedback'); export const MENU_INLINE_CHAT_WIDGET_DISCARD = MenuId.for('inlineChatWidget.undo'); 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 6fd5722bc72..276c86cb858 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 6d300714340..9f3899d7961 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 e0f91695461..d6192e7c946 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 45af28b94f0..3609b0046fa 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 6f66dbec274..ffe7a8738f5 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'); - } - } + 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 +816,7 @@ export class NotebookChatController extends Disposable implements INotebookEdito } public override dispose(): void { - this.dismiss(); - + this.dismiss(false); super.dispose(); } } @@ -896,4 +896,3 @@ export class EditStrategy { registerNotebookContribution(NotebookChatController.id, NotebookChatController); - diff --git a/src/vs/workbench/contrib/notebook/browser/diff/notebookDiffEditor.ts b/src/vs/workbench/contrib/notebook/browser/diff/notebookDiffEditor.ts index e2de1a22096..39617722b2b 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 0a4cc62f4ed..dacf253ffed 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 e7f7d018a80..399934d7c49 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 c08eb70c296..59beaaf2ef9 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/notebook/browser/viewModel/baseCellViewModel.ts b/src/vs/workbench/contrib/notebook/browser/viewModel/baseCellViewModel.ts index 548a5a7c043..dd136ac471b 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewModel/baseCellViewModel.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewModel/baseCellViewModel.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { Emitter, Event } from 'vs/base/common/event'; -import { Disposable, dispose, IDisposable, IReference } from 'vs/base/common/lifecycle'; +import { Disposable, IDisposable, IReference, MutableDisposable, dispose } from 'vs/base/common/lifecycle'; import { Mimes } from 'vs/base/common/mime'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; @@ -19,11 +19,11 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur import { IUndoRedoService } from 'vs/platform/undoRedo/common/undoRedo'; import { IWordWrapTransientState, readTransientState, writeTransientState } from 'vs/workbench/contrib/codeEditor/browser/toggleWordWrap'; import { CellEditState, CellFocusMode, CursorAtBoundary, CursorAtLineBoundary, IEditableCellViewModel, INotebookCellDecorationOptions } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { NotebookOptionsChangeEvent } from 'vs/workbench/contrib/notebook/browser/notebookOptions'; import { CellViewModelStateChangeEvent } from 'vs/workbench/contrib/notebook/browser/notebookViewEvents'; import { ViewContext } from 'vs/workbench/contrib/notebook/browser/viewModel/viewContext'; import { NotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookCellTextModel'; import { CellKind, INotebookCellStatusBarItem, INotebookSearchOptions } from 'vs/workbench/contrib/notebook/common/notebookCommon'; -import { NotebookOptionsChangeEvent } from 'vs/workbench/contrib/notebook/browser/notebookOptions'; export abstract class BaseCellViewModel extends Disposable { @@ -103,6 +103,7 @@ export abstract class BaseCellViewModel extends Disposable { private _editorViewStates: editorCommon.ICodeEditorViewState | null = null; private _editorTransientState: IWordWrapTransientState | null = null; private _resolvedCellDecorations = new Map(); + private _textModelRefChangeDisposable = this._register(new MutableDisposable()); private readonly _cellDecorationsChanged = this._register(new Emitter<{ added: INotebookCellDecorationOptions[]; removed: INotebookCellDecorationOptions[] }>()); onCellDecorationsChanged: Event<{ added: INotebookCellDecorationOptions[]; removed: INotebookCellDecorationOptions[] }> = this._cellDecorationsChanged.event; @@ -299,6 +300,7 @@ export abstract class BaseCellViewModel extends Disposable { this._textModelRef.dispose(); this._textModelRef = undefined; } + this._textModelRefChangeDisposable.clear(); } getText(): string { @@ -618,8 +620,7 @@ export abstract class BaseCellViewModel extends Disposable { if (!this._textModelRef) { throw new Error(`Cannot resolve text model for ${this.uri}`); } - - this._register(this.textModel!.onDidChangeContent(() => this.onDidChangeTextModelContent())); + this._textModelRefChangeDisposable.value = this.textModel!.onDidChangeContent(() => this.onDidChangeTextModelContent()); } return this.textModel!; diff --git a/src/vs/workbench/contrib/output/browser/outputView.ts b/src/vs/workbench/contrib/output/browser/outputView.ts index b7484919efa..53206dacd2e 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 f9b434adf8f..480d9b5ab89 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 33c9e99303b..3a5a03a9718 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 37948b2bf60..492516a79f7 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 305f9bae1c5..2afacd74992 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 c4e31c3c903..2c9474be1e9 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 0531e3a3776..5d2e48914c0 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 e7dbb128b7b..dd533aee173 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 f81dc87d394..066fbb8c836 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 0f280b7f1d0..4aa0ad102e4 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 6ea0cb4542e..17c961be050 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 a8f1bc84311..1b5d8a06a93 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 5e39773a0cc..9b3b9e4f4dd 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 0a591e21457..a90875c1384 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 c6d94a0ebf3..e71fab4b131 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 5c5fcd10aab..b6e7dc04bbb 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 76b49f5f2bf..4feb3d343ed 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 449b0d60787..84bb2199629 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 d0255933209..fb511647f02 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 43246667cfe..c32972eee7b 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 db2791f9bda..229e1b0b124 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/terminalContribExports.ts b/src/vs/workbench/contrib/terminal/browser/terminalContribExports.ts new file mode 100644 index 00000000000..86677966a9b --- /dev/null +++ b/src/vs/workbench/contrib/terminal/browser/terminalContribExports.ts @@ -0,0 +1,9 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +// This is a one-off/safe import, to expose to outside contfibs as in general we don't want them +// to touch terminalContrib either. +// eslint-disable-next-line local/code-import-patterns +export { TerminalChatController } from 'vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController'; diff --git a/src/vs/workbench/contrib/terminal/browser/terminalEditor.ts b/src/vs/workbench/contrib/terminal/browser/terminalEditor.ts index 6835b5e9c35..0ea8218fcce 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 81770243f91..b731bf456fd 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/common/terminalConfiguration.ts b/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts index 3b3cafe11ab..ecf3050bb43 100644 --- a/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts +++ b/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts @@ -660,6 +660,11 @@ const terminalConfiguration: IConfigurationNode = { type: 'boolean', default: false }, + [TerminalSettingId.ExperimentalInlineChat]: { + markdownDescription: localize('terminal.integrated.experimentalInlineChat', "Whether to enable the upcoming experimental inline terminal chat UI."), + type: 'boolean', + default: false + } } }; diff --git a/src/vs/workbench/contrib/terminal/electron-sandbox/localTerminalBackend.ts b/src/vs/workbench/contrib/terminal/electron-sandbox/localTerminalBackend.ts index 06779c99e21..cb35e8698c2 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/terminal/terminal.all.ts b/src/vs/workbench/contrib/terminal/terminal.all.ts index b49aa829f7b..e9cdc253212 100644 --- a/src/vs/workbench/contrib/terminal/terminal.all.ts +++ b/src/vs/workbench/contrib/terminal/terminal.all.ts @@ -17,6 +17,7 @@ import 'vs/workbench/contrib/terminalContrib/accessibility/browser/terminal.acce import 'vs/workbench/contrib/terminalContrib/developer/browser/terminal.developer.contribution'; import 'vs/workbench/contrib/terminalContrib/environmentChanges/browser/terminal.environmentChanges.contribution'; import 'vs/workbench/contrib/terminalContrib/find/browser/terminal.find.contribution'; +import 'vs/workbench/contrib/terminalContrib/chat/browser/terminal.chat.contribution'; import 'vs/workbench/contrib/terminalContrib/highlight/browser/terminal.highlight.contribution'; import 'vs/workbench/contrib/terminalContrib/links/browser/terminal.links.contribution'; import 'vs/workbench/contrib/terminalContrib/zoom/browser/terminal.zoom.contribution'; diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/media/terminalChatWidget.css b/src/vs/workbench/contrib/terminalContrib/chat/browser/media/terminalChatWidget.css new file mode 100644 index 00000000000..64253f67db5 --- /dev/null +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/media/terminalChatWidget.css @@ -0,0 +1,47 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +.terminal-inline-chat { + position: absolute; + left: 0; + bottom: 0; + z-index: 100; + height: auto !important; +} + +.terminal-inline-chat .inline-chat { + margin-top: 0 !important; +} + +.terminal-inline-chat.hide { + visibility: hidden; +} + +.terminal-inline-chat-response.hide { + visibility: hidden; +} + +.terminal-inline-chat .chatMessageContent { + width: 400px !important; +} + +.terminal-inline-chat .terminal-inline-chat-response { + border: 1px solid var(--vscode-input-border, transparent); + background-color: var(--vscode-panel-background); +} + +.terminal-inline-chat .terminal-inline-chat-response:has(.monaco-editor.focused) { + border-color: var(--vscode-focusBorder, transparent); +} + +.terminal-inline-chat .terminal-inline-chat-response, +.terminal-inline-chat .terminal-inline-chat-response .monaco-editor, +.terminal-inline-chat .terminal-inline-chat-response .monaco-editor .overflow-guard { + border-radius: 4px; +} + +.terminal-inline-chat .terminal-inline-chat-response { + padding-left: 8px; +} diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminal.chat.contribution.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminal.chat.contribution.ts new file mode 100644 index 00000000000..44eabbf13f6 --- /dev/null +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminal.chat.contribution.ts @@ -0,0 +1,17 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { WorkbenchPhase, registerWorkbenchContribution2 } from 'vs/workbench/common/contributions'; +import { registerTerminalContribution } from 'vs/workbench/contrib/terminal/browser/terminalExtensions'; +import { TerminalInlineChatAccessibleViewContribution } from 'vs/workbench/contrib/terminalContrib/chat/browser/terminalChatAccessibleView'; +import { TerminalChatController } from 'vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController'; + +import 'vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions'; +import { TerminalChatAccessibilityHelpContribution } from 'vs/workbench/contrib/terminalContrib/chat/browser/terminalChatAccessibilityHelp'; + +registerTerminalContribution(TerminalChatController.ID, TerminalChatController, false); + +registerWorkbenchContribution2(TerminalInlineChatAccessibleViewContribution.ID, TerminalInlineChatAccessibleViewContribution, WorkbenchPhase.Eventually); +registerWorkbenchContribution2(TerminalChatAccessibilityHelpContribution.ID, TerminalChatAccessibilityHelpContribution, WorkbenchPhase.Eventually); diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChat.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChat.ts new file mode 100644 index 00000000000..89893d6d31e --- /dev/null +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChat.ts @@ -0,0 +1,75 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { localize } from 'vs/nls'; +import { MenuId } from 'vs/platform/actions/common/actions'; +import { RawContextKey } from 'vs/platform/contextkey/common/contextkey'; + +export const enum TerminalChatCommandId { + Start = 'workbench.action.terminal.chat.start', + Close = 'workbench.action.terminal.chat.close', + Discard = 'workbench.action.terminal.chat.discard', + MakeRequest = 'workbench.action.terminal.chat.makeRequest', + Cancel = 'workbench.action.terminal.chat.cancel', + FeedbackHelpful = 'workbench.action.terminal.chat.feedbackHelpful', + FeedbackUnhelpful = 'workbench.action.terminal.chat.feedbackUnhelpful', + FeedbackReportIssue = 'workbench.action.terminal.chat.feedbackReportIssue', + RunCommand = 'workbench.action.terminal.chat.runCommand', + InsertCommand = 'workbench.action.terminal.chat.insertCommand', + ViewInChat = 'workbench.action.terminal.chat.viewInChat', +} + +export const MENU_TERMINAL_CHAT_INPUT = MenuId.for('terminalChatInput'); +export const MENU_TERMINAL_CHAT_WIDGET = MenuId.for('terminalChatWidget'); +export const MENU_TERMINAL_CHAT_WIDGET_STATUS = MenuId.for('terminalChatWidget.status'); +export const MENU_TERMINAL_CHAT_WIDGET_FEEDBACK = MenuId.for('terminalChatWidget.feedback'); +export const MENU_TERMINAL_CHAT_WIDGET_TOOLBAR = MenuId.for('terminalChatWidget.toolbar'); + +export const enum TerminalChatContextKeyStrings { + ChatFocus = 'terminalChatFocus', + ChatVisible = 'terminalChatVisible', + ChatActiveRequest = 'terminalChatActiveRequest', + ChatInputHasText = 'terminalChatInputHasText', + ChatAgentRegistered = 'terminalChatAgentRegistered', + ChatResponseEditorFocused = 'terminalChatResponseEditorFocused', + ChatResponseType = 'terminalChatResponseType', + ChatResponseSupportsIssueReporting = 'terminalChatResponseSupportsIssueReporting', + ChatSessionResponseVote = 'terminalChatSessionResponseVote', +} + +export const enum TerminalChatResponseTypes { + Message = 'message', + TerminalCommand = 'terminalCommand' +} + +export namespace TerminalChatContextKeys { + + /** Whether the chat widget is focused */ + export const focused = new RawContextKey(TerminalChatContextKeyStrings.ChatFocus, false, localize('chatFocusedContextKey', "Whether the chat view is focused.")); + + /** Whether the chat widget is visible */ + export const visible = new RawContextKey(TerminalChatContextKeyStrings.ChatVisible, false, localize('chatVisibleContextKey', "Whether the chat view is visible.")); + + /** Whether there is an active chat request */ + export const requestActive = new RawContextKey(TerminalChatContextKeyStrings.ChatActiveRequest, false, localize('chatRequestActiveContextKey', "Whether there is an active chat request.")); + + /** Whether the chat input has text */ + export const inputHasText = new RawContextKey(TerminalChatContextKeyStrings.ChatInputHasText, false, localize('chatInputHasTextContextKey', "Whether the chat input has text.")); + + /** Whether the terminal chat agent has been registered */ + export const agentRegistered = new RawContextKey(TerminalChatContextKeyStrings.ChatAgentRegistered, false, localize('chatAgentRegisteredContextKey', "Whether the terminal chat agent has been registered.")); + + /** Whether the chat response editor is focused */ + export const responseEditorFocused = new RawContextKey(TerminalChatContextKeyStrings.ChatResponseEditorFocused, false, localize('chatResponseEditorFocusedContextKey', "Whether the chat response editor is focused.")); + + /** The type of chat response, if any */ + export const responseType = new RawContextKey(TerminalChatContextKeyStrings.ChatResponseType, undefined, localize('chatResponseTypeContextKey', "The type of chat response, if any")); + + /** Whether the response supports issue reporting */ + export const responseSupportsIssueReporting = new RawContextKey(TerminalChatContextKeyStrings.ChatResponseSupportsIssueReporting, false, localize('chatResponseSupportsIssueReportingContextKey', "Whether the response supports issue reporting")); + + /** The chat vote, if any for the response, if any */ + export const sessionResponseVote = new RawContextKey(TerminalChatContextKeyStrings.ChatSessionResponseVote, undefined, { type: 'string', description: localize('interactiveSessionResponseVote', "When the response has been voted up, is set to 'up'. When voted down, is set to 'down'. Otherwise an empty string.") }); +} diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatAccessibilityHelp.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatAccessibilityHelp.ts new file mode 100644 index 00000000000..dad7747e7f1 --- /dev/null +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatAccessibilityHelp.ts @@ -0,0 +1,63 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Disposable } from 'vs/base/common/lifecycle'; +import { localize } from 'vs/nls'; +import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; +import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; +import { AccessibilityVerbositySettingId, AccessibleViewProviderId } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; +import { AccessibleViewType, IAccessibleViewService } from 'vs/workbench/contrib/accessibility/browser/accessibleView'; +import { AccessibilityHelpAction } from 'vs/workbench/contrib/accessibility/browser/accessibleViewActions'; +import { ITerminalService } from 'vs/workbench/contrib/terminal/browser/terminal'; +import { TerminalChatCommandId, TerminalChatContextKeys } from 'vs/workbench/contrib/terminalContrib/chat/browser/terminalChat'; +import { TerminalChatController } from 'vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController'; + +export class TerminalChatAccessibilityHelpContribution extends Disposable { + static ID = 'terminalChatAccessiblityHelp'; + constructor() { + super(); + this._register(AccessibilityHelpAction.addImplementation(110, 'terminalChat', runAccessibilityHelpAction, TerminalChatContextKeys.focused)); + } +} + +export async function runAccessibilityHelpAction(accessor: ServicesAccessor): Promise { + const accessibleViewService = accessor.get(IAccessibleViewService); + const terminalService = accessor.get(ITerminalService); + + const instance = terminalService.activeInstance; + if (!instance) { + return; + } + + const helpText = getAccessibilityHelpText(accessor); + accessibleViewService.show({ + id: AccessibleViewProviderId.TerminalChat, + verbositySettingKey: AccessibilityVerbositySettingId.TerminalChat, + provideContent: () => helpText, + onClose: () => TerminalChatController.get(instance)?.focus(), + options: { type: AccessibleViewType.Help } + }); +} + +export function getAccessibilityHelpText(accessor: ServicesAccessor): string { + const keybindingService = accessor.get(IKeybindingService); + const content = []; + const openAccessibleViewKeybinding = keybindingService.lookupKeybinding('editor.action.accessibleView')?.getAriaLabel(); + const runCommandKeybinding = keybindingService.lookupKeybinding(TerminalChatCommandId.RunCommand)?.getAriaLabel(); + const insertCommandKeybinding = keybindingService.lookupKeybinding(TerminalChatCommandId.InsertCommand)?.getAriaLabel(); + const makeRequestKeybinding = keybindingService.lookupKeybinding(TerminalChatCommandId.MakeRequest)?.getAriaLabel(); + const startChatKeybinding = keybindingService.lookupKeybinding(TerminalChatCommandId.Start)?.getAriaLabel(); + content.push(localize('inlineChat.overview', "Inline chat occurs within a terminal. It is useful for suggesting terminal commands. Keep in mind that AI generated code may be incorrect.")); + content.push(localize('inlineChat.access', "It can be activated using the command: Terminal: Start Chat ({0}), which will focus the input box.", startChatKeybinding)); + content.push(makeRequestKeybinding ? localize('inlineChat.input', "The input box is where the user can type a request and can make the request ({0}). The widget will be closed and all content will be discarded when the Escape key is pressed and the terminal will regain focus.", makeRequestKeybinding) : localize('inlineChat.inputNoKb', "The input box is where the user can type a request and can make the request by tabbing to the Make Request button, which is not currently triggerable via keybindings. The widget will be closed and all content will be discarded when the Escape key is pressed and the terminal will regain focus.")); + content.push(localize('inlineChat.results', "A result may contain a terminal command or just a message. In either case, the result will be announced.")); + content.push(openAccessibleViewKeybinding ? localize('inlineChat.inspectResponseMessage', 'If just a message comes back, it can be inspected in the accessible view ({0}).', openAccessibleViewKeybinding) : localize('inlineChat.inspectResponseNoKb', 'With the input box focused, inspect the response in the accessible view via the Open Accessible View command, which is currently not triggerable by a keybinding.')); + content.push(localize('inlineChat.inspectTerminalCommand', 'If a terminal command comes back, it can be inspected in an editor reached via Shift+Tab.')); + content.push(runCommandKeybinding ? localize('inlineChat.runCommand', 'With focus in the input box or command editor, the Terminal: Run Chat Command ({0}) action.', runCommandKeybinding) : localize('inlineChat.runCommandNoKb', 'Run a command by tabbing to the button as the action is currently not triggerable by a keybinding.')); + content.push(insertCommandKeybinding ? localize('inlineChat.insertCommand', 'With focus in the input box command editor, the Terminal: Insert Chat Command ({0}) action.', insertCommandKeybinding) : localize('inlineChat.insertCommandNoKb', 'Insert a command by tabbing to the button as the action is currently not triggerable by a keybinding.')); + content.push(localize('inlineChat.toolbar', "Use tab to reach conditional parts like commands, status, message responses and more.")); + content.push(localize('chat.signals', "Accessibility Signals can be changed via settings with a prefix of signals.chat. By default, if a request takes more than 4 seconds, you will hear a sound indicating that progress is still occurring.")); + return content.join('\n\n'); +} diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatAccessibleView.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatAccessibleView.ts new file mode 100644 index 00000000000..f5bbe82de03 --- /dev/null +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatAccessibleView.ts @@ -0,0 +1,38 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Disposable } from 'vs/base/common/lifecycle'; +import { AccessibilityVerbositySettingId, AccessibleViewProviderId } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; +import { AccessibleViewType, IAccessibleViewService } from 'vs/workbench/contrib/accessibility/browser/accessibleView'; +import { AccessibleViewAction } from 'vs/workbench/contrib/accessibility/browser/accessibleViewActions'; +import { ITerminalService } from 'vs/workbench/contrib/terminal/browser/terminal'; +import { TerminalChatContextKeys } from 'vs/workbench/contrib/terminalContrib/chat/browser/terminalChat'; +import { TerminalChatController } from 'vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController'; + +export class TerminalInlineChatAccessibleViewContribution extends Disposable { + static ID: 'terminalInlineChatAccessibleViewContribution'; + constructor() { + super(); + this._register(AccessibleViewAction.addImplementation(105, 'terminalInlineChat', accessor => { + const accessibleViewService = accessor.get(IAccessibleViewService); + const terminalService = accessor.get(ITerminalService); + const controller: TerminalChatController | undefined = terminalService.activeInstance?.getContribution(TerminalChatController.ID) ?? undefined; + if (!controller?.lastResponseContent) { + return false; + } + const responseContent = controller.lastResponseContent; + accessibleViewService.show({ + id: AccessibleViewProviderId.TerminalChat, + verbositySettingKey: AccessibilityVerbositySettingId.InlineChat, + provideContent(): string { return responseContent; }, + onClose() { + controller.focus(); + }, + options: { type: AccessibleViewType.View } + }); + return true; + }, TerminalChatContextKeys.focused)); + } +} diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts new file mode 100644 index 00000000000..ff650600398 --- /dev/null +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts @@ -0,0 +1,331 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Codicon } from 'vs/base/common/codicons'; +import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; +import { localize2 } from 'vs/nls'; +import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; +import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; +import { TerminalSettingId } from 'vs/platform/terminal/common/terminal'; +import { AbstractInlineChatAction } from 'vs/workbench/contrib/inlineChat/browser/inlineChatActions'; +import { CTX_INLINE_CHAT_EMPTY, CTX_INLINE_CHAT_FOCUSED, CTX_INLINE_CHAT_HAS_PROVIDER } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; +import { isDetachedTerminalInstance } from 'vs/workbench/contrib/terminal/browser/terminal'; +import { registerActiveXtermAction } from 'vs/workbench/contrib/terminal/browser/terminalActions'; +import { TerminalContextKeys } from 'vs/workbench/contrib/terminal/common/terminalContextKey'; +import { MENU_TERMINAL_CHAT_INPUT, MENU_TERMINAL_CHAT_WIDGET, MENU_TERMINAL_CHAT_WIDGET_FEEDBACK, MENU_TERMINAL_CHAT_WIDGET_STATUS, TerminalChatCommandId, TerminalChatContextKeys, TerminalChatResponseTypes } from 'vs/workbench/contrib/terminalContrib/chat/browser/terminalChat'; +import { TerminalChatController } from 'vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController'; + +registerActiveXtermAction({ + id: TerminalChatCommandId.Start, + title: localize2('startChat', 'Start in Terminal'), + keybinding: { + primary: KeyMod.CtrlCmd | KeyCode.KeyI, + when: ContextKeyExpr.and(TerminalContextKeys.focusInAny), + weight: KeybindingWeight.WorkbenchContrib, + }, + f1: true, + category: AbstractInlineChatAction.category, + precondition: ContextKeyExpr.and( + ContextKeyExpr.has(`config.${TerminalSettingId.ExperimentalInlineChat}`), + ContextKeyExpr.and(TerminalContextKeys.processSupported, TerminalContextKeys.focusInAny), + // TODO: This needs to change to check for a terminal location capable agent + CTX_INLINE_CHAT_HAS_PROVIDER + ), + run: (_xterm, _accessor, activeInstance) => { + if (isDetachedTerminalInstance(activeInstance)) { + return; + } + const contr = TerminalChatController.activeChatWidget || TerminalChatController.get(activeInstance); + contr?.chatWidget?.reveal(); + } +}); + +registerActiveXtermAction({ + id: TerminalChatCommandId.Close, + title: localize2('closeChat', 'Close Chat'), + keybinding: { + primary: KeyCode.Escape, + secondary: [KeyMod.Shift | KeyCode.Escape], + when: ContextKeyExpr.and(TerminalChatContextKeys.focused, TerminalChatContextKeys.visible), + weight: KeybindingWeight.WorkbenchContrib, + }, + icon: Codicon.close, + menu: { + id: MENU_TERMINAL_CHAT_WIDGET, + group: 'main', + order: 2 + }, + f1: true, + precondition: ContextKeyExpr.and( + ContextKeyExpr.has(`config.${TerminalSettingId.ExperimentalInlineChat}`), + ContextKeyExpr.and(TerminalChatContextKeys.focused, TerminalChatContextKeys.visible) + ), + run: (_xterm, _accessor, activeInstance) => { + if (isDetachedTerminalInstance(activeInstance)) { + return; + } + const contr = TerminalChatController.activeChatWidget || TerminalChatController.get(activeInstance); + contr?.clear(); + } +}); + + +registerActiveXtermAction({ + id: TerminalChatCommandId.Discard, + title: localize2('discard', 'Discard'), + icon: Codicon.discard, + menu: { + id: MENU_TERMINAL_CHAT_WIDGET_STATUS, + group: '0_main', + order: 2, + when: ContextKeyExpr.and(TerminalChatContextKeys.focused, + TerminalChatContextKeys.responseType.isEqualTo(TerminalChatResponseTypes.TerminalCommand)) + }, + f1: true, + precondition: ContextKeyExpr.and( + ContextKeyExpr.has(`config.${TerminalSettingId.ExperimentalInlineChat}`), + ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated), + TerminalChatContextKeys.focused, + TerminalChatContextKeys.responseType.isEqualTo(TerminalChatResponseTypes.TerminalCommand) + ), + run: (_xterm, _accessor, activeInstance) => { + if (isDetachedTerminalInstance(activeInstance)) { + return; + } + const contr = TerminalChatController.activeChatWidget || TerminalChatController.get(activeInstance); + contr?.clear(); + } +}); + + +registerActiveXtermAction({ + id: TerminalChatCommandId.RunCommand, + title: localize2('runCommand', 'Run Chat Command'), + shortTitle: localize2('run', 'Run'), + precondition: ContextKeyExpr.and( + ContextKeyExpr.has(`config.${TerminalSettingId.ExperimentalInlineChat}`), + ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated), + TerminalChatContextKeys.requestActive.negate(), + TerminalChatContextKeys.agentRegistered, + TerminalChatContextKeys.responseType.isEqualTo(TerminalChatResponseTypes.TerminalCommand) + ), + icon: Codicon.check, + keybinding: { + when: TerminalChatContextKeys.requestActive.negate(), + weight: KeybindingWeight.WorkbenchContrib, + primary: KeyMod.CtrlCmd | KeyCode.Enter, + }, + menu: { + // TODO: Allow action to be made primary, the action list is hardcoded within InlineChatWidget + id: MENU_TERMINAL_CHAT_WIDGET_STATUS, + group: '0_main', + order: 0, + when: ContextKeyExpr.and(TerminalChatContextKeys.responseType.isEqualTo(TerminalChatResponseTypes.TerminalCommand), TerminalChatContextKeys.requestActive.negate()), + }, + run: (_xterm, _accessor, activeInstance) => { + if (isDetachedTerminalInstance(activeInstance)) { + return; + } + const contr = TerminalChatController.activeChatWidget || TerminalChatController.get(activeInstance); + contr?.acceptCommand(true); + } +}); + +registerActiveXtermAction({ + id: TerminalChatCommandId.InsertCommand, + title: localize2('insertCommand', 'Insert Chat Command'), + shortTitle: localize2('insert', 'Insert'), + precondition: ContextKeyExpr.and( + ContextKeyExpr.has(`config.${TerminalSettingId.ExperimentalInlineChat}`), + ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated), + TerminalChatContextKeys.requestActive.negate(), + TerminalChatContextKeys.agentRegistered, + TerminalChatContextKeys.responseType.isEqualTo(TerminalChatResponseTypes.TerminalCommand) + ), + icon: Codicon.check, + keybinding: { + when: TerminalChatContextKeys.requestActive.negate(), + weight: KeybindingWeight.WorkbenchContrib, + primary: KeyMod.Alt | KeyCode.Enter, + }, + menu: { + id: MENU_TERMINAL_CHAT_WIDGET_STATUS, + group: '0_main', + order: 1, + when: ContextKeyExpr.and(TerminalChatContextKeys.responseType.isEqualTo(TerminalChatResponseTypes.TerminalCommand), TerminalChatContextKeys.requestActive.negate()), + }, + run: (_xterm, _accessor, activeInstance) => { + if (isDetachedTerminalInstance(activeInstance)) { + return; + } + const contr = TerminalChatController.activeChatWidget || TerminalChatController.get(activeInstance); + contr?.acceptCommand(false); + } +}); + +registerActiveXtermAction({ + id: TerminalChatCommandId.ViewInChat, + title: localize2('viewInChat', 'View in Chat'), + precondition: ContextKeyExpr.and( + ContextKeyExpr.has(`config.${TerminalSettingId.ExperimentalInlineChat}`), + ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated), + TerminalChatContextKeys.requestActive.negate(), + TerminalChatContextKeys.agentRegistered, + ), + icon: Codicon.commentDiscussion, + menu: [{ + id: MENU_TERMINAL_CHAT_WIDGET_STATUS, + group: '0_main', + order: 1, + when: ContextKeyExpr.and(TerminalChatContextKeys.responseType.isEqualTo(TerminalChatResponseTypes.Message), TerminalChatContextKeys.requestActive.negate()), + }, + { + id: MENU_TERMINAL_CHAT_WIDGET, + group: 'main', + order: 1, + when: ContextKeyExpr.and(CTX_INLINE_CHAT_EMPTY.negate(), TerminalChatContextKeys.responseType.isEqualTo(TerminalChatResponseTypes.TerminalCommand), TerminalChatContextKeys.requestActive.negate()), + }], + run: (_xterm, _accessor, activeInstance) => { + if (isDetachedTerminalInstance(activeInstance)) { + return; + } + const contr = TerminalChatController.activeChatWidget || TerminalChatController.get(activeInstance); + contr?.viewInChat(); + } +}); + +registerActiveXtermAction({ + id: TerminalChatCommandId.MakeRequest, + title: localize2('makeChatRequest', 'Make Chat Request'), + precondition: ContextKeyExpr.and( + ContextKeyExpr.has(`config.${TerminalSettingId.ExperimentalInlineChat}`), + ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated), + TerminalChatContextKeys.requestActive.negate(), + TerminalChatContextKeys.agentRegistered, + CTX_INLINE_CHAT_EMPTY.negate() + ), + icon: Codicon.send, + keybinding: { + when: ContextKeyExpr.and(CTX_INLINE_CHAT_FOCUSED, TerminalChatContextKeys.requestActive.negate()), + weight: KeybindingWeight.WorkbenchContrib, + primary: KeyCode.Enter + }, + menu: { + id: MENU_TERMINAL_CHAT_INPUT, + group: 'main', + order: 1, + when: TerminalChatContextKeys.requestActive.negate(), + }, + run: (_xterm, _accessor, activeInstance) => { + if (isDetachedTerminalInstance(activeInstance)) { + return; + } + const contr = TerminalChatController.activeChatWidget || TerminalChatController.get(activeInstance); + contr?.acceptInput(); + } +}); + +registerActiveXtermAction({ + id: TerminalChatCommandId.Cancel, + title: localize2('cancelChat', 'Cancel Chat'), + precondition: ContextKeyExpr.and( + ContextKeyExpr.has(`config.${TerminalSettingId.ExperimentalInlineChat}`), + TerminalChatContextKeys.requestActive, + TerminalChatContextKeys.agentRegistered + ), + icon: Codicon.debugStop, + menu: { + id: MENU_TERMINAL_CHAT_INPUT, + group: 'main', + when: TerminalChatContextKeys.requestActive, + }, + run: (_xterm, _accessor, activeInstance) => { + if (isDetachedTerminalInstance(activeInstance)) { + return; + } + const contr = TerminalChatController.activeChatWidget || TerminalChatController.get(activeInstance); + contr?.cancel(); + } +}); + +registerActiveXtermAction({ + id: TerminalChatCommandId.FeedbackHelpful, + title: localize2('feedbackHelpful', 'Helpful'), + precondition: ContextKeyExpr.and( + ContextKeyExpr.has(`config.${TerminalSettingId.ExperimentalInlineChat}`), + TerminalChatContextKeys.responseType.notEqualsTo(undefined) + ), + icon: Codicon.thumbsup, + toggled: TerminalChatContextKeys.sessionResponseVote.isEqualTo('up'), + menu: { + id: MENU_TERMINAL_CHAT_WIDGET_FEEDBACK, + group: 'inline', + order: 1, + when: TerminalChatContextKeys.responseType.notEqualsTo(undefined), + }, + run: (_xterm, _accessor, activeInstance) => { + if (isDetachedTerminalInstance(activeInstance)) { + return; + } + const contr = TerminalChatController.activeChatWidget || TerminalChatController.get(activeInstance); + contr?.acceptFeedback(true); + } +}); + +registerActiveXtermAction({ + id: TerminalChatCommandId.FeedbackUnhelpful, + title: localize2('feedbackUnhelpful', 'Unhelpful'), + precondition: ContextKeyExpr.and( + ContextKeyExpr.has(`config.${TerminalSettingId.ExperimentalInlineChat}`), + TerminalChatContextKeys.responseType.notEqualsTo(undefined), + ), + toggled: TerminalChatContextKeys.sessionResponseVote.isEqualTo('down'), + icon: Codicon.thumbsdown, + menu: { + id: MENU_TERMINAL_CHAT_WIDGET_FEEDBACK, + group: 'inline', + order: 2, + when: TerminalChatContextKeys.responseType.notEqualsTo(undefined), + }, + run: (_xterm, _accessor, activeInstance) => { + if (isDetachedTerminalInstance(activeInstance)) { + return; + } + const contr = TerminalChatController.activeChatWidget || TerminalChatController.get(activeInstance); + contr?.acceptFeedback(false); + } +}); + +registerActiveXtermAction({ + id: TerminalChatCommandId.FeedbackReportIssue, + title: localize2('reportIssue', 'Report Issue'), + precondition: ContextKeyExpr.and( + ContextKeyExpr.has(`config.${TerminalSettingId.ExperimentalInlineChat}`), + TerminalChatContextKeys.requestActive.negate(), + TerminalChatContextKeys.responseType.notEqualsTo(undefined), + TerminalChatContextKeys.responseSupportsIssueReporting + ), + icon: Codicon.report, + menu: [{ + id: MENU_TERMINAL_CHAT_WIDGET_FEEDBACK, + when: ContextKeyExpr.and(TerminalChatContextKeys.responseType.notEqualsTo(undefined), TerminalChatContextKeys.responseSupportsIssueReporting), + group: 'inline', + order: 3 + }], + // { + // id: MENU_TERMINAL_CHAT_WIDGET, + // when: ContextKeyExpr.and(TerminalChatContextKeys.chatResponseType.notEqualsTo(undefined), TerminalChatContextKeys.chatResponseSupportsIssueReporting), + // group: 'config', + // order: 3 + // }], + run: (_xterm, _accessor, activeInstance) => { + if (isDetachedTerminalInstance(activeInstance)) { + return; + } + const contr = TerminalChatController.activeChatWidget || TerminalChatController.get(activeInstance); + contr?.acceptFeedback(); + } +}); + diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts new file mode 100644 index 00000000000..e6a95220291 --- /dev/null +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts @@ -0,0 +1,373 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import type { Terminal as RawXtermTerminal } from '@xterm/xterm'; +import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; +import { Emitter, Event } from 'vs/base/common/event'; +import { Lazy } from 'vs/base/common/lazy'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { marked } from 'vs/base/common/marked/marked'; +import { localize } from 'vs/nls'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { TerminalSettingId } from 'vs/platform/terminal/common/terminal'; +import { IChatAccessibilityService, IChatWidgetService } from 'vs/workbench/contrib/chat/browser/chat'; +import { IChatAgentService, IChatAgentRequest } from 'vs/workbench/contrib/chat/common/chatAgents'; +import { IParsedChatRequest } from 'vs/workbench/contrib/chat/common/chatParserTypes'; +import { IChatService, IChatProgress, InteractiveSessionVoteDirection, ChatUserAction } from 'vs/workbench/contrib/chat/common/chatService'; +import { ITerminalContribution, ITerminalInstance, ITerminalService, IXtermTerminal, isDetachedTerminalInstance } from 'vs/workbench/contrib/terminal/browser/terminal'; +import { TerminalWidgetManager } from 'vs/workbench/contrib/terminal/browser/widgets/widgetManager'; +import { ITerminalProcessManager } from 'vs/workbench/contrib/terminal/common/terminal'; +import { TerminalChatWidget } from 'vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget'; + + +import { ChatModel, ChatRequestModel, IChatRequestVariableData } from 'vs/workbench/contrib/chat/common/chatModel'; +import { TerminalChatContextKeys, TerminalChatResponseTypes } from 'vs/workbench/contrib/terminalContrib/chat/browser/terminalChat'; + +const enum Message { + NONE = 0, + ACCEPT_SESSION = 1 << 0, + CANCEL_SESSION = 1 << 1, + PAUSE_SESSION = 1 << 2, + CANCEL_REQUEST = 1 << 3, + CANCEL_INPUT = 1 << 4, + ACCEPT_INPUT = 1 << 5, + RERUN_INPUT = 1 << 6, +} + +export class TerminalChatController extends Disposable implements ITerminalContribution { + static readonly ID = 'terminal.Chat'; + + static get(instance: ITerminalInstance): TerminalChatController | null { + return instance.getContribution(TerminalChatController.ID); + } + /** + * Currently focused chat widget. This is used to track action context since + * 'active terminals' are only tracked for non-detached terminal instanecs. + */ + static activeChatWidget?: TerminalChatController; + private _chatWidget: Lazy | undefined; + get chatWidget(): TerminalChatWidget | undefined { return this._chatWidget?.value; } + + private readonly _requestActiveContextKey!: IContextKey; + private readonly _terminalAgentRegisteredContextKey!: IContextKey; + private readonly _responseTypeContextKey!: IContextKey; + private readonly _responseSupportsIssueReportingContextKey!: IContextKey; + private readonly _sessionResponseVoteContextKey!: IContextKey; + + private _messages = this._store.add(new Emitter()); + + private _currentRequest: ChatRequestModel | undefined; + + private _lastInput: string | undefined; + private _lastResponseContent: string | undefined; + get lastResponseContent(): string | undefined { + return this._lastResponseContent; + } + + readonly onDidAcceptInput = Event.filter(this._messages.event, m => m === Message.ACCEPT_INPUT, this._store); + readonly onDidCancelInput = Event.filter(this._messages.event, m => m === Message.CANCEL_INPUT || m === Message.CANCEL_SESSION, this._store); + + private _terminalAgentId = 'terminal'; + + private _model: ChatModel | undefined; + + constructor( + private readonly _instance: ITerminalInstance, + processManager: ITerminalProcessManager, + widgetManager: TerminalWidgetManager, + @IConfigurationService private _configurationService: IConfigurationService, + @ITerminalService private readonly _terminalService: ITerminalService, + @IInstantiationService private readonly _instantiationService: IInstantiationService, + @IChatAgentService private readonly _chatAgentService: IChatAgentService, + @IContextKeyService private readonly _contextKeyService: IContextKeyService, + @IChatAccessibilityService private readonly _chatAccessibilityService: IChatAccessibilityService, + @IChatWidgetService private readonly _chatWidgetService: IChatWidgetService, + @IChatService private readonly _chatService: IChatService, + ) { + super(); + + if (!this._configurationService.getValue(TerminalSettingId.ExperimentalInlineChat)) { + return; + } + this._requestActiveContextKey = TerminalChatContextKeys.requestActive.bindTo(this._contextKeyService); + this._terminalAgentRegisteredContextKey = TerminalChatContextKeys.agentRegistered.bindTo(this._contextKeyService); + this._responseTypeContextKey = TerminalChatContextKeys.responseType.bindTo(this._contextKeyService); + this._responseSupportsIssueReportingContextKey = TerminalChatContextKeys.responseSupportsIssueReporting.bindTo(this._contextKeyService); + this._sessionResponseVoteContextKey = TerminalChatContextKeys.sessionResponseVote.bindTo(this._contextKeyService); + + if (!this._chatAgentService.getRegisteredAgent(this._terminalAgentId)) { + this._register(this._chatAgentService.onDidChangeAgents(() => { + if (this._chatAgentService.getRegisteredAgent(this._terminalAgentId)) { + this._terminalAgentRegisteredContextKey.set(true); + } + })); + } else { + this._terminalAgentRegisteredContextKey.set(true); + } + } + + xtermReady(xterm: IXtermTerminal & { raw: RawXtermTerminal }): void { + if (!this._configurationService.getValue(TerminalSettingId.ExperimentalInlineChat)) { + return; + } + this._chatWidget = new Lazy(() => { + + const chatWidget = this._register(this._instantiationService.createInstance(TerminalChatWidget, this._instance.domElement!, this._instance)); + this._register(chatWidget.focusTracker.onDidFocus(() => { + TerminalChatController.activeChatWidget = this; + if (!isDetachedTerminalInstance(this._instance)) { + this._terminalService.setActiveInstance(this._instance); + } + })); + this._register(chatWidget.focusTracker.onDidBlur(() => { + TerminalChatController.activeChatWidget = undefined; + this._instance.resetScrollbarVisibility(); + })); + if (!this._instance.domElement) { + throw new Error('FindWidget expected terminal DOM to be initialized'); + } + + return chatWidget; + }); + } + + acceptFeedback(helpful?: boolean): void { + const providerId = this._chatService.getProviderInfos()?.[0]?.id; + if (!providerId || !this._currentRequest || !this._model) { + return; + } + let action: ChatUserAction; + if (helpful === undefined) { + action = { kind: 'bug' }; + } else { + this._sessionResponseVoteContextKey.set(helpful ? 'up' : 'down'); + action = { kind: 'vote', direction: helpful ? InteractiveSessionVoteDirection.Up : InteractiveSessionVoteDirection.Down }; + } + // TODO:extract into helper method + for (const request of this._model.getRequests()) { + if (request.response?.response.value || request.response?.result) { + this._chatService.notifyUserAction({ + providerId, + sessionId: request.session.sessionId, + requestId: request.id, + agentId: request.response?.agent?.id, + result: request.response?.result, + action + }); + } + } + this._chatWidget?.rawValue?.inlineChatWidget.updateStatus('Thank you for your feedback!', { resetAfter: 1250 }); + } + + cancel(): void { + if (this._currentRequest) { + this._model?.cancelRequest(this._currentRequest); + } + this._requestActiveContextKey.set(false); + this._chatWidget?.rawValue?.inlineChatWidget.updateProgress(false); + this._chatWidget?.rawValue?.inlineChatWidget.updateInfo(''); + this._chatWidget?.rawValue?.inlineChatWidget.updateToolbar(true); + } + + private _forcedPlaceholder: string | undefined = undefined; + + private _updatePlaceholder(): void { + const inlineChatWidget = this._chatWidget?.rawValue?.inlineChatWidget; + if (inlineChatWidget) { + inlineChatWidget.placeholder = this._getPlaceholderText(); + } + } + + private _getPlaceholderText(): string { + return this._forcedPlaceholder ?? ''; + } + + setPlaceholder(text: string): void { + this._forcedPlaceholder = text; + this._updatePlaceholder(); + } + + resetPlaceholder(): void { + this._forcedPlaceholder = undefined; + this._updatePlaceholder(); + } + + clear(): void { + if (this._currentRequest) { + this._model?.cancelRequest(this._currentRequest); + } + this._model?.dispose(); + this._model = undefined; + this._chatWidget?.rawValue?.hide(); + this._chatWidget?.rawValue?.setValue(undefined); + this._responseTypeContextKey.reset(); + this._sessionResponseVoteContextKey.reset(); + this._requestActiveContextKey.reset(); + } + + private updateModel(): void { + const providerInfo = this._chatService.getProviderInfos()?.[0]; + if (!providerInfo) { + return; + } + this._model ??= this._chatService.startSession(providerInfo.id, CancellationToken.None); + } + + async acceptInput(): Promise { + this.updateModel(); + this._lastInput = this._chatWidget?.rawValue?.input(); + if (!this._lastInput) { + return; + } + const accessibilityRequestId = this._chatAccessibilityService.acceptRequest(); + this._requestActiveContextKey.set(true); + const cancellationToken = new CancellationTokenSource().token; + let responseContent = ''; + let firstCodeBlock: string | undefined; + let shellType: string | undefined; + const progressCallback = (progress: IChatProgress) => { + if (cancellationToken.isCancellationRequested) { + return; + } + + if (progress.kind === 'content' || progress.kind === 'markdownContent') { + responseContent += progress.content; + } + if (this._currentRequest) { + this._model?.acceptResponseProgress(this._currentRequest, progress); + } + if (!firstCodeBlock) { + const firstCodeBlockContent = marked.lexer(responseContent).filter(token => token.type === 'code')?.[0]?.raw; + if (firstCodeBlockContent) { + const regex = /```(?[\w\n]+)\n(?[\s\S]*?)```/g; + const match = regex.exec(firstCodeBlockContent); + firstCodeBlock = match?.groups?.content.trim(); + shellType = match?.groups?.language; + if (firstCodeBlock) { + this._chatWidget?.rawValue?.renderTerminalCommand(firstCodeBlock, shellType); + this._chatAccessibilityService.acceptResponse(firstCodeBlock, accessibilityRequestId); + this._responseTypeContextKey.set(TerminalChatResponseTypes.TerminalCommand); + this._chatWidget?.rawValue?.inlineChatWidget.updateToolbar(true); + this._messages.fire(Message.ACCEPT_INPUT); + } + } + } + }; + + await this._model?.waitForInitialization(); + const request: IParsedChatRequest = { + text: this._lastInput, + parts: [] + }; + const requestVarData: IChatRequestVariableData = { + variables: [] + }; + this._currentRequest = this._model?.addRequest(request, requestVarData); + const requestProps: IChatAgentRequest = { + sessionId: this._model!.sessionId, + requestId: this._currentRequest!.id, + agentId: this._terminalAgentId, + message: this._lastInput, + variables: { variables: [] }, + }; + try { + const task = this._chatAgentService.invokeAgent(this._terminalAgentId, requestProps, progressCallback, [], cancellationToken); + this._chatWidget?.rawValue?.inlineChatWidget.updateChatMessage(undefined); + this._chatWidget?.rawValue?.inlineChatWidget.updateFollowUps(undefined); + this._chatWidget?.rawValue?.inlineChatWidget.updateProgress(true); + this._chatWidget?.rawValue?.inlineChatWidget.updateInfo(localize('thinking', "Thinking\u2026")); + await task; + } catch (e) { + + } finally { + this._requestActiveContextKey.set(false); + this._chatWidget?.rawValue?.inlineChatWidget.updateProgress(false); + this._chatWidget?.rawValue?.inlineChatWidget.updateInfo(''); + this._chatWidget?.rawValue?.inlineChatWidget.updateToolbar(true); + if (this._currentRequest) { + this._model?.completeResponse(this._currentRequest); + } + this._lastResponseContent = responseContent; + if (!firstCodeBlock && this._currentRequest) { + this._chatAccessibilityService.acceptResponse(responseContent, accessibilityRequestId); + this._chatWidget?.rawValue?.renderMessage(responseContent, this._currentRequest.id); + this._responseTypeContextKey.set(TerminalChatResponseTypes.Message); + this._chatWidget?.rawValue?.inlineChatWidget.updateToolbar(true); + this._messages.fire(Message.ACCEPT_INPUT); + } + const supportIssueReporting = this._currentRequest?.response?.agent?.metadata?.supportIssueReporting; + if (supportIssueReporting !== undefined) { + this._responseSupportsIssueReportingContextKey.set(supportIssueReporting); + } + } + } + + updateInput(text: string, selectAll = true): void { + const widget = this._chatWidget?.rawValue?.inlineChatWidget; + if (widget) { + widget.value = text; + if (selectAll) { + widget.selectAll(); + } + } + } + + getInput(): string { + return this._chatWidget?.rawValue?.input() ?? ''; + } + + focus(): void { + this._chatWidget?.rawValue?.focus(); + } + + hasFocus(): boolean { + return !!this._chatWidget?.rawValue?.hasFocus(); + } + + acceptCommand(shouldExecute: boolean): void { + this._chatWidget?.rawValue?.acceptCommand(shouldExecute); + } + + reveal(): void { + this._chatWidget?.rawValue?.reveal(); + } + + async viewInChat(): Promise { + const providerInfo = this._chatService.getProviderInfos()?.[0]; + if (!providerInfo) { + return; + } + const widget = await this._chatWidgetService.revealViewForProvider(providerInfo.id); + if (widget && widget.viewModel && this._model) { + for (const request of this._model.getRequests()) { + if (request.response?.response.value || request.response?.result) { + this._chatService.addCompleteRequest(widget.viewModel.sessionId, + request.message as IParsedChatRequest, + request.variableData, + { + message: request.response.response.value, + result: request.response.result, + followups: request.response.followups + }); + } + } + widget.focusLastMessage(); + } else if (!this._model) { + widget?.focusInput(); + } + } + + override dispose() { + if (this._currentRequest) { + this._model?.cancelRequest(this._currentRequest); + } + super.dispose(); + this.clear(); + this._chatWidget?.rawValue?.dispose(); + } +} + diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts new file mode 100644 index 00000000000..320199ad859 --- /dev/null +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts @@ -0,0 +1,320 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Dimension, IFocusTracker, hide, show, trackFocus } from 'vs/base/browser/dom'; +import { Event } from 'vs/base/common/event'; +import { MarkdownString } from 'vs/base/common/htmlContent'; +import { Disposable, toDisposable } from 'vs/base/common/lifecycle'; +import { URI } from 'vs/base/common/uri'; +import 'vs/css!./media/terminalChatWidget'; +import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditor/codeEditorWidget'; +import { ILanguageService } from 'vs/editor/common/languages/language'; +import { ITextModel } from 'vs/editor/common/model'; +import { IModelService } from 'vs/editor/common/services/model'; +import { localize } from 'vs/nls'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { AccessibilityVerbositySettingId } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; +import { IChatProgress } from 'vs/workbench/contrib/chat/common/chatService'; +import { InlineChatWidget } from 'vs/workbench/contrib/inlineChat/browser/inlineChatWidget'; +import { ITerminalInstance } from 'vs/workbench/contrib/terminal/browser/terminal'; +import { MENU_TERMINAL_CHAT_INPUT, MENU_TERMINAL_CHAT_WIDGET, MENU_TERMINAL_CHAT_WIDGET_FEEDBACK, MENU_TERMINAL_CHAT_WIDGET_STATUS, TerminalChatContextKeys } from 'vs/workbench/contrib/terminalContrib/chat/browser/terminalChat'; + +export class TerminalChatWidget extends Disposable { + + private readonly _container: HTMLElement; + + private readonly _inlineChatWidget: InlineChatWidget; + public get inlineChatWidget(): InlineChatWidget { return this._inlineChatWidget; } + + private _responseEditor: TerminalChatResponseEditor | undefined; + + private readonly _focusTracker: IFocusTracker; + + private readonly _focusedContextKey: IContextKey; + private readonly _visibleContextKey: IContextKey; + + constructor( + terminalElement: HTMLElement, + private readonly _instance: ITerminalInstance, + @IInstantiationService private readonly _instantiationService: IInstantiationService, + @IContextKeyService private readonly _contextKeyService: IContextKeyService + ) { + super(); + + this._focusedContextKey = TerminalChatContextKeys.focused.bindTo(this._contextKeyService); + this._visibleContextKey = TerminalChatContextKeys.visible.bindTo(this._contextKeyService); + + this._container = document.createElement('div'); + this._container.classList.add('terminal-inline-chat'); + terminalElement.appendChild(this._container); + + // The inline chat widget requires a parent editor that it bases the diff view on, since the + // terminal doesn't use that feature we can just pass in an unattached editor instance. + const fakeParentEditorElement = document.createElement('div'); + const fakeParentEditor = this._instantiationService.createInstance( + CodeEditorWidget, + fakeParentEditorElement, + { + extraEditorClassName: 'ignore-panel-bg' + }, + { isSimpleWidget: true } + ); + + this._inlineChatWidget = this._instantiationService.createInstance( + InlineChatWidget, + fakeParentEditor, + { + inputMenuId: MENU_TERMINAL_CHAT_INPUT, + widgetMenuId: MENU_TERMINAL_CHAT_WIDGET, + statusMenuId: MENU_TERMINAL_CHAT_WIDGET_STATUS, + feedbackMenuId: MENU_TERMINAL_CHAT_WIDGET_FEEDBACK, + telemetrySource: 'terminal-inline-chat', + } + ); + this._reset(); + this._container.appendChild(this._inlineChatWidget.domNode); + + this._focusTracker = this._register(trackFocus(this._container)); + } + + private _reset() { + this._inlineChatWidget.placeholder = localize('default.placeholder', "Ask how to do something in the terminal"); + this._inlineChatWidget.updateInfo(localize('welcome.1', "AI-generated commands may be incorrect")); + } + + async renderTerminalCommand(command: string, shellType?: string): Promise { + this._responseEditor?.show(); + if (!this._responseEditor) { + this._responseEditor = this._instantiationService.createInstance(TerminalChatResponseEditor, command, shellType, this._container, this._instance); + } + this._responseEditor.setValue(command); + } + + renderMessage(message: string, requestId: string): void { + this._responseEditor?.hide(); + this._inlineChatWidget.updateChatMessage({ message: new MarkdownString(message), requestId, providerId: 'terminal' }); + } + + reveal(): void { + this._inlineChatWidget.layout(new Dimension(640, 150)); + this._container.classList.remove('hide'); + this._focusedContextKey.set(true); + this._visibleContextKey.set(true); + this._inlineChatWidget.focus(); + const font = this._instance.xterm?.getFont(); + if (!font?.charHeight) { + return; + } + const cursorY = (this._instance.xterm?.raw.buffer.active.cursorY ?? 0) + 1; + const height = font.charHeight * font.lineHeight; + const top = cursorY * height + 12; + this._container.style.top = `${top}px`; + const terminalHeight = this._instance.domElement.clientHeight; + if (terminalHeight && top > terminalHeight - this._inlineChatWidget.getHeight()) { + this._container.style.top = ''; + } + this._updateWidth(); + this._register(this._instance.onDimensionsChanged(() => this._updateWidth())); + } + + private _updateWidth() { + const terminalWidth = this._instance.domElement.clientWidth; + if (terminalWidth && terminalWidth < 640) { + this._inlineChatWidget.layout(new Dimension(terminalWidth - 40, this._inlineChatWidget.getHeight())); + } + } + + hide(): void { + this._container.classList.add('hide'); + this._reset(); + this._responseEditor?.dispose(); + this._responseEditor = undefined; + this._inlineChatWidget.updateChatMessage(undefined); + this._inlineChatWidget.updateFollowUps(undefined); + this._inlineChatWidget.updateProgress(false); + this._inlineChatWidget.updateToolbar(false); + this._focusedContextKey.set(false); + this._visibleContextKey.set(false); + this._inlineChatWidget.value = ''; + this._instance.focus(); + } + focus(): void { + this._inlineChatWidget.focus(); + } + hasFocus(): boolean { + return this._inlineChatWidget.hasFocus(); + } + input(): string { + return this._inlineChatWidget.value; + } + setValue(value?: string) { + this._inlineChatWidget.value = value ?? ''; + if (!value) { + this._responseEditor?.hide(); + } + } + acceptCommand(shouldExecute: boolean): void { + // Trim command to remove any whitespace, otherwise this may execute the command + const value = this._responseEditor?.getValue().trim(); + if (!value) { + return; + } + this._instance.runCommand(value, shouldExecute); + this.hide(); + } + updateProgress(progress?: IChatProgress): void { + this._inlineChatWidget.updateProgress(progress?.kind === 'content' || progress?.kind === 'markdownContent'); + } + public get focusTracker(): IFocusTracker { + return this._focusTracker; + } +} + +class TerminalChatResponseEditor extends Disposable { + private readonly _editorContainer: HTMLElement; + private readonly _editor: CodeEditorWidget; + + private readonly _responseEditorFocusedContextKey: IContextKey; + + readonly model: Promise; + + constructor( + initialCommandResponse: string, + shellType: string | undefined, + private readonly _container: HTMLElement, + private readonly _instance: ITerminalInstance, + @IConfigurationService private readonly _configurationService: IConfigurationService, + @IContextKeyService private readonly _contextKeyService: IContextKeyService, + @IInstantiationService private readonly _instantiationService: IInstantiationService, + @ILanguageService private readonly _languageService: ILanguageService, + @IModelService private readonly _modelService: IModelService, + ) { + super(); + + this._responseEditorFocusedContextKey = TerminalChatContextKeys.responseEditorFocused.bindTo(this._contextKeyService); + + this._editorContainer = document.createElement('div'); + this._editorContainer.classList.add('terminal-inline-chat-response'); + this._container.prepend(this._editorContainer); + this._register(toDisposable(() => this._editorContainer.remove())); + const editor = this._register(this._instantiationService.createInstance(CodeEditorWidget, this._editorContainer, { + readOnly: false, + ariaLabel: this._getAriaLabel(), + fontSize: 13, + lineHeight: 20, + padding: { top: 8, bottom: 8 }, + overviewRulerLanes: 0, + glyphMargin: false, + lineNumbers: 'off', + folding: false, + hideCursorInOverviewRuler: true, + selectOnLineNumbers: false, + selectionHighlight: false, + scrollbar: { + useShadows: false, + vertical: 'hidden', + horizontal: 'hidden', + alwaysConsumeMouseWheel: false + }, + lineDecorationsWidth: 0, + overviewRulerBorder: false, + scrollBeyondLastLine: false, + renderLineHighlight: 'none', + fixedOverflowWidgets: true, + dragAndDrop: false, + revealHorizontalRightPadding: 5, + minimap: { enabled: false }, + guides: { indentation: false }, + rulers: [], + renderWhitespace: 'none', + dropIntoEditor: { enabled: true }, + quickSuggestions: false, + suggest: { + showIcons: false, + showSnippets: false, + showWords: true, + showStatusBar: false, + }, + wordWrap: 'on' + }, { isSimpleWidget: true })); + this._editor = editor; + this._register(editor.onDidFocusEditorText(() => this._responseEditorFocusedContextKey.set(true))); + this._register(editor.onDidBlurEditorText(() => this._responseEditorFocusedContextKey.set(false))); + this._register(Event.any(editor.onDidChangeModelContent, editor.onDidChangeModelDecorations)(() => { + const height = editor.getContentHeight(); + editor.layout(new Dimension(640, height)); + })); + + this.model = this._getTextModel(URI.from({ + path: `terminal-inline-chat-${this._instance.instanceId}`, + scheme: 'terminal-inline-chat', + fragment: initialCommandResponse + })); + this.model.then(model => { + if (model) { + // Initial layout + editor.layout(new Dimension(640, 0)); + editor.setModel(model); + const height = editor.getContentHeight(); + editor.layout(new Dimension(640, height)); + + // Initialize language + const languageId = this._getLanguageFromShell(shellType); + model.setLanguage(languageId); + } + }); + } + + private _getAriaLabel(): string { + const verbose = this._configurationService.getValue(AccessibilityVerbositySettingId.Chat); + if (verbose) { + // TODO: Add verbose description + } + return localize('terminalResponseEditor', "Terminal Response Editor"); + } + + private async _getTextModel(resource: URI): Promise { + const existing = this._modelService.getModel(resource); + if (existing && !existing.isDisposed()) { + return existing; + } + return this._modelService.createModel(resource.fragment, null, resource, false); + } + + private _getLanguageFromShell(shell?: string): string { + switch (shell) { + case 'fish': + return this._languageService.isRegisteredLanguageId('fish') ? 'fish' : 'shellscript'; + case 'zsh': + return this._languageService.isRegisteredLanguageId('zsh') ? 'zsh' : 'shellscript'; + case 'bash': + return this._languageService.isRegisteredLanguageId('bash') ? 'bash' : 'shellscript'; + case 'sh': + return 'shellscript'; + case 'pwsh': + return 'powershell'; + default: + return 'plaintext'; + } + } + + setValue(value: string) { + this._editor.setValue(value); + } + + getValue(): string { + return this._editor.getValue(); + } + + hide() { + hide(this._editorContainer); + } + + show() { + show(this._editorContainer); + } +} diff --git a/src/vs/workbench/contrib/terminalContrib/links/browser/links.ts b/src/vs/workbench/contrib/terminalContrib/links/browser/links.ts index b680864e23d..389f40afc6e 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 6b5af707012..d4247bbd835 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 259df08a249..adee3ee9f1f 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 1675d0fac73..02a9b2cc39f 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 faae5d685ee..785ad9658ab 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 04f14751372..087576f6ca2 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/update/browser/update.ts b/src/vs/workbench/contrib/update/browser/update.ts index 969c563d5de..45a8227909b 100644 --- a/src/vs/workbench/contrib/update/browser/update.ts +++ b/src/vs/workbench/contrib/update/browser/update.ts @@ -173,6 +173,7 @@ export class UpdateContribution extends Disposable implements IWorkbenchContribu @IContextKeyService private readonly contextKeyService: IContextKeyService, @IProductService private readonly productService: IProductService, @IOpenerService private readonly openerService: IOpenerService, + @IConfigurationService private readonly configurationService: IConfigurationService, @IHostService private readonly hostService: IHostService ) { super(); @@ -316,8 +317,12 @@ export class UpdateContribution extends Disposable implements IWorkbenchContribu ); } - // windows fast updates (target === system) + // windows fast updates private onUpdateDownloaded(update: IUpdate): void { + if (this.configurationService.getValue('update.enableWindowsBackgroundUpdates') && this.productService.target === 'user') { + return; + } + if (!this.shouldShowNotification()) { return; } diff --git a/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts b/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts index 0787368edba..08c30bd1f30 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 6514979da3d..3a258267b1f 100644 --- a/src/vs/workbench/contrib/webview/browser/webviewElement.ts +++ b/src/vs/workbench/contrib/webview/browser/webviewElement.ts @@ -804,7 +804,8 @@ export class WebviewElement extends Disposable implements IWebview, WebviewFindD } try { - this.element.contentWindow?.focus(); + this.element.parentElement?.focus(); // this helps to move floating windows to the front if any... + this.element.contentWindow?.focus(); // ...because `contentWindow` is not able to do so } catch { // noop } diff --git a/src/vs/workbench/contrib/webviewPanel/browser/webviewEditor.ts b/src/vs/workbench/contrib/webviewPanel/browser/webviewEditor.ts index f9b3f0c34f3..67fbfb9e4dc 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 fc6c589ca16..66b9062611f 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 99d6a260886..a34712f15cb 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 50efd6cfc84..dd11b080f6b 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 fe7f625a784..51339f65077 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 5d989fc5e1b..4e4e8f5b7d0 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 f207e15834f..eb2159272be 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 00000000000..565821fcb50 --- /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 00000000000..eaec30e9f18 --- /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 524b7402f04..6c22b70cd63 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,55 +54,86 @@ 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(); + } + + 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 { + return this._authenticationProviders.has(id); + } + + registerAuthenticationProvider(id: string, authenticationProvider: IAuthenticationProvider): void { + this._authenticationProviders.set(id, authenticationProvider); + const disposableStore = new DisposableStore(); + 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 }); + } + + unregisterAuthenticationProvider(id: string): void { + const provider = this._authenticationProviders.get(id); + if (provider) { + this._authenticationProviders.delete(id); + this._onDidUnregisterAuthenticationProvider.fire({ id, label: provider.label }); + } + this._authenticationProviderDisposables.deleteAndDispose(id); } getProviderIds(): string[] { @@ -292,495 +144,11 @@ export class AuthenticationService extends Disposable implements IAuthentication return providerIds; } - isAuthenticationProviderRegistered(id: string): boolean { - return this._authenticationProviders.has(id); - } - - registerAuthenticationProvider(id: string, authenticationProvider: IAuthenticationProvider): void { - this._authenticationProviders.set(id, authenticationProvider); - const disposableStore = new DisposableStore(); - disposableStore.add(authenticationProvider.onDidChangeSessions(e => this.sessionsUpdate(authenticationProvider, e))); - if (isDisposable(authenticationProvider)) { - disposableStore.add(authenticationProvider); + getProvider(id: string): IAuthenticationProvider { + if (this._authenticationProviders.has(id)) { + return this._authenticationProviders.get(id)!; } - this._authenticationProviderDisposables.set(id, disposableStore); - this._onDidRegisterAuthenticationProvider.fire({ id, label: authenticationProvider.label }); - - if (placeholderMenuItem) { - placeholderMenuItem.dispose(); - placeholderMenuItem = undefined; - } - } - - unregisterAuthenticationProvider(id: string): void { - const provider = this._authenticationProviders.get(id); - 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; - }); - - 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); - } - - //#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); - } - - 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'); - } - - 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 - } - } 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; - } - - 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); - if (authProvider) { - return authProvider.label; - } else { - throw new Error(`No authentication provider '${id}' is currently registered.`); - } - } - - supportsMultipleAccounts(id: string): boolean { - const authProvider = this._authenticationProviders.get(id); - if (authProvider) { - return authProvider.supportsMultipleAccounts; - } 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); - let provider = this._authenticationProviders.get(providerId); - if (provider) { - return provider; - } - - // When activate has completed, the extension has made the call to `registerAuthenticationProvider`. - // However, activate cannot block on this, so the renderer may not have gotten the event yet. - const didRegister: Promise = new Promise((resolve, _) => { - this.onDidRegisterAuthenticationProvider(e => { - if (e.id === providerId) { - provider = this._authenticationProviders.get(providerId); - if (provider) { - resolve(provider); - } else { - throw new Error(`No authentication provider '${providerId}' is currently registered.`); - } - } - }); - }); - - const didTimeout: Promise = new Promise((_, reject) => { - setTimeout(() => { - reject('Timed out waiting for authentication provider to register'); - }, 5000); - }); - - return Promise.race([didRegister, didTimeout]); + throw new Error(`No authentication provider '${id}' is currently registered.`); } async getSessions(id: string, scopes?: string[], activateImmediate: boolean = false): Promise> { @@ -812,177 +180,35 @@ export class AuthenticationService extends Disposable implements IAuthentication } } - // 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.`); + private async tryActivateProvider(providerId: string, activateImmediate: boolean): Promise { + await this._extensionService.activateByEvent(getAuthenticationProviderActivationEvent(providerId), activateImmediate ? ActivationKind.Immediate : ActivationKind.Normal); + let provider = this._authenticationProviders.get(providerId); + if (provider) { + return provider; } - 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); + // When activate has completed, the extension has made the call to `registerAuthenticationProvider`. + // However, activate cannot block on this, so the renderer may not have gotten the event yet. + const didRegister: Promise = new Promise((resolve, _) => { + this.onDidRegisterAuthenticationProvider(e => { + if (e.id === providerId) { + provider = this._authenticationProviders.get(providerId); + if (provider) { + resolve(provider); } else { - trustItem.extension.allowed = false; + throw new Error(`No authentication provider '${providerId}' is currently registered.`); } } }); - 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); - } + const didTimeout: Promise = new Promise((_, reject) => { + setTimeout(() => { + reject('Timed out waiting for authentication provider to register'); + }, 5000); + }); + + return Promise.race([didRegister, didTimeout]); } } 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 00000000000..8a40ac36958 --- /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 eaeaffc56ac..6da6e530237 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; + /** + * 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 }>; + + /** + * 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; - getProviderIds(): string[]; + + /** + * 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; - isAccessAllowed(providerId: string, accountName: string, extensionId: string): boolean | undefined; - updateAllowedExtension(providerId: string, accountName: string, extensionId: string, extensionName: string, isAllowed: boolean): 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>; + + /** + * 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; +} + +// 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; - 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; - - readonly onDidRegisterAuthenticationProvider: Event; - readonly onDidUnregisterAuthenticationProvider: Event; - - 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; - - getSessions(id: string, scopes?: string[], activateImmediate?: boolean): Promise>; - getLabel(providerId: string): string; - supportsMultipleAccounts(providerId: string): boolean; - createSession(providerId: string, scopes: string[], options?: IAuthenticationCreateSessionOptions): Promise; - 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; } 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 00000000000..180c1ae8a3a --- /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 ce6f32b0914..6864585e014 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,24 +296,20 @@ 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.'); - }; - } - private applyMeta(auxiliaryWindow: CodeWindow): void { for (const metaTag of ['meta[charset="utf-8"]', 'meta[http-equiv="Content-Security-Policy"]', 'meta[name="viewport"]', 'meta[name="theme-color"]']) { const metaElement = mainWindow.document.querySelector(metaTag); diff --git a/src/vs/workbench/services/auxiliaryWindow/electron-sandbox/auxiliaryWindowService.ts b/src/vs/workbench/services/auxiliaryWindow/electron-sandbox/auxiliaryWindowService.ts index d07c6d8addf..4a687027faf 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 31f1057999d..edf7fb27300 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 a940bd1a309..7671e480d79 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 600b2380d39..8c86a741081 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 45978737960..c60c940c4d5 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 e58051fda3f..80ff4f879d8 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 d5e8f029682..5e78b841010 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 071545be93b..8571e00fc4d 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://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.aiRelatedInformation.d.ts', aiTextSearchProvider: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.aiTextSearchProvider.d.ts', authGetSessions: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.authGetSessions.d.ts', + authLearnMore: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.authLearnMore.d.ts', authSession: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.authSession.d.ts', canonicalUriProvider: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.canonicalUriProvider.d.ts', chatParticipant: 'https://raw.githubusercontent.com/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 a2c2076994b..d4c2c6f025e 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 efaf3103c6c..6b67a036cb5 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 28f269d3bee..82dfcd4434d 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 10caaf7ed6a..1834c7f3f53 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,36 +61,108 @@ 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._isPurePromise = this._isPure(); - - this.isPure().then(r => { - if (r.isPure) { - return; // all is good - } - - this._prompt(); - }); + this._compute(); } - private _prompt(): void { + private async _compute(): Promise { + const { isPure } = await this.isPure(); + if (isPure) { + return; // all is good + } + + this.logService.warn(` + +!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! +!!! Installation has been modified on disk and is UNSUPPORTED. Please reinstall !!! +!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +`); + const storedData = this._storage.get(); if (storedData?.dontShowPrompt && storedData.commit === this.productService.commit) { return; // Do not prompt } + this._showBanner(); + this._showNotification(); + } + + private async _isPure(): Promise { + const expectedChecksums = this.productService.checksums || {}; + + await this.lifecycleService.when(LifecyclePhase.Eventually); + + const allResults = await Promise.all(Object.keys(expectedChecksums).map(filename => this._resolve(filename, expectedChecksums[filename]))); + + let isPure = true; + for (let i = 0, len = allResults.length; i < len; i++) { + if (!allResults[i].isPure) { + isPure = false; + break; + } + } + + return { + isPure, + proof: allResults + }; + } + + private async _resolve(filename: AppResourcePath, expected: string): Promise { + const fileUri = FileAccess.asFileUri(filename); + + try { + const checksum = await this.checksumService.checksum(fileUri); + + return IntegrityService._createChecksumPair(fileUri, checksum, expected); + } catch (error) { + return IntegrityService._createChecksumPair(fileUri, '', expected); + } + } + + private static _createChecksumPair(uri: URI, actual: string, expected: string): ChecksumPair { + return { + uri: uri, + actual: actual, + expected: expected, + 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) { @@ -114,56 +189,11 @@ export class IntegrityService implements IIntegrityService { this.notificationService.notify({ severity: Severity.Warning, message, - sticky: true + sticky: true, + priority: NotificationPriority.URGENT }); } } - - isPure(): Promise { - return this._isPurePromise; - } - - private async _isPure(): Promise { - const expectedChecksums = this.productService.checksums || {}; - - await this.lifecycleService.when(LifecyclePhase.Eventually); - - const allResults = await Promise.all(Object.keys(expectedChecksums).map(filename => this._resolve(filename, expectedChecksums[filename]))); - - let isPure = true; - for (let i = 0, len = allResults.length; i < len; i++) { - if (!allResults[i].isPure) { - isPure = false; - break; - } - } - - return { - isPure: isPure, - proof: allResults - }; - } - - private async _resolve(filename: AppResourcePath, expected: string): Promise { - const fileUri = FileAccess.asFileUri(filename); - - try { - const checksum = await this.checksumService.checksum(fileUri); - - return IntegrityService._createChecksumPair(fileUri, checksum, expected); - } catch (error) { - return IntegrityService._createChecksumPair(fileUri, '', expected); - } - } - - private static _createChecksumPair(uri: URI, actual: string, expected: string): ChecksumPair { - return { - uri: uri, - actual: actual, - expected: expected, - isPure: (actual === expected) - }; - } } 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 0a397839220..68dcd16ab14 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 c257d6ccc42..351cad96bc7 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 6aa7358deda..62aa7db5201 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 0ebb5a9f69d..c621e4dc2c9 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 42ffb1111ed..bb45e53d87b 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 f4203933951..693c4e9f0c0 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 cfb4bd79404..a9cb06c4565 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 8b833a0ce2a..40cea524423 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 142d49dadc1..3d817b86fea 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 c8bcb2d24a1..60ab56b329a 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 6d9b702cea6..f65d50c3aea 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,7 +19,7 @@ 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 enableWindowFocusOnElementFocus(): void { } diff --git a/src/vs/workbench/test/browser/workbenchTestServices.ts b/src/vs/workbench/test/browser/workbenchTestServices.ts index 4a05917faec..6fcf99d03f3 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 f9ed2a208bd..3319ce9ca8a 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 07a2b6f5c42..377187539be 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 9e3ead9be40..2715014a0a8 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 5644d00ce83..bf9d7d26045 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 8732b0e67e0..9cd0f8c1ccb 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. * - * - user consent not given - * - quote limits exceeded - * - model does not exist + * - *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. * - * @param languageModel A language model identifier. See {@link languageModels} for aviailable values. + * 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, 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. * @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 f434a0d1e6a..c96688f58da 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-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.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-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.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-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.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-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.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-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.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/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.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/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.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/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"